diff --git a/.gitignore b/.gitignore index 0b4a50f..c5884f9 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,11 @@ buildNumber.properties # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* - +.idea +*.iml + +notes/* + +docs/javadoc/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8ce4e..3d27b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,13 @@ All notable changes to this project will be documented in this file. Nothing unreleased. +## [5.0.2] - 2022-09-17 + +Rewrite docs. #21 + ## [5.0.1] - 2022-08-21 -Optimized comparison and hash. #20 +Optimize comparison and hash. #20 ## [5.0.0] - 2022-07-09 @@ -294,7 +298,8 @@ Project created as an alternative Java implementation of [ULID spec](https://git - Added `LICENSE` - Added test cases -[unreleased]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-5.0.1...HEAD +[unreleased]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-5.0.2...HEAD +[5.0.2]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-5.0.0...ulid-creator-5.0.2 [5.0.1]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-5.0.0...ulid-creator-5.0.1 [5.0.0]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.2.1...ulid-creator-5.0.0 [4.2.1]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.2.0...ulid-creator-4.2.1 diff --git a/README.md b/README.md index c519ca7..9a187ec 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,11 @@ In summary: * String format is encoded to [Crockford's base32](https://www.crockford.com/base32.html); * String format is URL safe, is case insensitive, and has no hyphens. -This library contains a good amount of [unit tests](https://github.com/f4b6a3/ulid-creator/tree/master/src/test/java/com/github/f4b6a3/ulid). It also has a [micro benchmark](https://github.com/f4b6a3/ulid-creator/tree/master/benchmark) for you to check if the performance is good enough. +This project contains a [micro benchmark](https://github.com/f4b6a3/ulid-creator/tree/master/benchmark) and a good amount of [unit tests](https://github.com/f4b6a3/ulid-creator/tree/master/src/test/java/com/github/f4b6a3/ulid). + +The jar file can be downloaded directly from [maven.org](https://repo1.maven.org/maven2/com/github/f4b6a3/ulid-creator/). + +Read the [Javadocs](https://javadoc.io/doc/com.github.f4b6a3/ulid-creator). How to Use ------------------------------------------------------ @@ -39,7 +43,7 @@ Add these lines to your `pom.xml`. com.github.f4b6a3 ulid-creator - 5.0.1 + 5.0.2 ``` See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). @@ -191,9 +195,9 @@ A `UlidFactory` with `java.util.Random`: ```java // use a `java.util.Random` instance for fast generation UlidFactory factory = UlidFactory.newInstance(new Random()); -Ulid ulid = factory.create(); // use the factory +Ulid ulid = factory.create(); ``` --- @@ -243,26 +247,26 @@ Ulid ulid = factory.create(); Benchmark ------------------------------------------------------ -This section shows benchmarks comparing `UlidCreator` to `java.util.UUID`. +This section shows benchmarks comparing `UlidCreator` to `UUID.randomUUID()`. ``` -------------------------------------------------------------------------------- -THROUGHPUT (operations/msec) Mode Cnt Score Error Units +THROUGHPUT (operations/msec) Mode Cnt Score Error Units -------------------------------------------------------------------------------- -UUID_randomUUID thrpt 5 2060,570 ± 37,242 ops/ms -UUID_randomUUID_toString thrpt 5 1177,386 ± 39,803 ops/ms +UUID_randomUUID thrpt 5 3459,889 ± 98,257 ops/ms +UUID_randomUUID_toString thrpt 5 3148,298 ± 159,507 ops/ms - - - - - - - - - - - - - - - - - - - - - - - - - - - -UlidCreator_getUlid thrpt 5 2740,609 ± 86,350 ops/ms -UlidCreator_getUlid_toString thrpt 5 2526,284 ± 56,726 ops/ms +UlidCreator_getUlid thrpt 5 4276,614 ± 11,069 ops/ms +UlidCreator_getUlid_toString thrpt 5 3645,088 ± 85,478 ops/ms - - - - - - - - - - - - - - - - - - - - - - - - - - - -UlidCreator_getMonotonicUlid thrpt 5 19373,150 ± 192,402 ops/ms -UlidCreator_getMonotonicUlid_toString thrpt 5 13269,201 ± 254,953 ops/ms +UlidCreator_getMonotonicUlid thrpt 5 32921,698 ± 1286,983 ops/ms +UlidCreator_getMonotonicUlid_toString thrpt 5 18541,252 ± 710,281 ops/ms -------------------------------------------------------------------------------- -Total time: 00:08:01 +Total time: 00:02:01 -------------------------------------------------------------------------------- ``` -System: JVM 8, Ubuntu 20.04, CPU i5-3330, 8G RAM. +System: CPU i7-8565U, 16G RAM, Ubuntu 22.04, JVM 11, rng-tools installed. To execute the benchmark, run `./benchmark/run.sh`. @@ -279,3 +283,4 @@ License ------------------------------------------------------ This library is Open Source software released under the [MIT license](https://opensource.org/licenses/MIT). + diff --git a/benchmark/src/main/java/benchmark/Throughput.java b/benchmark/src/main/java/benchmark/Throughput.java index 23ae6ea..3588c07 100644 --- a/benchmark/src/main/java/benchmark/Throughput.java +++ b/benchmark/src/main/java/benchmark/Throughput.java @@ -21,9 +21,9 @@ import com.github.f4b6a3.ulid.UlidCreator; @Fork(1) @Threads(1) @State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) @Warmup(iterations = 5, time = 1) @Measurement(iterations = 5, time = 3) -@BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class Throughput { diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..654310f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ +To generate the javadocs, run the script `doc/run.sh`. + +The docs are generated inside of `docs/javadoc`. + +It can be useful to read offline. + diff --git a/docs/run.sh b/docs/run.sh new file mode 100755 index 0000000..0050851 --- /dev/null +++ b/docs/run.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# find the script folder +SCRIPT_DIR=$(dirname "$0") + +# go to the parent folder +cd "${SCRIPT_DIR}/.." + +# clear old docs +rm -rf docs/javadoc + +# generate new docs +find src/main/java/com/github/f4b6a3/ulid/ -name "*.java" | xargs javadoc -d docs/javadoc + diff --git a/pom.xml b/pom.xml index 026a698..ea20ff3 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ulid-creator http://github.com/f4b6a3/ulid-creator - A Java library for generating and handling ULIDs (Universally Unique Lexicographically Sortable Identifier). + A Java library for generating ULIDs (Universally Unique Lexicographically Sortable Identifier). diff --git a/src/main/java/com/github/f4b6a3/ulid/Ulid.java b/src/main/java/com/github/f4b6a3/ulid/Ulid.java index 8b6f63a..c9a3eea 100644 --- a/src/main/java/com/github/f4b6a3/ulid/Ulid.java +++ b/src/main/java/com/github/f4b6a3/ulid/Ulid.java @@ -29,17 +29,22 @@ import java.time.Instant; import java.util.UUID; /** - * This class represents a ULID. + * A class that represents ULIDs. + *

+ * ULID is 128-bit value that has two components: + *

+ *

+ * ULID has 128-bit compatibility with {@link UUID}. Like a UUID, a ULID can + * also be stored as a 16-byte array. + *

+ * Instances of this class are immutable. * - * The ULID has two components: - * - * - Time component: a part of 48 bits that represent the amount of milliseconds - * since Unix Epoch, 1970-01-01. - * - * - Random component: a byte array of 80 bits that has a random value generated - * a secure random generator. - * - * Instances of this class are immutable. + * @see ULID Specification */ public final class Ulid implements Serializable, Comparable { @@ -48,12 +53,30 @@ public final class Ulid implements Serializable, Comparable { private final long msb; // most significant bits private final long lsb; // least significant bits + /** + * Number of characters of a ULID. + */ public static final int ULID_CHARS = 26; + /** + * Number of characters of the time component of a ULID. + */ public static final int TIME_CHARS = 10; + /** + * Number of characters of the random component of a ULID. + */ public static final int RANDOM_CHARS = 16; + /** + * Number of bytes of a ULID. + */ public static final int ULID_BYTES = 16; + /** + * Number of bytes of the time component of a ULID. + */ public static final int TIME_BYTES = 6; + /** + * Number of bytes of the random component of a ULID. + */ public static final int RANDOM_BYTES = 10; private static final char[] ALPHABET_UPPERCASE = // @@ -142,8 +165,8 @@ public final class Ulid implements Serializable, Comparable { private static final long INCREMENT_OVERFLOW = 0x0000000000000000L; /** - * Create a new ULID. - * + * Creates a new ULID. + *

* Useful to make copies of ULIDs. * * @param ulid a ULID @@ -154,7 +177,10 @@ public final class Ulid implements Serializable, Comparable { } /** - * Create a new ULID. + * Creates a new ULID. + *

+ * If you want to make a copy of a {@link UUID}, use {@link Ulid#from(UUID)} + * instead. * * @param mostSignificantBits the first 8 bytes as a long value * @param leastSignificantBits the last 8 bytes as a long value @@ -165,10 +191,17 @@ public final class Ulid implements Serializable, Comparable { } /** - * Create a new ULID. + * Creates a new ULID. + *

+ * Time parameter is the number of milliseconds since 1970-01-01 (Unix epoch). + * It must be a positive number not larger than 2^48-1. + *

+ * Random parameter must be an array of 10 bytes. * - * @param time the time component in milliseconds since 1970-01-01 - * @param random the random component in byte array + * @param time the the number of milliseconds since 1970-01-01 + * @param random an array of 10 bytes + * @throws IllegalArgumentException if time is negative or larger than 2^48-1 + * @throws IllegalArgumentException if random is null or its length is not 10 */ public Ulid(long time, byte[] random) { @@ -177,7 +210,7 @@ public final class Ulid implements Serializable, Comparable { // ULID specification: // "Any attempt to decode or encode a ULID larger than this (time > 2^48-1) // should be rejected by all implementations, to prevent overflow bugs." - throw new IllegalArgumentException("Invalid time value"); // time overflow! + throw new IllegalArgumentException("Invalid time value"); // overflow or negative time! } // The random component has 80 bits (10 bytes). if (random == null || random.length != RANDOM_BYTES) { @@ -217,8 +250,9 @@ public final class Ulid implements Serializable, Comparable { /** * Converts a byte array into a ULID. * - * @param bytes a byte array + * @param bytes an array of 16 bytes * @return a ULID + * @throws IllegalArgumentException if bytes are null or its length is not 16 */ public static Ulid from(byte[] bytes) { @@ -252,14 +286,16 @@ public final class Ulid implements Serializable, Comparable { /** * Converts a canonical string into a ULID. - * + *

* The input string must be 26 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 7. * * @param string a canonical string * @return a ULID + * @throws IllegalArgumentException if the input string is invalid + * @see Crockford's Base 32 */ public static Ulid from(String string) { @@ -306,8 +342,10 @@ public final class Ulid implements Serializable, Comparable { /** * Convert the ULID into a UUID. - * - * If you need a RFC-4122 UUID v4 do this: {@code Ulid.toRfc4122().toUuid()}. + *

+ * A ULID has 128-bit compatibility with a {@link UUID}. + *

+ * If you need a RFC-4122 UUIDv4 do this: {@code Ulid.toRfc4122().toUuid()}. * * @return a UUID. */ @@ -347,16 +385,15 @@ public final class Ulid implements Serializable, Comparable { /** * Converts the ULID into a canonical string in upper case. - * + *

* The output string is 26 characters long and contains only characters from - * Crockford's base 32 alphabet. - * + * Crockford's Base 32 alphabet. + *

* For lower case string, use the shorthand {@code Ulid#toLowerCase()}, instead * of {@code Ulid#toString()#toLowerCase()}. * - * See: https://www.crockford.com/base32.html - * * @return a ULID string + * @see Crockford's Base 32 */ @Override public String toString() { @@ -365,47 +402,32 @@ public final class Ulid implements Serializable, Comparable { /** * Converts the ULID into a canonical string in lower case. - * + *

* The output string is 26 characters long and contains only characters from - * Crockford's base 32 alphabet. - * + * Crockford's Base 32 alphabet. + *

* It is a shorthand at least twice as fast as * {@code Ulid.toString().toLowerCase()}. * - * See: https://www.crockford.com/base32.html - * * @return a string + * @see Crockford's Base 32 */ public String toLowerCase() { return toString(ALPHABET_LOWERCASE); } /** - * Converts the ULID into into another ULID that is compatible with UUID v4. - * + * Converts the ULID into into another ULID that is compatible with UUIDv4. + *

* The bytes of the returned ULID are compliant with the RFC-4122 version 4. - * - * If you need a RFC-4122 UUID v4 do this: {@code Ulid.toRfc4122().toUuid()}. - * - * Read: https://tools.ietf.org/html/rfc4122 - * - * ### RFC-4122 - 4.4. Algorithms for Creating a UUID from Truly Random or - * Pseudo-Random Numbers - * - * The version 4 UUID is meant for generating UUIDs from truly-random or - * pseudo-random numbers. - * - * The algorithm is as follows: - * - * - Set the two most significant bits (bits 6 and 7) of the - * clock_seq_hi_and_reserved to zero and one, respectively. - * - * - Set the four most significant bits (bits 12 through 15) of the - * time_hi_and_version field to the 4-bit version number from Section 4.1.3. - * - * - Set all the other bits to randomly (or pseudo-randomly) chosen values. + *

+ * If you need a RFC-4122 UUIDv4 do this: {@code Ulid.toRfc4122().toUuid()}. + *

+ * Note: If you use this method, you can not get the original ULID, since + * it changes 6 bits of it to generate a UUIDv4. * * @return a ULID + * @see RFC-4122 */ public Ulid toRfc4122() { @@ -419,10 +441,10 @@ public final class Ulid implements Serializable, Comparable { /** * Returns the instant of creation. - * + *

* The instant of creation is extracted from the time component. * - * @return {@link Instant} + * @return the {@link Instant} of creation */ public Instant getInstant() { return Instant.ofEpochMilli(this.getTime()); @@ -430,11 +452,12 @@ public final class Ulid implements Serializable, Comparable { /** * Returns the instant of creation. - * + *

* The instant of creation is extracted from the time component. * * @param string a canonical string - * @return {@link Instant} + * @return the {@link Instant} of creation + * @throws IllegalArgumentException if the input string is invalid */ public static Instant getInstant(String string) { return Instant.ofEpochMilli(getTime(string)); @@ -442,11 +465,11 @@ public final class Ulid implements Serializable, Comparable { /** * Returns the time component as a number. - * + *

* The time component is a number between 0 and 2^48-1. It is equivalent to the * count of milliseconds since 1970-01-01 (Unix epoch). * - * @return a number of milliseconds. + * @return a number of milliseconds */ public long getTime() { return this.msb >>> 16; @@ -454,12 +477,13 @@ public final class Ulid implements Serializable, Comparable { /** * Returns the time component as a number. - * + *

* The time component is a number between 0 and 2^48-1. It is equivalent to the * count of milliseconds since 1970-01-01 (Unix epoch). * * @param string a canonical string - * @return a number of milliseconds. + * @return a number of milliseconds + * @throws IllegalArgumentException if the input string is invalid */ public static long getTime(String string) { @@ -483,7 +507,7 @@ public final class Ulid implements Serializable, Comparable { /** * Returns the random component as a byte array. - * + *

* The random component is an array of 10 bytes (80 bits). * * @return a byte array @@ -509,11 +533,12 @@ public final class Ulid implements Serializable, Comparable { /** * Returns the random component as a byte array. - * + *

* The random component is an array of 10 bytes (80 bits). * * @param string a canonical string * @return a byte array + * @throws IllegalArgumentException if the input string is invalid */ public static byte[] getRandom(String string) { @@ -577,19 +602,20 @@ public final class Ulid implements Serializable, Comparable { /** * Returns a new ULID by incrementing the random component of the current ULID. - * + *

* Since the random component contains 80 bits: - * - * (1) This method can generate up to 1208925819614629174706176 (2^80) ULIDs per - * millisecond; - * - * (2) This method can generate monotonic increasing ULIDs 99.999999999999992% - * ((2^80 - 10^9) / (2^80)) of the time within a single millisecond interval, - * considering an unrealistic rate of 1,000,000,000 ULIDs per millisecond. - * + *

+ *

* Due to (1) and (2), it does not throw the error message recommended by the * specification. When an overflow occurs in the random 80 bits, the time - * component is simply incremented. + * component is simply incremented to maintain monotonicity. * * @return a ULID */ @@ -607,39 +633,56 @@ public final class Ulid implements Serializable, Comparable { /** * Checks if the input string is valid. - * + *

* The input string must be 26 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 7. * - * @param string a string - * @return true if valid + * @param string a canonical string + * @return true if the input string is valid + * @see Crockford's Base 32 */ public static boolean isValid(String string) { return string != null && isValidCharArray(string.toCharArray()); } + /** + * Returns a hash code value for the ULID. + */ @Override public int hashCode() { final long bits = msb ^ lsb; return (int) (bits ^ (bits >>> 32)); } + /** + * Checks if some other ULID is equal to this one. + */ @Override - public boolean equals(Object obj) { - if (obj == null) + public boolean equals(Object other) { + if (other == null) return false; - if (obj.getClass() != Ulid.class) + if (other.getClass() != Ulid.class) return false; - Ulid that = (Ulid) obj; + Ulid that = (Ulid) other; if (lsb != that.lsb) return false; - if (msb != that.msb) + else if (msb != that.msb) return false; return true; } + /** + * Compares two ULIDs as unsigned 128-bit integers. + *

+ * The first of two ULIDs is greater than the second if the most significant + * byte in which they differ is greater for the first UUID. + * + * @param that a ULID 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(Ulid that) { @@ -665,7 +708,7 @@ public final class Ulid implements Serializable, Comparable { return 0; } - protected String toString(char[] alphabet) { + String toString(char[] alphabet) { final char[] chars = new char[ULID_CHARS]; @@ -705,7 +748,7 @@ public final class Ulid implements Serializable, Comparable { return new String(chars); } - protected static char[] toCharArray(String string) { + static char[] toCharArray(String string) { char[] chars = string == null ? null : string.toCharArray(); if (!isValidCharArray(chars)) { throw new IllegalArgumentException(String.format("Invalid ULID: \"%s\"", string)); @@ -713,18 +756,15 @@ public final class Ulid implements Serializable, Comparable { return chars; } - /** + /* * Checks if the string is a valid ULID. * - * A valid ULID string is a sequence of 26 characters from Crockford's base 32 + * A valid ULID string is a sequence of 26 characters from Crockford's Base 32 * alphabet. * * The first character of the input string must be between 0 and 7. - * - * @param chars a char array - * @return boolean true if valid */ - protected static boolean isValidCharArray(final char[] chars) { + static boolean isValidCharArray(final char[] chars) { if (chars == null || chars.length != ULID_CHARS) { return false; // null or wrong size! diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java index ef56a09..8a1aa0d 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -25,17 +25,10 @@ package com.github.f4b6a3.ulid; /** - * A class for generating ULIDs. - * - * The ULID has two components: - * - * - Time component: a part of 48 bits that represent the number of milliseconds - * since Unix Epoch, 1970-01-01. - * - * - Random component: a byte array of 80 bits that has a random value generated - * a secure random generator. - * - * The maximum ULIDs that can be generated per millisecond is 2^80. + * A class that generates ULIDs. + *

+ * Both types of ULID can be easily created by this generator, i.e. monotonic + * and non-monotonic. */ public final class UlidCreator { @@ -45,8 +38,6 @@ public final class UlidCreator { /** * Returns a ULID. * - * The random component is always reset to a new random value. - * * @return a ULID */ public static Ulid getUlid() { @@ -56,9 +47,7 @@ public final class UlidCreator { /** * Returns a ULID with a given time. * - * The time must be the number of milliseconds since 1970-01-01 (Unix epoch). - * - * @param time a given time + * @param time a number of milliseconds since 1970-01-01 (Unix epoch). * @return a ULID */ public static Ulid getUlid(final long time) { @@ -68,11 +57,6 @@ public final class UlidCreator { /** * Returns a Monotonic ULID. * - * The random component is reset to a new value whenever the time changes. - * - * If more than one ULID is generated within the same time, the random component - * is incremented by one. - * * @return a ULID */ public static Ulid getMonotonicUlid() { @@ -82,14 +66,7 @@ public final class UlidCreator { /** * Returns a Monotonic ULID with a given time. * - * The time must be the number of milliseconds since 1970-01-01 (Unix epoch). - * - * The random component is reset to a new value whenever the time changes. - * - * If more than one ULID is generated within the same time, the random component - * is incremented by one. - * - * @param time a given time + * @param time a number of milliseconds since 1970-01-01 (Unix epoch). * @return a ULID */ public static Ulid getMonotonicUlid(final long time) { diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java b/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java index 8aa53f6..ae98301 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java @@ -32,14 +32,19 @@ import java.util.function.LongFunction; import java.util.function.LongSupplier; /** - * Factory that generates ULIDs. - * - * If the factory is not monotonic, the random component always changes. - * - * If the factory is monotonic, the random component changes whenever the - * millisecond changes. If more than one ULID is generated within the same - * millisecond, the random component is incremented by one. - * + * A class that actually generates ULIDs. + *

+ * This class is used by {@link UlidCreator}. + *

+ * You can use this class if you need to use a specific random generator + * strategy. However, most people just need {@link UlidCreator}. + *

+ * Instances of this class can behave in one of two ways: monotonic or + * non-monotonic (default). + *

+ * If the factory is monotonic, the random component is incremented by 1 If more + * than one ULID is generated within the same millisecond. + *

* The maximum ULIDs that can be generated per millisecond is 2^80. */ public final class UlidFactory { @@ -47,8 +52,15 @@ public final class UlidFactory { private final Clock clock; // for tests private final LongFunction ulidFunction; + // ****************************** + // Constructors + // ****************************** + + /** + * Default constructor. + */ public UlidFactory() { - this(new UlidFunction()); + this(new UlidFunction(IRandom.newInstance())); } private UlidFactory(LongFunction ulidFunction) { @@ -62,13 +74,13 @@ public final class UlidFactory { /** * Returns a new factory. - * + *

* It is equivalent to {@code new UlidFactory()}. * * @return {@link UlidFactory} */ public static UlidFactory newInstance() { - return new UlidFactory(new UlidFunction()); + return new UlidFactory(new UlidFunction(IRandom.newInstance())); } /** @@ -78,31 +90,31 @@ public final class UlidFactory { * @return {@link UlidFactory} */ public static UlidFactory newInstance(Random random) { - return new UlidFactory(new UlidFunction(random)); + return new UlidFactory(new UlidFunction(IRandom.newInstance(random))); } /** * Returns a new factory. - * + *

* The given random function must return a long value. * * @param randomFunction a random function that returns a long value * @return {@link UlidFactory} */ public static UlidFactory newInstance(LongSupplier randomFunction) { - return new UlidFactory(new UlidFunction(randomFunction)); + return new UlidFactory(new UlidFunction(IRandom.newInstance(randomFunction))); } /** * Returns a new factory. - * + *

* The given random function must return a byte array. * * @param randomFunction a random function that returns a byte array * @return {@link UlidFactory} */ public static UlidFactory newInstance(IntFunction randomFunction) { - return new UlidFactory(new UlidFunction(randomFunction)); + return new UlidFactory(new UlidFunction(IRandom.newInstance(randomFunction))); } /** @@ -111,7 +123,7 @@ public final class UlidFactory { * @return {@link UlidFactory} */ public static UlidFactory newMonotonicInstance() { - return new UlidFactory(new MonotonicFunction()); + return new UlidFactory(new MonotonicFunction(IRandom.newInstance())); } /** @@ -121,59 +133,63 @@ public final class UlidFactory { * @return {@link UlidFactory} */ public static UlidFactory newMonotonicInstance(Random random) { - return new UlidFactory(new MonotonicFunction(random)); + return new UlidFactory(new MonotonicFunction(IRandom.newInstance(random))); } /** * Returns a new monotonic factory. - * + *

* The given random function must return a long value. * * @param randomFunction a random function that returns a long value * @return {@link UlidFactory} */ public static UlidFactory newMonotonicInstance(LongSupplier randomFunction) { - return new UlidFactory(new MonotonicFunction(randomFunction)); + return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction))); } /** * Returns a new monotonic factory. - * + *

* The given random function must return a byte array. * * @param randomFunction a random function that returns a byte array * @return {@link UlidFactory} */ public static UlidFactory newMonotonicInstance(IntFunction randomFunction) { - return new UlidFactory(new MonotonicFunction(randomFunction)); + return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction))); } /** * Returns a new monotonic factory. - * + *

* The given random function must return a long value. * * @param randomFunction a random function that returns a long value * @param clock a custom clock instance for tests * @return {@link UlidFactory} */ - protected static UlidFactory newMonotonicInstance(LongSupplier randomFunction, Clock clock) { - return new UlidFactory(new MonotonicFunction(randomFunction), clock); + static UlidFactory newMonotonicInstance(LongSupplier randomFunction, Clock clock) { + return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction)), clock); } /** * Returns a new monotonic factory. - * + *

* The given random function must return a byte array. * * @param randomFunction a random function that returns a byte array * @param clock a custom clock instance for tests * @return {@link UlidFactory} */ - protected static UlidFactory newMonotonicInstance(IntFunction randomFunction, Clock clock) { - return new UlidFactory(new MonotonicFunction(randomFunction), clock); + static UlidFactory newMonotonicInstance(IntFunction randomFunction, Clock clock) { + return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction)), clock); } + // ****************************** + // Public methods + // ****************************** + /** * Returns a UUID. * @@ -186,36 +202,26 @@ public final class UlidFactory { /** * Returns a UUID with a specific time. * - * The time must be the number of milliseconds since 1970-01-01 (Unix epoch). - * - * @param time a given time + * @param time a number of milliseconds since 1970-01-01 (Unix epoch). * @return a ULID */ public synchronized Ulid create(final long time) { return this.ulidFunction.apply(time); } + // ****************************** + // Package-private inner classes + // ****************************** + /** * Function that creates ULIDs. */ - protected static final class UlidFunction implements LongFunction { + static final class UlidFunction implements LongFunction { private final IRandom random; - public UlidFunction() { - this.random = new ByteRandom(); - } - - public UlidFunction(Random random) { - this.random = IRandom.newInstance(random); - } - - public UlidFunction(IntFunction randomFunction) { - this.random = new ByteRandom(randomFunction); - } - - public UlidFunction(LongSupplier randomFunction) { - this.random = new LongRandom(randomFunction); + public UlidFunction(IRandom random) { + this.random = random; } @Override @@ -233,9 +239,8 @@ public final class UlidFactory { /** * Function that creates Monotonic ULIDs. */ - protected static final class MonotonicFunction implements LongFunction { + static final class MonotonicFunction implements LongFunction { - private long lastTime; private Ulid lastUlid; private final IRandom random; @@ -245,40 +250,23 @@ public final class UlidFactory { // system clock jumps back by 1 second due to leap second. protected static final int CLOCK_DRIFT_TOLERANCE = 10_000; - public MonotonicFunction() { - this(new ByteRandom()); - } - - public MonotonicFunction(Random random) { - this(IRandom.newInstance(random)); - } - - public MonotonicFunction(IntFunction randomFunction) { - this(new ByteRandom(randomFunction)); - } - - public MonotonicFunction(LongSupplier randomFunction) { - this(new LongRandom(randomFunction)); - } - - private MonotonicFunction(IRandom random) { + public MonotonicFunction(IRandom random) { this.random = random; - // initialize internal state - this.lastTime = Clock.systemUTC().millis(); - this.lastUlid = new Ulid(lastTime, random.nextBytes(Ulid.RANDOM_BYTES)); + this.lastUlid = new Ulid(0L, this.random.nextBytes(Ulid.RANDOM_BYTES)); } @Override public synchronized Ulid apply(final long time) { + final long lastTime = lastUlid.getTime(); + // Check if the current time is the same as the previous time or has moved // backwards after a small system clock adjustment or after a leap second. // Drift tolerance = (previous_time - 10s) < current_time <= previous_time - if ((time > this.lastTime - CLOCK_DRIFT_TOLERANCE) && (time <= this.lastTime)) { - this.lastUlid = lastUlid.increment(); + if ((time > lastTime - CLOCK_DRIFT_TOLERANCE) && (time <= lastTime)) { + this.lastUlid = this.lastUlid.increment(); } else { - this.lastTime = time; if (this.random instanceof ByteRandom) { this.lastUlid = new Ulid(time, this.random.nextBytes(Ulid.RANDOM_BYTES)); } else { @@ -292,12 +280,16 @@ public final class UlidFactory { } } - protected static interface IRandom { + static interface IRandom { public long nextLong(); public byte[] nextBytes(int length); + static IRandom newInstance() { + return new ByteRandom(); + } + static IRandom newInstance(Random random) { if (random == null) { return new ByteRandom(); @@ -309,9 +301,17 @@ public final class UlidFactory { } } } + + static IRandom newInstance(LongSupplier randomFunction) { + return new LongRandom(randomFunction); + } + + static IRandom newInstance(IntFunction randomFunction) { + return new ByteRandom(randomFunction); + } } - protected static class LongRandom implements IRandom { + static class LongRandom implements IRandom { private final LongSupplier randomFunction; @@ -344,20 +344,20 @@ public final class UlidFactory { shift = Long.SIZE; random = randomFunction.getAsLong(); } - shift -= Byte.SIZE; // 56, 48, 42... + shift -= Byte.SIZE; // 56, 48, 40... bytes[i] = (byte) (random >>> shift); } return bytes; } - protected static LongSupplier newRandomFunction(Random random) { + static LongSupplier newRandomFunction(Random random) { final Random entropy = random != null ? random : new SecureRandom(); return entropy::nextLong; } } - protected static class ByteRandom implements IRandom { + static class ByteRandom implements IRandom { private final IntFunction randomFunction; @@ -388,7 +388,7 @@ public final class UlidFactory { return this.randomFunction.apply(length); } - protected static IntFunction newRandomFunction(Random random) { + static IntFunction newRandomFunction(Random random) { final Random entropy = random != null ? random : new SecureRandom(); return (final int length) -> { final byte[] bytes = new byte[length]; diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java index 495621f..07f8a2a 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java @@ -123,6 +123,60 @@ public class UlidFactoryMonotonicTest extends UlidFactoryTest { assertEquals(ms1, ms2); // LEAP SECOND! DON'T MOVE BACKWARDS! } + @Test + public void testGetMonotonicUlidAfterRandomBitsOverflowFollowedByTimeBitsIncrement() { + + long time = Instant.parse("2021-12-31T23:59:59.999Z").toEpochMilli(); + long times[] = { time, time, time + 1, time + 2 }; + + Clock clock = new Clock() { + private int i; + + @Override + public long millis() { + return times[i++ % times.length]; + } + + @Override + public ZoneId getZone() { + return null; + } + + @Override + public Clock withZone(ZoneId zone) { + return null; + } + + @Override + public Instant instant() { + return null; + } + }; + + LongSupplier randomSupplier = () -> 0xffffffffffffffffL; + UlidFactory factory = UlidFactory.newMonotonicInstance(randomSupplier, clock); + + Ulid ulid1 = factory.create(); + Ulid ulid2 = factory.create(); // time bits should be incremented here + Ulid ulid3 = factory.create(); + Ulid ulid4 = factory.create(); + + assertEquals(ulid1.getTime(), time); + assertEquals(ulid2.getTime(), time + 1); // check if time bits increment occurred + assertEquals(ulid3.getTime(), time + 1); + assertEquals(ulid4.getTime(), time + 2); + + assertEquals(ulid1.getMostSignificantBits() & 0xffffL, 0xffffL); + assertEquals(ulid2.getMostSignificantBits() & 0xffffL, 0x0000L); + assertEquals(ulid3.getMostSignificantBits() & 0xffffL, 0x0000L); + assertEquals(ulid4.getMostSignificantBits() & 0xffffL, 0xffffL); + + assertEquals(ulid1.getLeastSignificantBits(), 0xffffffffffffffffL); + assertEquals(ulid2.getLeastSignificantBits(), 0x0000000000000000L); + assertEquals(ulid3.getLeastSignificantBits(), 0x0000000000000001L); + assertEquals(ulid4.getLeastSignificantBits(), 0xffffffffffffffffL); + } + private void checkOrdering(Ulid[] list) { Ulid[] other = Arrays.copyOf(list, list.length); Arrays.sort(other);