All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.hypersistence.tsid.TSID Maven / Gradle / Ivy

The newest version!
/*
 * MIT License
 * 
 * Copyright (c) 2020-2022 Fabio Lima
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package io.hypersistence.tsid;

import java.io.Serializable;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Instant;
import java.util.Random;
import java.util.SplittableRandom;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntFunction;
import java.util.function.IntSupplier;

/**
 * A value object that represents a Time-Sorted Unique Identifier (TSID).
 * 

* TSID is a 64-bit value that has 2 components: *

    *
  • Time component (42 bits): a number of milliseconds since * 1970-01-01 (Unix epoch). *
  • Random component (22 bits): a sequence of random bits generated by * a secure random generator. *
*

* The Random component has 2 sub-parts: *

    *
  • Node (0 to 20 bits): a number used to identify the machine or * node. *
  • Counter (2 to 22 bits): a randomly generated number that is * incremented whenever the time component is repeated. *
*

* The random component layout depend on the node bits. If the node bits are 10, * the counter bits are limited to 12. In this example, the maximum node value * is 2^10-1 = 1023 and the maximum counter value is 2^12-1 = 4093. So the * maximum TSIDs that can be generated per millisecond per node is 4096. *

* Instances of this class are immutable. * * @see Snowflake ID */ public final class TSID implements Serializable, Comparable { private static final long serialVersionUID = -5446820982139116297L; private final long number; /** * Number of bytes of a TSID. */ public static final int TSID_BYTES = 8; /** * Number of characters of a TSID. */ public static final int TSID_CHARS = 13; /** * Number of milliseconds of 2020-01-01T00:00:00.000Z. */ public static final long TSID_EPOCH = Instant.parse("2020-01-01T00:00:00.000Z").toEpochMilli(); static final int RANDOM_BITS = 22; static final int RANDOM_MASK = 0x003fffff; private static final char[] ALPHABET_UPPERCASE = // { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', // 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' }; private static final char[] ALPHABET_LOWERCASE = // { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', // 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' }; private static final long[] ALPHABET_VALUES = new long[128]; static { for (int i = 0; i < ALPHABET_VALUES.length; i++) { ALPHABET_VALUES[i] = -1; } // Numbers ALPHABET_VALUES['0'] = 0x00; ALPHABET_VALUES['1'] = 0x01; ALPHABET_VALUES['2'] = 0x02; ALPHABET_VALUES['3'] = 0x03; ALPHABET_VALUES['4'] = 0x04; ALPHABET_VALUES['5'] = 0x05; ALPHABET_VALUES['6'] = 0x06; ALPHABET_VALUES['7'] = 0x07; ALPHABET_VALUES['8'] = 0x08; ALPHABET_VALUES['9'] = 0x09; // Lower case ALPHABET_VALUES['a'] = 0x0a; ALPHABET_VALUES['b'] = 0x0b; ALPHABET_VALUES['c'] = 0x0c; ALPHABET_VALUES['d'] = 0x0d; ALPHABET_VALUES['e'] = 0x0e; ALPHABET_VALUES['f'] = 0x0f; ALPHABET_VALUES['g'] = 0x10; ALPHABET_VALUES['h'] = 0x11; ALPHABET_VALUES['j'] = 0x12; ALPHABET_VALUES['k'] = 0x13; ALPHABET_VALUES['m'] = 0x14; ALPHABET_VALUES['n'] = 0x15; ALPHABET_VALUES['p'] = 0x16; ALPHABET_VALUES['q'] = 0x17; ALPHABET_VALUES['r'] = 0x18; ALPHABET_VALUES['s'] = 0x19; ALPHABET_VALUES['t'] = 0x1a; ALPHABET_VALUES['v'] = 0x1b; ALPHABET_VALUES['w'] = 0x1c; ALPHABET_VALUES['x'] = 0x1d; ALPHABET_VALUES['y'] = 0x1e; ALPHABET_VALUES['z'] = 0x1f; // Lower case OIL ALPHABET_VALUES['o'] = 0x00; ALPHABET_VALUES['i'] = 0x01; ALPHABET_VALUES['l'] = 0x01; // Upper case ALPHABET_VALUES['A'] = 0x0a; ALPHABET_VALUES['B'] = 0x0b; ALPHABET_VALUES['C'] = 0x0c; ALPHABET_VALUES['D'] = 0x0d; ALPHABET_VALUES['E'] = 0x0e; ALPHABET_VALUES['F'] = 0x0f; ALPHABET_VALUES['G'] = 0x10; ALPHABET_VALUES['H'] = 0x11; ALPHABET_VALUES['J'] = 0x12; ALPHABET_VALUES['K'] = 0x13; ALPHABET_VALUES['M'] = 0x14; ALPHABET_VALUES['N'] = 0x15; ALPHABET_VALUES['P'] = 0x16; ALPHABET_VALUES['Q'] = 0x17; ALPHABET_VALUES['R'] = 0x18; ALPHABET_VALUES['S'] = 0x19; ALPHABET_VALUES['T'] = 0x1a; ALPHABET_VALUES['V'] = 0x1b; ALPHABET_VALUES['W'] = 0x1c; ALPHABET_VALUES['X'] = 0x1d; ALPHABET_VALUES['Y'] = 0x1e; ALPHABET_VALUES['Z'] = 0x1f; // Upper case OIL ALPHABET_VALUES['O'] = 0x00; ALPHABET_VALUES['I'] = 0x01; ALPHABET_VALUES['L'] = 0x01; } /** * Creates a new TSID. *

* This constructor wraps the input value in an immutable object. * * @param number a number */ public TSID(final long number) { this.number = number; } /** * Converts a number into a TSID. *

* This method wraps the input value in an immutable object. * * @param number a number * @return a TSID */ public static TSID from(final long number) { return new TSID(number); } /** * Converts a byte array into a TSID. * * @param bytes a byte array * @return a TSID * @throws IllegalArgumentException if bytes are null or its length is not 8 */ public static TSID from(final byte[] bytes) { if (bytes == null || bytes.length != TSID_BYTES) { throw new IllegalArgumentException("Invalid TSID bytes"); // null or wrong length! } long number = 0; number |= (bytes[0x0] & 0xffL) << 56; number |= (bytes[0x1] & 0xffL) << 48; number |= (bytes[0x2] & 0xffL) << 40; number |= (bytes[0x3] & 0xffL) << 32; number |= (bytes[0x4] & 0xffL) << 24; number |= (bytes[0x5] & 0xffL) << 16; number |= (bytes[0x6] & 0xffL) << 8; number |= (bytes[0x7] & 0xffL); return new TSID(number); } /** * Converts a canonical string into a TSID. *

* The input string must be 13 characters long and must contain only characters * from Crockford's base 32 alphabet. *

* The first character of the input string must be between 0 and F. * * @param string a canonical string * @return a TSID * @throws IllegalArgumentException if the input string is invalid * @see Crockford's Base 32 */ public static TSID from(final String string) { final char[] chars = toCharArray(string); long number = 0; number |= ALPHABET_VALUES[chars[0x00]] << 60; number |= ALPHABET_VALUES[chars[0x01]] << 55; number |= ALPHABET_VALUES[chars[0x02]] << 50; number |= ALPHABET_VALUES[chars[0x03]] << 45; number |= ALPHABET_VALUES[chars[0x04]] << 40; number |= ALPHABET_VALUES[chars[0x05]] << 35; number |= ALPHABET_VALUES[chars[0x06]] << 30; number |= ALPHABET_VALUES[chars[0x07]] << 25; number |= ALPHABET_VALUES[chars[0x08]] << 20; number |= ALPHABET_VALUES[chars[0x09]] << 15; number |= ALPHABET_VALUES[chars[0x0a]] << 10; number |= ALPHABET_VALUES[chars[0x0b]] << 5; number |= ALPHABET_VALUES[chars[0x0c]]; return new TSID(number); } /** * Converts the TSID into a number. *

* This method simply unwraps the internal value. * * @return an number. */ public long toLong() { return this.number; } /** * Converts the TSID into a byte array. * * @return an byte array. */ public byte[] toBytes() { final byte[] bytes = new byte[TSID_BYTES]; bytes[0x0] = (byte) (number >>> 56); bytes[0x1] = (byte) (number >>> 48); bytes[0x2] = (byte) (number >>> 40); bytes[0x3] = (byte) (number >>> 32); bytes[0x4] = (byte) (number >>> 24); bytes[0x5] = (byte) (number >>> 16); bytes[0x6] = (byte) (number >>> 8); bytes[0x7] = (byte) (number); return bytes; } /** * Returns a fast new TSID. *

* This static method is a quick alternative to {@link Factory#getTsid()}. *

* It employs {@link AtomicInteger} to generate up to 2^22 (4,194,304) TSIDs per * millisecond. It can be useful, for example, for logging. *

* Security-sensitive applications that require a cryptographically secure * pseudo-random generator should use {@link Factory#getTsid()}. *

* System property "tsidcreator.node" and environment variable * "TSID_NODE" are ignored by this method. Therefore, there will be * collisions if more than one process is generating TSIDs using this method. In * that case, {@link Factory#getTsid()} should be used in conjunction * with that property or variable. * * @return a TSID * @see {@link AtomicInteger} * @since 5.1.0 */ public static TSID fast() { final long time = (System.currentTimeMillis() - TSID_EPOCH) << RANDOM_BITS; final long tail = LazyHolder.counter.incrementAndGet() & RANDOM_MASK; return new TSID(time | tail); } /** * Converts the TSID into a canonical string in upper case. *

* The output string is 13 characters long and contains only characters from * Crockford's base 32 alphabet. *

* For lower case string, use the shorthand {@code Tsid.toLowerCase()} instead * of {@code Tsid.toString().toLowerCase()}. * * @return a TSID string * @see Crockford's Base 32 */ @Override public String toString() { return toString(ALPHABET_UPPERCASE); } /** * Converts the TSID into a canonical string in lower case. *

* The output string is 13 characters long and contains only characters from * Crockford's base 32 alphabet. *

* It is faster shorthand for {@code Tsid.toString().toLowerCase()}. * * @return a string * @see Crockford's Base 32 */ public String toLowerCase() { return toString(ALPHABET_LOWERCASE); } /** * Returns the instant of creation. *

* The instant of creation is extracted from the time component. * * @return {@link Instant} */ public Instant getInstant() { return Instant.ofEpochMilli(getUnixMilliseconds()); } /** * Returns the instant of creation. *

* The instant of creation is extracted from the time component. * * @param customEpoch the custom epoch instant * @return {@link Instant} */ public Instant getInstant(final Instant customEpoch) { return Instant.ofEpochMilli(getUnixMilliseconds(customEpoch.toEpochMilli())); } /** * Returns the time of creation in milliseconds since 1970-01-01. *

* The time of creation is extracted from the time component. * * @return the number of milliseconds since 1970-01-01 */ public long getUnixMilliseconds() { return this.getTime() + TSID_EPOCH; } /** * Returns the time of creation in milliseconds since 1970-01-01. *

* The time of creation is extracted from the time component. * * @param customEpoch the custom epoch in milliseconds since 1970-01-01 * @return the number of milliseconds since 1970-01-01 */ public long getUnixMilliseconds(final long customEpoch) { return this.getTime() + customEpoch; } /** * Returns the time component as a number. *

* The time component is a number between 0 and 2^42-1. * * @return a number of milliseconds. */ long getTime() { return this.number >>> RANDOM_BITS; } /** * Returns the random component as a number. *

* The time component is a number between 0 and 2^22-1. * * @return a number */ long getRandom() { return this.number & RANDOM_MASK; } /** * Checks if the input string is valid. *

* The input string must be 13 characters long and must contain only characters * from Crockford's base 32 alphabet. *

* The first character of the input string must be between 0 and F. * * @param string a string * @return true if valid */ public static boolean isValid(final String string) { return string != null && isValidCharArray(string.toCharArray()); } /** * Returns a hash code value for the TSID. */ @Override public int hashCode() { return (int) (number ^ (number >>> 32)); } /** * Checks if some other TSID is equal to this one. */ @Override public boolean equals(Object other) { if (other == null) return false; if (other.getClass() != TSID.class) return false; TSID that = (TSID) other; return (this.number == that.number); } /** * Compares two TSIDs as unsigned 64-bit integers. *

* The first of two TSIDs is greater than the second if the most significant * byte in which they differ is greater for the first TSID. * * @param that a TSID to be compared with * @return -1, 0 or 1 as {@code this} is less than, equal to, or greater than * {@code that} */ @Override public int compareTo(TSID that) { // used to compare as UNSIGNED longs final long min = 0x8000000000000000L; final long a = this.number + min; final long b = that.number + min; if (a > b) return 1; else if (a < b) return -1; return 0; } /** * Converts the TSID to a base-n encoded string. *

* Example: *

    *
  • TSID: 0AXS751X00W7C *
  • Base: 62 *
  • Output: 0T5jFDIkmmy *
*

* The output string is left padded with zeros. * * @param base a radix between 2 and 62 * @return a base-n encoded string * @since 5.2.0 */ public String encode(final int base) { if (base < 2 || base > 62) { throw new IllegalArgumentException(String.format("Invalid base: %s", base)); } return BaseN.encode(this, base); } /** * Converts a base-n encoded string to a TSID. *

* Example: *

    *
  • String: 05772439BB9F9074 *
  • Base: 16 *
  • Output: 0AXS476XSZ43M *
*

* The input string is left padded with zeros. * * @param string a base-n encoded string * @param base a radix between 2 and 62 * @return a TSID * @since 5.2.0 */ public static TSID decode(final String string, final int base) { if (base < 2 || base > 62) { throw new IllegalArgumentException(String.format("Invalid base: %s", base)); } return BaseN.decode(string, base); } /** * Converts the TSID to a string using a custom format. *

* The custom format uses a placeholder that will be substituted by the TSID * string. Only the first occurrence of a placeholder will replaced. *

* Placeholders: *

    *
  • %S: canonical string in upper case *
  • %s: canonical string in lower case *
  • %X: hexadecimal in upper case *
  • %x: hexadecimal in lower case *
  • %d: base-10 *
  • %z: base-62 *
*

* Examples: *

    *
  • An key that starts with a letter: *
      *
    • TSID: 0AWE5HZP3SKTK *
    • Format: K%S *
    • Output: K0AWE5HZP3SKTK *
    *
  • *
  • A file name in hexadecimal with a prefix and an extension: *
      *
    • TSID: 0AXFXR5W7VBX0 *
    • Format: DOC-%X.PDF *
    • Output: DOC-0575FDC1786137D6.PDF *
    *
  • *
*

* The opposite operation can be done by {@link TSID#unformat(String, String)}. * * @param format a custom format * @return a string using a custom format * @since 5.2.0 */ public String format(final String format) { if (format != null) { final int i = format.indexOf("%"); if (i < 0 || i == format.length() - 1) { throw new IllegalArgumentException(String.format("Invalid format string: \"%s\"", format)); } final String head = format.substring(0, i); final String tail = format.substring(i + 2); final char placeholder = format.charAt(i + 1); switch (placeholder) { case 'S': // canonical string in upper case return head + toString() + tail; case 's': // canonical string in lower case return head + toLowerCase() + tail; case 'X': // hexadecimal in upper case return head + BaseN.encode(this, 16) + tail; case 'x': // hexadecimal in lower case return head + BaseN.encode(this, 16).toLowerCase() + tail; case 'd': // base-10 return head + BaseN.encode(this, 10) + tail; case 'z': // base-62 return head + BaseN.encode(this, 62) + tail; default: throw new IllegalArgumentException(String.format("Invalid placeholder: \"%%%s\"", placeholder)); } } throw new IllegalArgumentException(String.format("Invalid format string: \"%s\"", format)); } /** * Converts a string using a custom format to a TSID. *

* This method does the opposite operation of {@link TSID#format(String)}. *

* Examples: *

    *
  • An key that starts with a letter: *
      *
    • String: K0AWE5HZP3SKTK *
    • Format: K%S *
    • Output: 0AWE5HZP3SKTK *
    *
  • *
  • A file name in hexadecimal with a prefix and an extension: *
      *
    • String: DOC-0575FDC1786137D6.PDF *
    • Format: DOC-%X.PDF *
    • Output: 0AXFXR5W7VBX0 *
    *
  • *
* * @param formatted a string using a custom format * @param format a custom format * @return a TSID * @since 5.2.0 */ public static TSID unformat(final String formatted, final String format) { if (formatted != null && format != null) { final int i = format.indexOf("%"); if (i < 0 || i == format.length() - 1) { throw new IllegalArgumentException(String.format("Invalid format string: \"%s\"", format)); } final String head = format.substring(0, i); final String tail = format.substring(i + 2); final char placeholder = format.charAt(i + 1); final int length = formatted.length() - head.length() - tail.length(); if (formatted.startsWith(head) && formatted.endsWith(tail)) { switch (placeholder) { case 'S': // canonical string (case insensitive) return TSID.from(formatted.substring(i, i + length)); case 's': // canonical string (case insensitive) return TSID.from(formatted.substring(i, i + length)); case 'X': // hexadecimal (case insensitive) return BaseN.decode(formatted.substring(i, i + length).toUpperCase(), 16); case 'x': // hexadecimal (case insensitive) return BaseN.decode(formatted.substring(i, i + length).toUpperCase(), 16); case 'd': // base-10 return BaseN.decode(formatted.substring(i, i + length), 10); case 'z': // base-62 return BaseN.decode(formatted.substring(i, i + length), 62); default: throw new IllegalArgumentException(String.format("Invalid placeholder: \"%%%s\"", placeholder)); } } } throw new IllegalArgumentException(String.format("Invalid formatted string: \"%s\"", formatted)); } String toString(final char[] alphabet) { final char[] chars = new char[TSID_CHARS]; chars[0x00] = alphabet[(int) ((number >>> 60) & 0b11111)]; chars[0x01] = alphabet[(int) ((number >>> 55) & 0b11111)]; chars[0x02] = alphabet[(int) ((number >>> 50) & 0b11111)]; chars[0x03] = alphabet[(int) ((number >>> 45) & 0b11111)]; chars[0x04] = alphabet[(int) ((number >>> 40) & 0b11111)]; chars[0x05] = alphabet[(int) ((number >>> 35) & 0b11111)]; chars[0x06] = alphabet[(int) ((number >>> 30) & 0b11111)]; chars[0x07] = alphabet[(int) ((number >>> 25) & 0b11111)]; chars[0x08] = alphabet[(int) ((number >>> 20) & 0b11111)]; chars[0x09] = alphabet[(int) ((number >>> 15) & 0b11111)]; chars[0x0a] = alphabet[(int) ((number >>> 10) & 0b11111)]; chars[0x0b] = alphabet[(int) ((number >>> 5) & 0b11111)]; chars[0x0c] = alphabet[(int) (number & 0b11111)]; return new String(chars); } static char[] toCharArray(final String string) { char[] chars = string == null ? null : string.toCharArray(); if (!isValidCharArray(chars)) { throw new IllegalArgumentException(String.format("Invalid TSID string: \"%s\"", string)); } return chars; } /** * Checks if the string is a valid TSID. * * A valid TSID string is a sequence of 13 characters from Crockford's base 32 * alphabet. * * The first character of the input string must be between 0 and F. * * @param chars a char array * @return boolean true if valid */ static boolean isValidCharArray(final char[] chars) { if (chars == null || chars.length != TSID_CHARS) { return false; // null or wrong size! } // The extra bit added by base-32 encoding must be zero // As a consequence, the 1st char of the input string must be between 0 and F. if ((ALPHABET_VALUES[chars[0]] & 0b10000) != 0) { return false; // overflow! } for (int i = 0; i < chars.length; i++) { if (ALPHABET_VALUES[chars[i]] == -1) { return false; // invalid character! } } return true; // It seems to be OK. } static class BaseN { private static final BigInteger MAX = BigInteger.valueOf(2).pow(64); private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // base-62 static String encode(final TSID tsid, final int base) { BigInteger x = new BigInteger(1, tsid.toBytes()); final BigInteger radix = BigInteger.valueOf(base); final int length = (int) Math.ceil(Long.SIZE / (Math.log(base) / Math.log(2))); int b = length; // buffer index char[] buffer = new char[length]; while (x.compareTo(BigInteger.ZERO) > 0) { BigInteger[] result = x.divideAndRemainder(radix); buffer[--b] = ALPHABET.charAt(result[1].intValue()); x = result[0]; } while (b > 0) { buffer[--b] = '0'; } return new String(buffer); } static TSID decode(final String string, final int base) { BigInteger x = BigInteger.ZERO; final BigInteger radix = BigInteger.valueOf(base); final int length = (int) Math.ceil(Long.SIZE / (Math.log(base) / Math.log(2))); if (string == null) { throw new IllegalArgumentException(String.format("Invalid base-%d string: null", base)); } if (string.length() != length) { throw new IllegalArgumentException(String.format("Invalid base-%d length: %s", base, string.length())); } for (int i = 0; i < string.length(); i++) { final long plus = (int) ALPHABET.indexOf(string.charAt(i)); if (plus < 0 || plus >= base) { throw new IllegalArgumentException( String.format("Invalid base-%d character: %s", base, string.charAt(i))); } x = x.multiply(radix).add(BigInteger.valueOf(plus)); } if (x.compareTo(MAX) > 0) { throw new IllegalArgumentException(String.format("Invalid base-%d value (overflow): %s", base, x)); } return new TSID(x.longValue()); } } private static class LazyHolder { private static final AtomicInteger counter = new AtomicInteger((new SplittableRandom()).nextInt()); } /** * A factory that actually generates Time-Sorted Unique Identifiers (TSID). *

* You can use this class to generate a Tsid or to make some customizations, * for example changing the default {@link SecureRandom} random generator to a faster pseudo-random generator. *

* If a system property "tsidcreator.node" or environment variable * "TSID_NODE" is defined, its value is utilized as node identifier. One * of them should be defined to embed a machine ID in the generated TSID * in order to avoid TSID collisions. Using that property or variable is * highly recommended. If no property or variable is defined, a random * node ID is generated at initialization. *

* If a system property "tsidcreator.node.count" or environment variable * "TSID_NODE_COUNT" is defined, its value is utilized by the * constructors of this class to adjust the amount of bits needed to embed the * node ID. For example, if the number 50 is given, the node bit amount is * adjusted to 6, which is the minimum number of bits to accommodate 50 nodes. * If no property or variable is defined, the number of bits reserved for node * ID is set to 10, which can accommodate 1024 nodes. *

* This class should be used as a singleton. Make sure that you create * and reuse a single instance of {@link Factory} per node in your * distributed system. */ public static final class Factory { private static final ReentrantLock LOCK = new ReentrantLock(); public static final Factory INSTANCE = new Factory(); public static final Factory INSTANCE_256 = newInstance256(); public static final Factory INSTANCE_1024 = newInstance1024(); public static final Factory INSTANCE_4096 = newInstance4096(); public static final IntSupplier THREAD_LOCAL_RANDOM_FUNCTION = () -> ThreadLocalRandom.current().nextInt(); private int counter; private long lastTime; private final int node; private final int nodeBits; private final int counterBits; private final int nodeMask; private final int counterMask; private final Clock clock; private final long customEpoch; private final IRandom random; private final int randomBytes; static final int NODE_BITS_256 = 8; static final int NODE_BITS_1024 = 10; static final int NODE_BITS_4096 = 12; // ****************************** // Constructors // ****************************** /** * It builds a new factory. *

* The node identifier provided by the "tsidcreator.node" system property or the * "TSID_NODE" environment variable is embedded in the generated TSIDs in * order to avoid collisions. It is highly recommended defining that * property or variable. Otherwise the node identifier will be randomly chosen. *

* If a system property "tsidcreator.node.count" or environment variable * "TSID_NODE_COUNT" is defined, its value is used to adjust the node * bits amount. */ public Factory() { this(builder()); } /** * It builds a new factory. *

* The node identifier provided by parameter is embedded in the generated TSIDs * in order to avoid collisions. *

* If a system property "tsidcreator.node.count" or environment variable * "TSID_NODE_COUNT" is defined, its value is used to adjust the node * bits amount. * * @param node the node identifier */ public Factory(int node) { this(builder().withNode(node)); } /** * It builds a generator with the given builder. * * @param builder a builder instance */ private Factory(Builder builder) { // setup node bits, custom epoch and random function this.customEpoch = builder.getCustomEpoch(); this.nodeBits = builder.getNodeBits(); this.random = builder.getRandom(); this.clock = builder.getClock(); // setup constants that depend on node bits this.counterBits = RANDOM_BITS - nodeBits; this.counterMask = RANDOM_MASK >>> nodeBits; this.nodeMask = RANDOM_MASK >>> counterBits; // setup how many bytes to get from the random function this.randomBytes = ((this.counterBits - 1) / 8) + 1; // setup the node identifier this.node = builder.getNode() & nodeMask; // finally, initialize internal state this.lastTime = clock.millis(); try { LOCK.lock(); this.counter = getRandomValue(); } finally { LOCK.unlock(); } } /** * Returns a new factory for up to 256 nodes and 16384 ID/ms. * * @return {@link Factory} */ public static Factory newInstance256() { return Factory.builder().withNodeBits(NODE_BITS_256).build(); } /** * Returns a new factory for up to 256 nodes and 16384 ID/ms. * * @param node the node identifier * @return {@link Factory} */ public static Factory newInstance256(int node) { return Factory.builder().withNodeBits(NODE_BITS_256).withNode(node).build(); } /** * Returns a new factory for up to 1024 nodes and 4096 ID/ms. * * It is equivalent to {@code new TsidFactory()}. * * @return {@link Factory} */ public static Factory newInstance1024() { return Factory.builder().withNodeBits(NODE_BITS_1024).build(); } /** * Returns a new factory for up to 1024 nodes and 4096 ID/ms. * * It is equivalent to {@code new TsidFactory(int)}. * * @param node the node identifier * @return {@link Factory} */ public static Factory newInstance1024(int node) { return Factory.builder().withNodeBits(NODE_BITS_1024).withNode(node).build(); } /** * Returns a new factory for up to 4096 nodes and 1024 ID/ms. * * @return {@link Factory} */ public static Factory newInstance4096() { return Factory.builder().withNodeBits(NODE_BITS_4096).build(); } /** * Returns a new factory for up to 4096 nodes and 1024 ID/ms. * * @param node the node identifier * @return {@link Factory} */ public static Factory newInstance4096(int node) { return Factory.builder().withNodeBits(NODE_BITS_4096).withNode(node).build(); } // ****************************** // Public methods // ****************************** /** * Returns a TSID. * * @return a TSID. */ public TSID generate() { final long _time; final long _counter; try { LOCK.lock(); _time = getTime() << RANDOM_BITS; _counter = (long) this.counter & this.counterMask; } finally { LOCK.unlock(); } final long _node = (long) this.node << this.counterBits; return new TSID(_time | _node | _counter); } /** * Returns the current time. *

* If the current time is equal to the previous time, the counter is incremented * by one. Otherwise, the counter is reset to a random value. *

* The maximum number of increment operations depend on the counter bits. For * example, if the counter bits is 12, the maximum number of increment * operations is 2^12 = 4096. * * @return the current time */ private long getTime() { long time = clock.millis(); if (time <= this.lastTime) { this.counter++; // Carry is 1 if an overflow occurs after ++. int carry = this.counter >>> this.counterBits; this.counter = this.counter & this.counterMask; time = this.lastTime + carry; // increment time } else { // If the system clock has advanced as expected, // simply reset the counter to a new random value. this.counter = this.getRandomValue(); } // save current time this.lastTime = time; // adjust to the custom epoch return time - this.customEpoch; } /** * Returns a random counter value from 0 to 0x3fffff (2^22-1 = 4,194,303). *

* The counter maximum value depends on the node identifier bits. For example, * if the node identifier has 10 bits, the counter has 12 bits. * * @return a number */ private int getRandomCounter() { if (random instanceof ByteRandom) { final byte[] bytes = random.nextBytes(this.randomBytes); switch (bytes.length) { case 1: return (bytes[0] & 0xff) & this.counterMask; case 2: return (((bytes[0] & 0xff) << 8) | (bytes[1] & 0xff)) & this.counterMask; default: return (((bytes[0] & 0xff) << 16) | ((bytes[1] & 0xff) << 8) | (bytes[2] & 0xff)) & this.counterMask; } } else { return random.nextInt() & this.counterMask; } } /** * Returns a random value based on the counter and the current Thread id. * * @return a number */ private int getRandomValue() { int randomCounter = getRandomCounter(); int threadId = (((int) (Thread.currentThread().getId()) % 256) << (counterBits - 8)); return (threadId | (randomCounter >> (counterBits - 8))); } /** * Returns a builder object. *

* It is used to build a custom {@link Factory}. */ public static Builder builder() { return new Builder(); } // ****************************** // Package-private inner classes // ****************************** /** * A nested class that builds custom TSID factories. *

* It is used to setup a custom {@link Factory}. */ public static class Builder { private Integer node; private Integer nodeBits; private Long customEpoch; private IRandom random; private Clock clock; /** * Set the node identifier. * * @param node a number that must be between 0 and 2^nodeBits-1. * @return {@link Builder} * @throws IllegalArgumentException if the node identifier is out of the range * [0, 2^nodeBits-1] when {@code build()} is * invoked */ public Builder withNode(Integer node) { this.node = node; return this; } /** * Set the node identifier bits length. * * @param nodeBits a number that must be between 0 and 20. * @return {@link Builder} * @throws IllegalArgumentException if the node bits are out of the range [0, * 20] when {@code build()} is invoked */ public Builder withNodeBits(Integer nodeBits) { this.nodeBits = nodeBits; return this; } /** * Set the custom epoch. * * @param customEpoch an instant that represents the custom epoch. * @return {@link Builder} */ public Builder withCustomEpoch(Instant customEpoch) { this.customEpoch = customEpoch.toEpochMilli(); return this; } /** * Set the random generator. *

* The random generator is used to create a random function that is used to * reset the counter when the millisecond changes. * * @param random a {@link Random} generator * @return {@link Builder} */ public Builder withRandom(Random random) { if (random != null) { if (random instanceof SecureRandom) { this.random = new ByteRandom(random); } else { this.random = new IntRandom(random); } } return this; } /** * Set the random function. *

* The random function is used to reset the counter when the millisecond * changes. * * @param randomFunction a random function that returns a integer value * @return {@link Builder} */ public Builder withRandomFunction(IntSupplier randomFunction) { this.random = new IntRandom(randomFunction); return this; } /** * Set the random function. *

* The random function must return a byte array of a given length. *

* The random function is used to reset the counter when the millisecond * changes. *

* Despite its name, the random function MAY return a fixed value, for example, * if your app requires the counter to be reset to ZERO whenever the millisecond * changes, like Twitter Snowflakes, this function should return an array filled * with ZEROS. * * @param randomFunction a random function that returns a byte array * @return {@link Builder} */ public Builder withRandomFunction(IntFunction randomFunction) { this.random = new ByteRandom(randomFunction); return this; } /** * Set the clock to be used in tests. * * @param clock a clock * @return {@link Builder} */ public Builder withClock(Clock clock) { this.clock = clock; return this; } /** * Get the node identifier. * * @return a number * @throws IllegalArgumentException if the node is out of range */ protected Integer getNode() { final int max = (1 << nodeBits) - 1; if (this.node == null) { if (Settings.getNode() != null) { // use property or variable this.node = Settings.getNode(); } else { // use random node identifier this.node = this.random.nextInt() & max; } } if (node < 0 || node > max) { node = Math.floorMod(node, max); } return this.node; } /** * Get the node identifier bits length within the range 0 to 20. * * @return a number * @throws IllegalArgumentException if the node bits are out of range */ protected Integer getNodeBits() { if (this.nodeBits == null) { if (Settings.getNodeCount() != null) { // use property or variable this.nodeBits = (int) Math.ceil(Math.log(Settings.getNodeCount()) / Math.log(2)); } else { // use default bit length: 10 bits this.nodeBits = Factory.NODE_BITS_1024; } } if (nodeBits < 0 || nodeBits > 20) { throw new IllegalArgumentException(String.format("Node bits out of range [0, 20]: %s", nodeBits)); } return this.nodeBits; } /** * Gets the custom epoch. * * @return a number */ protected Long getCustomEpoch() { if (this.customEpoch == null) { this.customEpoch = TSID_EPOCH; // 2020-01-01 } return this.customEpoch; } /** * Gets the random generator. * * @return a random generator */ protected IRandom getRandom() { if (this.random == null) { this.withRandom(new SecureRandom()); } return this.random; } /** * Gets the clock to be used in tests. * * @return a clock */ protected Clock getClock() { if (this.clock == null) { this.withClock(Clock.systemUTC()); } return this.clock; } /** * Returns a build TSID factory. * * @return {@link Factory} * @throws IllegalArgumentException if the node is out of range * @throws IllegalArgumentException if the node bits are out of range */ public Factory build() { return new Factory(this); } } interface IRandom { int nextInt(); byte[] nextBytes(int length); } static class IntRandom implements IRandom { private final IntSupplier randomFunction; public IntRandom() { this(newRandomFunction(null)); } public IntRandom(Random random) { this(newRandomFunction(random)); } public IntRandom(IntSupplier randomFunction) { this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null); } @Override public int nextInt() { return randomFunction.getAsInt(); } @Override public byte[] nextBytes(int length) { int shift = 0; long random = 0; final byte[] bytes = new byte[length]; for (int i = 0; i < length; i++) { if (shift < Byte.SIZE) { shift = Integer.SIZE; random = randomFunction.getAsInt(); } shift -= Byte.SIZE; // 56, 48, 40... bytes[i] = (byte) (random >>> shift); } return bytes; } protected static IntSupplier newRandomFunction(Random random) { final Random entropy = random != null ? random : new SecureRandom(); return entropy::nextInt; } } static class ByteRandom implements IRandom { private final IntFunction randomFunction; public ByteRandom() { this(newRandomFunction(null)); } public ByteRandom(Random random) { this(newRandomFunction(random)); } public ByteRandom(IntFunction randomFunction) { this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null); } @Override public int nextInt() { int number = 0; byte[] bytes = this.randomFunction.apply(Integer.BYTES); for (int i = 0; i < Integer.BYTES; i++) { number = (number << 8) | (bytes[i] & 0xff); } return number; } @Override public byte[] nextBytes(int length) { return this.randomFunction.apply(length); } protected static IntFunction newRandomFunction(Random random) { final Random entropy = random != null ? random : new SecureRandom(); return (final int length) -> { final byte[] bytes = new byte[length]; entropy.nextBytes(bytes); return bytes; }; } } static class Settings { static final String NODE = "tsid.node"; static final String NODE_COUNT = "tsid.node.count"; private Settings() { } public static Integer getNode() { return getPropertyAsInteger(NODE); } public static Integer getNodeCount() { return getPropertyAsInteger(NODE_COUNT); } static Integer getPropertyAsInteger(String property) { try { return Integer.decode(getProperty(property)); } catch (NumberFormatException | NullPointerException e) { return null; } } static String getProperty(String name) { String property = System.getProperty(name); if (property != null && !property.isEmpty()) { return property; } String variable = System.getenv(name.toUpperCase().replace(".", "_")); if (variable != null && !variable.isEmpty()) { return variable; } return null; } } /** * Returns a new TSID. *

* The node ID is is set by defining the system property "tsid.node" or * the environment variable "TSID_NODE". One of them should be * used to embed a machine ID in the generated TSID in order to avoid TSID * collisions. If that property or variable is not defined, the node ID is * chosen randomly. *

* The amount of nodes can be set by defining the system property * "tsid.node.count" or the environment variable * "TSID_NODE_COUNT". That property or variable is used to adjust the * minimum amount of bits to accommodate the node ID. If that property or * variable is not defined, the default amount of nodes is 1024, which takes 10 * bits. *

* The amount of bits needed to accommodate the node ID is calculated by this * pseudo-code formula: {@code node_bits = ceil(log(node_count)/log(2))}. *

* Random component settings: *

    *
  • Node bits: node_bits *
  • Counter bits: 22-node_bits *
  • Maximum node: 2^node_bits *
  • Maximum counter: 2^(22-node_bits) *
*

* The time component can be 1 ms or more ahead of the system time when * necessary to maintain monotonicity and generation speed. * * @return a TSID * @since 5.1.0 */ public static TSID getTsid() { return INSTANCE.generate(); } /** * Returns a new TSID. *

* It supports up to 256 nodes. *

* It can generate up to 16,384 TSIDs per millisecond per node. *

* The node ID is is set by defining the system property "tsid.node" or * the environment variable "TSID_NODE". One of them should be * used to embed a machine ID in the generated TSID in order to avoid TSID * collisions. If that property or variable is not defined, the node ID is * chosen randomly. * *

* Random component settings: *

    *
  • Node bits: 8 *
  • Counter bits: 14 *
  • Maximum node: 256 (2^8) *
  • Maximum counter: 16,384 (2^14) *
*

* The time component can be 1 ms or more ahead of the system time when * necessary to maintain monotonicity and generation speed. * * @return a TSID */ public static TSID getTsid256() { return INSTANCE_256.generate(); } /** * Returns a new TSID. *

* It supports up to 1,024 nodes. *

* It can generate up to 4,096 TSIDs per millisecond per node. *

* The node ID is is set by defining the system property "tsid.node" or * the environment variable "TSID_NODE". One of them should be * used to embed a machine ID in the generated TSID in order to avoid TSID * collisions. If that property or variable is not defined, the node ID is * chosen randomly. *

* Random component settings: *

    *
  • Node bits: 10 *
  • Counter bits: 12 *
  • Maximum node: 1,024 (2^10) *
  • Maximum counter: 4,096 (2^12) *
*

* The time component can be 1 ms or more ahead of the system time when * necessary to maintain monotonicity and generation speed. * * @return a TSID */ public static TSID getTsid1024() { return INSTANCE_1024.generate(); } /** * Returns a new TSID. *

* It supports up to 4,096 nodes. *

* It can generate up to 1,024 TSIDs per millisecond per node. *

* The node ID is is set by defining the system property "tsid.node" or * the environment variable "TSID_NODE". One of them should be * used to embed a machine ID in the generated TSID in order to avoid TSID * collisions. If that property or variable is not defined, the node ID is * chosen randomly. *

* Random component settings: *

    *
  • Node bits: 12 *
  • Counter bits: 10 *
  • Maximum node: 4,096 (2^12) *
  • Maximum counter: 1,024 (2^10) *
*

* The time component can be 1 ms or more ahead of the system time when * necessary to maintain monotonicity and generation speed. * * @return a TSID number */ public static TSID getTsid4096() { return INSTANCE_4096.generate(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy