From f1eaebd3bd5bd5de25eff223dba03557db069b66 Mon Sep 17 00:00:00 2001 From: Fabio Lima Date: Sat, 30 Jan 2021 06:51:27 -0300 Subject: [PATCH] Development of version 3.0.0 #7 continuing... List of changes: Improve Ulid Create UlidFactory Create DefaultUlidFactory Create MonotonicUlidFactory Improve UlidTest Create UlidFactoryTest Create DefaultUlidFactoryTest Create MonotonicUlidFactoryTest Update UniquenessTest Update README.md Update javadoc Test coverage: 99.3% --- README.md | 256 +++++---- pom.xml | 2 +- .../java/com/github/f4b6a3/ulid/Ulid.java | 519 ++++++++++++------ .../com/github/f4b6a3/ulid/UlidCreator.java | 82 ++- ...ecCreator.java => DefaultUlidFactory.java} | 34 +- .../ulid/creator/MonotonicUlidFactory.java | 64 +++ ...{UlidSpecCreator.java => UlidFactory.java} | 50 +- .../DefaultRandomGenerator.java} | 4 +- .../RandomGenerator.java} | 4 +- .../com/github/f4b6a3/ulid/TestSuite.java | 8 +- .../java/com/github/f4b6a3/ulid/UlidTest.java | 471 +++++++++++++--- .../github/f4b6a3/ulid/UniquenessTest.java | 26 +- .../github/f4b6a3/ulid/bench/Benchmarks.java | 43 +- ...rTest.java => DefaultUlidFactoryTest.java} | 7 +- ...est.java => MonotonicUlidFactoryTest.java} | 7 +- ...cCreatorTest.java => UlidFactoryTest.java} | 6 +- 16 files changed, 1127 insertions(+), 456 deletions(-) rename src/main/java/com/github/f4b6a3/ulid/creator/{MonotonicUlidSpecCreator.java => DefaultUlidFactory.java} (71%) create mode 100644 src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidFactory.java rename src/main/java/com/github/f4b6a3/ulid/creator/{UlidSpecCreator.java => UlidFactory.java} (56%) rename src/main/java/com/github/f4b6a3/ulid/{strategy/DefaultRandomStrategy.java => random/DefaultRandomGenerator.java} (92%) rename src/main/java/com/github/f4b6a3/ulid/{strategy/RandomStrategy.java => random/RandomGenerator.java} (94%) rename src/test/java/com/github/f4b6a3/ulid/creator/{UlidSpecCreatorTest.java => DefaultUlidFactoryTest.java} (88%) rename src/test/java/com/github/f4b6a3/ulid/creator/{MonotonicUlidSpecCreatorTest.java => MonotonicUlidFactoryTest.java} (89%) rename src/test/java/com/github/f4b6a3/ulid/creator/{AbstractUlidSpecCreatorTest.java => UlidFactoryTest.java} (88%) diff --git a/README.md b/README.md index 3e2a222..787d3d9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ + # ULID Creator -A Java library for generating ULIDs. +A Java library for generating [ULIDs](https://github.com/ulid/spec). * Generated in lexicographical order; * Can be stored as a UUID/GUID; * Can be stored as a string of 26 chars; +* Can be stored as an array of 16 bytes; * String format is encoded to [Crockford's base32](https://www.crockford.com/base32.html); -* String format is URL safe, case insensitive and accepts hyphens. +* String format is URL safe and case insensitive. How to Use ------------------------------------------------------ @@ -15,13 +17,13 @@ How to Use Create a ULID: ```java -UUID ulid = UlidCreator.getUlid(); // 01706d6c-6aad-c795-370c-98d0be881bba +Ulid ulid = UlidCreator.getUlid(); ``` -Create a ULID string: +Create a Monotonic ULID: ```java -String ulid = UlidCreator.getUlidString(); // 01E1PPRTMSQ34W7JR5YSND6B8Z +Ulid ulid = UlidCreator.getMonotonicUlid(); ``` ### Maven dependency @@ -33,7 +35,7 @@ Add these lines to your `pom.xml`. com.github.f4b6a3 ulid-creator - 2.3.3 + 3.0.0 ``` See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). @@ -43,116 +45,162 @@ Implementation ### ULID -The GUIDs in this library are based on the [ULID specification](https://github.com/ulid/spec). The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator. - -Every time the timestamp changes the random part is reset to a new random value. If the current timestamp is equal to the previous one, the random bits are incremented by 1. - -The default random number generator is `java.security.SecureRandom`. +The ULID is a 128 bit long identifier. The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator. ```java -// GUID based on ULID spec -UUID ulid = UlidCreator.getUlid(); +// Generate a ULID +Ulid ulid = UlidCreator.getUlid(); ``` ```java -// GUID based on ULID spec -// Compatible with RFC-4122 UUID v4 -UUID ulid = UlidCreator.getUlid4(); +// Generate a ULID with a specific time +Ulid ulid = UlidCreator.getUlid(1234567890); ``` -Sequence of GUIDs based on ULID spec: +Sequence of ULIDs: ```text -01706d6c-6aac-80bd-7ff5-f660c2dd58ea -01706d6c-6aac-80bd-7ff5-f660c2dd58eb -01706d6c-6aac-80bd-7ff5-f660c2dd58ec -01706d6c-6aac-80bd-7ff5-f660c2dd58ed -01706d6c-6aac-80bd-7ff5-f660c2dd58ee -01706d6c-6aac-80bd-7ff5-f660c2dd58ef -01706d6c-6aac-80bd-7ff5-f660c2dd58f0 -01706d6c-6aac-80bd-7ff5-f660c2dd58f1 -01706d6c-6aad-c795-370c-98d0be881bb8 < millisecond changed -01706d6c-6aad-c795-370c-98d0be881bb9 -01706d6c-6aad-c795-370c-98d0be881bba -01706d6c-6aad-c795-370c-98d0be881bbb -01706d6c-6aad-c795-370c-98d0be881bbc -01706d6c-6aad-c795-370c-98d0be881bbd -01706d6c-6aad-c795-370c-98d0be881bbe -01706d6c-6aad-c795-370c-98d0be881bbf - ^ look ^ look - -|------------|---------------------| - millisecs randomness -``` +01EX8Y21KBH49ZZCA7KSKH6X1C +01EX8Y21KBJTFK0JV5J20QPQNR +01EX8Y21KBG2CS1V6WQCTVM7K6 +01EX8Y21KB8HPZNBP3PTW7HVEY +01EX8Y21KB3HZV38VAPTPAG1TY +01EX8Y21KB9FTEJHPAGAKYG9Z8 +01EX8Y21KBQGKGH2SVPQAYEFFC +01EX8Y21KBY17J9WR9KQR8SE7H +01EX8Y21KCVHYSJGVK4HBXDMR9 < millisecond changed +01EX8Y21KC668W3PEDEAGDHMVG +01EX8Y21KC53D2S5ADQ2EST327 +01EX8Y21KCPQ3TENMTY1S7HV56 +01EX8Y21KC3755QF9STQEV05EB +01EX8Y21KC5ZSHK908GMDK69WE +01EX8Y21KCSGJS8S1FVS06B3SX +01EX8Y21KC6ZBWQ0JBV337R1CN + ^ look -### ULID string - -The ULID string is a sequence of 26 chars. See the [ULID specification](https://github.com/ulid/spec) for more information. - -See the section on GUIDs to know how the 128 bits are generated in this library. - -```java -// String based on ULID spec -String ulid = UlidCreator.getUlidString(); -``` - -```java -// String based on ULID spec -// Compatible with RFC-4122 UUID v4 -String ulid = UlidCreator.getUlidString4(); -``` - -Sequence of Strings based on ULID spec: - -```text -01E1PPRTMSQ34W7JR5YSND6B8T -01E1PPRTMSQ34W7JR5YSND6B8V -01E1PPRTMSQ34W7JR5YSND6B8W -01E1PPRTMSQ34W7JR5YSND6B8X -01E1PPRTMSQ34W7JR5YSND6B8Y -01E1PPRTMSQ34W7JR5YSND6B8Z -01E1PPRTMSQ34W7JR5YSND6B90 -01E1PPRTMSQ34W7JR5YSND6B91 -01E1PPRTMTYMX8G17TWSJJZMEE < millisecond changed -01E1PPRTMTYMX8G17TWSJJZMEF -01E1PPRTMTYMX8G17TWSJJZMEG -01E1PPRTMTYMX8G17TWSJJZMEH -01E1PPRTMTYMX8G17TWSJJZMEJ -01E1PPRTMTYMX8G17TWSJJZMEK -01E1PPRTMTYMX8G17TWSJJZMEM -01E1PPRTMTYMX8G17TWSJJZMEN - ^ look ^ look - |---------|--------------| - millisecs randomness + time random ``` -### How use the `UlidSpecCreator` directly +### Monotonic ULID -These are some examples of using the `UlidSpecCreator` to create ULID strings: +The Monotonic ULID is a 128 bit long identifier. The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator. + +The random component is incremented by 1 whenever the current millisecond is equal to the previous one. But when the current millisecond is different, the random component changes to another random value. ```java -// with your custom timestamp strategy -TimestampStrategy customStrategy = new CustomTimestampStrategy(); -UlidSpecCreator creator = UlidCreator.getUlidSpecCreator() - .withTimestampStrategy(customStrategy); -String ulid = creator.createString(); +// Generate a Monotonic ULID +Ulid ulid = UlidCreator.getMonotonicUlid(); ``` + ```java -// with your custom random strategy that wraps any random generator -RandomStrategy customStrategy = new CustomRandomStrategy(); -UlidSpecCreator creator = UlidCreator.getUlidSpecCreator() - .withRandomStrategy(customStrategy); -String ulid = creator.createString(); +// Generate a Monotonic ULID with a specific time +Ulid ulid = UlidCreator.getMonotonicUlid(1234567890); ``` + +Sequence of Monotonic ULIDs: + +```text +01EX8Y7M8MDVX3M3EQG69EEMJW +01EX8Y7M8MDVX3M3EQG69EEMJX +01EX8Y7M8MDVX3M3EQG69EEMJY +01EX8Y7M8MDVX3M3EQG69EEMJZ +01EX8Y7M8MDVX3M3EQG69EEMK0 +01EX8Y7M8MDVX3M3EQG69EEMK1 +01EX8Y7M8MDVX3M3EQG69EEMK2 +01EX8Y7M8MDVX3M3EQG69EEMK3 +01EX8Y7M8N1G30CYF2PJR23J2J < millisecond changed +01EX8Y7M8N1G30CYF2PJR23J2K +01EX8Y7M8N1G30CYF2PJR23J2M +01EX8Y7M8N1G30CYF2PJR23J2N +01EX8Y7M8N1G30CYF2PJR23J2P +01EX8Y7M8N1G30CYF2PJR23J2Q +01EX8Y7M8N1G30CYF2PJR23J2R +01EX8Y7M8N1G30CYF2PJR23J2S + ^ look ^ look + +|---------|--------------| + time random +``` + +### Other usage examples + +Create a ULID from a canonical string (26 chars): + +```java +Ulid ulid = Ulid.from("0123456789ABCDEFGHJKMNPQRS"); +``` + +Convert a ULID into a canonical string in upper case: + +```java +String string = ulid.toUpperCase(); // 0123456789ABCDEFGHJKMNPQRS +``` + +Convert a ULID into a canonical string in lower case: + +```java +String string = ulid.toLowerCase(); // 0123456789abcdefghjkmnpqrs +``` + +Convert a ULID into a UUID: + +```java +UUID uuid = ulid.toUuid(); // 0110c853-1d09-52d8-d73e-1194e95b5f19 +``` + +Convert a ULID into a [RFC-4122](https://tools.ietf.org/html/rfc4122) UUID v4: + +```java +UUID uuid = ulid.toRfc4122().toUuid(); // 0110c853-1d09-42d8-973e-1194e95b5f19 + // ^ UUID v4 +``` + +Convert a ULID into a byte array: + +```java +byte[] bytes = ulid.toBytes(); // 16 bytes (128 bits) +``` + +Get the creation instant of a ULID: + +```java +Instant instant = ulid.getInstant(); // 2007-02-16T02:13:14.633Z +``` + +Get the time component of a ULID: + +```java +long time = ulid.getTime(); // 1171591994633 +``` + +Get the random component of a ULID: + +```java +byte[] random = ulid.getRandom(); // 10 bytes (80 bits) +``` + +Use a `UlidFctory` instance with `java.util.Random` to generate ULIDs: + ```java -// with `java.util.Random` number generator Random random = new Random(); -UlidSpecCreator creator = UlidCreator.getUlidSpecCreator() - .withRandomGenerator(random); -String ulid = creator.createString(); +UlidFactory factory = UlidCreator.getDefaultFactory().withRandomGenerator(random::nextBytes); + +Ulid ulid = facory.create(); ``` +Use a `UlidFctory` instance with any random generator you like(*) to generate ULIDs: + +```java +import com.github.niceguy.random.AwesomeRandom; // a hypothetical RNG +AwesomeRandom awesomeRandom = new AwesomeRandom(); +UlidFactory factory = UlidCreator.getDefaultFactory().withRandomGenerator(awesomeRandom::nextBytes); + +Ulid ulid = facory.create(); +``` + +(*) since it provides a void method like `nextBytes(byte[])`. + Benchmark ------------------------------------------------------ @@ -160,14 +208,18 @@ This section shows benchmarks comparing `UlidCreator` to `java.util.UUID`. ``` ================================================================================ -THROUGHPUT (operations/millis) Mode Cnt Score Error Units +THROUGHPUT (operations/msec) Mode Cnt Score Error Units ================================================================================ -Throughput.JDK_RandomBased thrpt 5 2050,995 ± 21,636 ops/ms +Throughput.Uuid01_toString thrpt 5 2876,799 ± 39,938 ops/ms +Throughput.Uuid02_fromString thrpt 5 1936,569 ± 38,822 ops/ms +Throughput.Uuid03_RandomBased thrpt 5 2011,774 ± 21,198 ops/ms -------------------------------------------------------------------------------- -Throughput.UlidCreator_Ulid thrpt 5 18524,721 ± 563,781 ops/ms -Throughput.UlidCreator_UlidString thrpt 5 12223,501 ± 89,836 ops/ms +Throughput.UlidCreator01_toString thrpt 5 29487,382 ± 627,808 ops/ms +Throughput.UlidCreator02_fromString thrpt 5 21194,263 ± 706,398 ops/ms +Throughput.UlidCreator03_Ulid thrpt 5 2745,123 ± 41,326 ops/ms +Throughput.UlidCreator04_MonotonicUlid thrpt 5 19542,344 ± 423,271 ops/ms ================================================================================ -Total time: 00:04:00 +Total time: 00:09:22 ================================================================================ ``` @@ -175,8 +227,8 @@ System: JVM 8, Ubuntu 20.04, CPU i5-3330, 8G RAM. See: [uuid-creator-benchmark](https://github.com/fabiolimace/uuid-creator-benchmark) -Links for generators +Other generators ------------------------------------------- -* [UUID Creator](https://github.com/f4b6a3/uuid-creator) -* [ULID Creator](https://github.com/f4b6a3/ulid-creator) -* [TSID Creator](https://github.com/f4b6a3/tsid-creator) +* [UUID Creator](https://github.com/f4b6a3/uuid-creator): for generating UUIDs +* [TSID Creator](https://github.com/f4b6a3/tsid-creator): for generating Time Sortable IDs + diff --git a/pom.xml b/pom.xml index e16bab8..1ea479a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.github.f4b6a3 ulid-creator - 2.3.4-SNAPSHOT + 3.0.1-SNAPSHOT jar ulid-creator diff --git a/src/main/java/com/github/f4b6a3/ulid/Ulid.java b/src/main/java/com/github/f4b6a3/ulid/Ulid.java index d811e93..b2cf1ce 100644 --- a/src/main/java/com/github/f4b6a3/ulid/Ulid.java +++ b/src/main/java/com/github/f4b6a3/ulid/Ulid.java @@ -30,13 +30,23 @@ import java.util.UUID; /** * This class represents a ULID. + * + * 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. */ public final class Ulid implements Serializable, Comparable { - private final long msb; - private final long lsb; + private static final long serialVersionUID = 2625269413446854731L; - protected static final long TIME_MAX = 281474976710655L; // 2^48 - 1 + private final long msb; // most significant bits + private final long lsb; // least significant bits public static final int ULID_LENGTH = 26; public static final int TIME_LENGTH = 10; @@ -46,20 +56,17 @@ public final class Ulid implements Serializable, Comparable { public static final int TIME_BYTES_LENGTH = 6; public static final int RANDOM_BYTES_LENGTH = 10; - // 0xffffffffffffffffL + 1 = 0x0000000000000000L - private static final long INCREMENT_OVERFLOW = 0x0000000000000000L; - - protected static final char[] ALPHABET_UPPERCASE = // + 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' }; - protected static final char[] ALPHABET_LOWERCASE = // + 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' }; - protected static final long[] ALPHABET_VALUES = new long[128]; + private static final long[] ALPHABET_VALUES = new long[128]; static { for (int i = 0; i < ALPHABET_VALUES.length; i++) { ALPHABET_VALUES[i] = -1; @@ -129,139 +136,190 @@ public final class Ulid implements Serializable, Comparable { ALPHABET_VALUES['O'] = 0x00; ALPHABET_VALUES['I'] = 0x01; ALPHABET_VALUES['L'] = 0x01; - } - private static final long serialVersionUID = 2625269413446854731L; + // 0xffffffffffffffffL + 1 = 0x0000000000000000L + private static final long INCREMENT_OVERFLOW = 0x0000000000000000L; + /** + * Create a new ULID. + * + * Useful to make copies of ULIDs. + * + * @param ulid a ULID + */ + public Ulid(Ulid ulid) { + this.msb = ulid.getMostSignificantBits(); + this.lsb = ulid.getLeastSignificantBits(); + } + + /** + * Create a new ULID. + * + * @param mostSignificantBits the first 8 bytes as a long value + * @param leastSignificantBits the last 8 bytes as a long value + */ public Ulid(long mostSignificantBits, long leastSignificantBits) { this.msb = mostSignificantBits; this.lsb = leastSignificantBits; } - public static Ulid of(Ulid ulid) { - return new Ulid(ulid.getMostSignificantBits(), ulid.getLeastSignificantBits()); - } + /** + * Create a new ULID. + * + * @param time the time component in milliseconds since 1970-01-01 + * @param random the random component in byte array + */ + public Ulid(long time, byte[] random) { - public static Ulid of(UUID uuid) { - return new Ulid(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); - } - - // TODO: test - public static Ulid of(byte[] bytes) { - - if (bytes == null || bytes.length != ULID_BYTES_LENGTH) { - throw new IllegalArgumentException("Invalid ULID bytes"); + // The time component has 48 bits. + if ((time & 0xffff000000000000L) != 0) { + // 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! + } + // The random component has 80 bits (10 bytes). + if (random == null || random.length != RANDOM_BYTES_LENGTH) { + throw new IllegalArgumentException("Invalid random bytes"); // null or wrong length! } long long0 = 0; long long1 = 0; - long0 |= (bytes[0x0] & 0xffL) << 56; - long0 |= (bytes[0x1] & 0xffL) << 48; - long0 |= (bytes[0x2] & 0xffL) << 40; - long0 |= (bytes[0x3] & 0xffL) << 32; - long0 |= (bytes[0x4] & 0xffL) << 24; - long0 |= (bytes[0x5] & 0xffL) << 16; - long0 |= (bytes[0x6] & 0xffL) << 8; - long0 |= (bytes[0x7] & 0xffL); + long0 |= time << 16; + long0 |= (long) (random[0x0] & 0xff) << 8; + long0 |= (long) (random[0x1] & 0xff); - long1 |= (bytes[0x8] & 0xffL) << 56; - long1 |= (bytes[0x9] & 0xffL) << 48; - long1 |= (bytes[0xa] & 0xffL) << 40; - long1 |= (bytes[0xb] & 0xffL) << 32; - long1 |= (bytes[0xc] & 0xffL) << 24; - long1 |= (bytes[0xd] & 0xffL) << 16; - long1 |= (bytes[0xe] & 0xffL) << 8; - long1 |= (bytes[0xf] & 0xffL); + long1 |= (long) (random[0x2] & 0xff) << 56; + long1 |= (long) (random[0x3] & 0xff) << 48; + long1 |= (long) (random[0x4] & 0xff) << 40; + long1 |= (long) (random[0x5] & 0xff) << 32; + long1 |= (long) (random[0x6] & 0xff) << 24; + long1 |= (long) (random[0x7] & 0xff) << 16; + long1 |= (long) (random[0x8] & 0xff) << 8; + long1 |= (long) (random[0x9] & 0xff); - return new Ulid(long0, long1); + this.msb = long0; + this.lsb = long1; } - // TODO: optimize - public static Ulid of(String string) { - - final char[] chars = toCharArray(string); - - long tm = 0; - long r1 = 0; - long r2 = 0; - - tm |= ALPHABET_VALUES[chars[0x00]] << 45; - tm |= ALPHABET_VALUES[chars[0x01]] << 40; - tm |= ALPHABET_VALUES[chars[0x02]] << 35; - tm |= ALPHABET_VALUES[chars[0x03]] << 30; - tm |= ALPHABET_VALUES[chars[0x04]] << 25; - tm |= ALPHABET_VALUES[chars[0x05]] << 20; - tm |= ALPHABET_VALUES[chars[0x06]] << 15; - tm |= ALPHABET_VALUES[chars[0x07]] << 10; - tm |= ALPHABET_VALUES[chars[0x08]] << 5; - tm |= ALPHABET_VALUES[chars[0x09]]; - - r1 |= ALPHABET_VALUES[chars[0x0a]] << 35; - r1 |= ALPHABET_VALUES[chars[0x0b]] << 30; - r1 |= ALPHABET_VALUES[chars[0x0c]] << 25; - r1 |= ALPHABET_VALUES[chars[0x0d]] << 20; - r1 |= ALPHABET_VALUES[chars[0x0e]] << 15; - r1 |= ALPHABET_VALUES[chars[0x0f]] << 10; - r1 |= ALPHABET_VALUES[chars[0x10]] << 5; - r1 |= ALPHABET_VALUES[chars[0x11]]; - - r2 |= ALPHABET_VALUES[chars[0x12]] << 35; - r2 |= ALPHABET_VALUES[chars[0x13]] << 30; - r2 |= ALPHABET_VALUES[chars[0x14]] << 25; - r2 |= ALPHABET_VALUES[chars[0x15]] << 20; - r2 |= ALPHABET_VALUES[chars[0x16]] << 15; - r2 |= ALPHABET_VALUES[chars[0x17]] << 10; - r2 |= ALPHABET_VALUES[chars[0x18]] << 5; - r2 |= ALPHABET_VALUES[chars[0x19]]; - - final long msb = (tm << 16) | (r1 >>> 24); - final long lsb = (r1 << 40) | (r2 & 0xffffffffffL); - - return new Ulid(msb, lsb); + /** + * Converts a UUID into a ULID. + * + * @param uuid a UUID + * @return a ULID + */ + public static Ulid from(UUID uuid) { + return new Ulid(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); } - public static Ulid of(long time, byte[] random) { + /** + * Converts a byte array into a ULID. + * + * @param bytes a byte array + * @return a ULID + */ + public static Ulid from(byte[] bytes) { - if ((time & 0xffff000000000000L) != 0) { - throw new IllegalArgumentException("Invalid time value"); - } - if (random == null || random.length != RANDOM_BYTES_LENGTH) { - throw new IllegalArgumentException("Invalid random bytes"); + if (bytes == null || bytes.length != ULID_BYTES_LENGTH) { + throw new IllegalArgumentException("Invalid ULID bytes"); // null or wrong length! } long msb = 0; long lsb = 0; - msb |= time << 16; - msb |= (long) (random[0x0] & 0xff) << 8; - msb |= (long) (random[0x1] & 0xff); + msb |= (bytes[0x0] & 0xffL) << 56; + msb |= (bytes[0x1] & 0xffL) << 48; + msb |= (bytes[0x2] & 0xffL) << 40; + msb |= (bytes[0x3] & 0xffL) << 32; + msb |= (bytes[0x4] & 0xffL) << 24; + msb |= (bytes[0x5] & 0xffL) << 16; + msb |= (bytes[0x6] & 0xffL) << 8; + msb |= (bytes[0x7] & 0xffL); - lsb |= (long) (random[0x2] & 0xff) << 56; - lsb |= (long) (random[0x3] & 0xff) << 48; - lsb |= (long) (random[0x4] & 0xff) << 40; - lsb |= (long) (random[0x5] & 0xff) << 32; - lsb |= (long) (random[0x6] & 0xff) << 24; - lsb |= (long) (random[0x7] & 0xff) << 16; - lsb |= (long) (random[0x8] & 0xff) << 8; - lsb |= (long) (random[0x9] & 0xff); + lsb |= (bytes[0x8] & 0xffL) << 56; + lsb |= (bytes[0x9] & 0xffL) << 48; + lsb |= (bytes[0xa] & 0xffL) << 40; + lsb |= (bytes[0xb] & 0xffL) << 32; + lsb |= (bytes[0xc] & 0xffL) << 24; + lsb |= (bytes[0xd] & 0xffL) << 16; + lsb |= (bytes[0xe] & 0xffL) << 8; + lsb |= (bytes[0xf] & 0xffL); return new Ulid(msb, lsb); } + /** + * 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 + */ + public static Ulid from(String string) { + + final char[] chars = toCharArray(string); + + long time = 0; + long random0 = 0; + long random1 = 0; + + time |= ALPHABET_VALUES[chars[0x00]] << 45; + time |= ALPHABET_VALUES[chars[0x01]] << 40; + time |= ALPHABET_VALUES[chars[0x02]] << 35; + time |= ALPHABET_VALUES[chars[0x03]] << 30; + time |= ALPHABET_VALUES[chars[0x04]] << 25; + time |= ALPHABET_VALUES[chars[0x05]] << 20; + time |= ALPHABET_VALUES[chars[0x06]] << 15; + time |= ALPHABET_VALUES[chars[0x07]] << 10; + time |= ALPHABET_VALUES[chars[0x08]] << 5; + time |= ALPHABET_VALUES[chars[0x09]]; + + random0 |= ALPHABET_VALUES[chars[0x0a]] << 35; + random0 |= ALPHABET_VALUES[chars[0x0b]] << 30; + random0 |= ALPHABET_VALUES[chars[0x0c]] << 25; + random0 |= ALPHABET_VALUES[chars[0x0d]] << 20; + random0 |= ALPHABET_VALUES[chars[0x0e]] << 15; + random0 |= ALPHABET_VALUES[chars[0x0f]] << 10; + random0 |= ALPHABET_VALUES[chars[0x10]] << 5; + random0 |= ALPHABET_VALUES[chars[0x11]]; + + random1 |= ALPHABET_VALUES[chars[0x12]] << 35; + random1 |= ALPHABET_VALUES[chars[0x13]] << 30; + random1 |= ALPHABET_VALUES[chars[0x14]] << 25; + random1 |= ALPHABET_VALUES[chars[0x15]] << 20; + random1 |= ALPHABET_VALUES[chars[0x16]] << 15; + random1 |= ALPHABET_VALUES[chars[0x17]] << 10; + random1 |= ALPHABET_VALUES[chars[0x18]] << 5; + random1 |= ALPHABET_VALUES[chars[0x19]]; + + final long msb = (time << 16) | (random0 >>> 24); + final long lsb = (random0 << 40) | (random1 & 0xffffffffffL); + + return new Ulid(msb, lsb); + } + + /** + * Convert the ULID into a UUID. + * + * If you need a RFC-4122 UUID v4 do this: {@code Ulid.toRfc4122().toUuid()}. + * + * @return a UUID. + */ public UUID toUuid() { return new UUID(this.msb, this.lsb); } - // TODO: test - public UUID toUuid4() { - final long msb4 = (this.msb & 0xffffffffffff0fffL) | 0x0000000000004000L; // apply version 4 - final long lsb4 = (this.lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // apply variant RFC-4122 - return new UUID(msb4, lsb4); - } - - // TODO: test + /** + * Convert the ULID into a byte array. + * + * @return an byte array. + */ public byte[] toBytes() { final byte[] bytes = new byte[ULID_BYTES_LENGTH]; @@ -287,64 +345,183 @@ public final class Ulid implements Serializable, Comparable { return bytes; } - // TODO: test - public byte[] toBytes4() { - return Ulid.of(this.toUuid4()).toBytes(); - } - - @Override - public String toString() { - return this.toUpperCase(); - } - - // TODO: test + /** + * 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. + * + * See: https://www.crockford.com/base32.html + * + * @return a string + */ public String toUpperCase() { return toString(ALPHABET_UPPERCASE); } - // TODO: test - public String toUpperCase4() { - return Ulid.of(this.toUuid4()).toUpperCase(); - } - - // TODO: test + /** + * 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. + * + * It is at least twice as fast as {@code Ulid.toString().toLowerCase()}. + * + * See: https://www.crockford.com/base32.html + * + * @return a string + */ public String toLowerCase() { return toString(ALPHABET_LOWERCASE); } - // TODO: test - public String toLowerCase4() { - return Ulid.of(this.toUuid4()).toLowerCase(); - } - - public long getTime() { - return this.msb >>> 16; + /** + * Converts the ULID into into another ULID that is compatible with UUID v4. + * + * 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. + * + * @return a ULID + */ + public Ulid toRfc4122() { + + // set the 4 most significant bits of the 7th byte to 0, 1, 0 and 0 + final long msb4 = (this.msb & 0xffffffffffff0fffL) | 0x0000000000004000L; // RFC-4122 version 4 + // set the 2 most significant bits of the 9th byte to 1 and 0 + final long lsb4 = (this.lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // RFC-4122 variant 2 + + return new Ulid(msb4, lsb4); } + /** + * Returns the instant of creation. + * + * The instant of creation is extracted from the time component. + * + * @return {@link Instant} + */ public Instant getInstant() { return Instant.ofEpochMilli(this.getTime()); } + /** + * 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. + */ + public long getTime() { + return this.msb >>> 16; + } + + /** + * Returns the random component as a byte array. + * + * The random component is an array of 10 bytes (80 bits). + * + * @return a byte array + */ + public byte[] getRandom() { + final byte[] bytes = new byte[RANDOM_BYTES_LENGTH]; + System.arraycopy(this.toBytes(), TIME_BYTES_LENGTH, bytes, 0, RANDOM_BYTES_LENGTH); + return bytes; + } + + /** + * Returns the most significant bits as a number. + * + * @return a number. + */ public long getMostSignificantBits() { return this.msb; } + /** + * Returns the least significant bits as a number. + * + * @return a number. + */ public long getLeastSignificantBits() { return this.lsb; } - // TODO: test + /** + * 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, 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 last 80 bits, the random + * component simply wraps around. + * + * @return a ULID + */ public Ulid increment() { - long msb1 = this.msb; - long lsb1 = this.lsb + 1; // Increment the LSB + long newMsb = this.msb; + long newLsb = this.lsb + 1; // increment the LEAST significant bits - if (lsb1 == INCREMENT_OVERFLOW) { - // Increment the random bits of the MSB - msb1 = (msb1 & 0xffffffffffff0000L) | ((msb1 + 1) & 0x000000000000ffffL); + if (newLsb == INCREMENT_OVERFLOW) { + // carrying the extra bit by incrementing the MOST significant bits + newMsb = (newMsb & 0xffffffffffff0000L) | ((newMsb + 1) & 0x000000000000ffffL); } - return new Ulid(msb1, lsb1); + return new Ulid(newMsb, newLsb); + } + + /** + * 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 + */ + public static boolean isValid(String string) { + return string != null && isValidCharArray(string.toCharArray()); + } + + /** + * Converts the ULID into a canonical string in upper case. + * + * It is the same as {@code Ulid.toUpperCase()}. + * + * @return a ULID string + */ + @Override + public String toString() { + return this.toUpperCase(); } @Override @@ -385,14 +562,13 @@ public final class Ulid implements Serializable, Comparable { return 0; } - // TODO: optimize protected String toString(char[] alphabet) { final char[] chars = new char[ULID_LENGTH]; long time = this.msb >>> 16; - long random1 = ((this.msb & 0xffffL) << 24) | (this.lsb >>> 40); - long random2 = (this.lsb & 0xffffffffffL); + long random0 = ((this.msb & 0xffffL) << 24) | (this.lsb >>> 40); + long random1 = (this.lsb & 0xffffffffffL); chars[0x00] = alphabet[(int) (time >>> 45 & 0b11111)]; chars[0x01] = alphabet[(int) (time >>> 40 & 0b11111)]; @@ -405,29 +581,33 @@ public final class Ulid implements Serializable, Comparable { chars[0x08] = alphabet[(int) (time >>> 5 & 0b11111)]; chars[0x09] = alphabet[(int) (time & 0b11111)]; - chars[0x0a] = alphabet[(int) (random1 >>> 35 & 0b11111)]; - chars[0x0b] = alphabet[(int) (random1 >>> 30 & 0b11111)]; - chars[0x0c] = alphabet[(int) (random1 >>> 25 & 0b11111)]; - chars[0x0d] = alphabet[(int) (random1 >>> 20 & 0b11111)]; - chars[0x0e] = alphabet[(int) (random1 >>> 15 & 0b11111)]; - chars[0x0f] = alphabet[(int) (random1 >>> 10 & 0b11111)]; - chars[0x10] = alphabet[(int) (random1 >>> 5 & 0b11111)]; - chars[0x11] = alphabet[(int) (random1 & 0b11111)]; + chars[0x0a] = alphabet[(int) (random0 >>> 35 & 0b11111)]; + chars[0x0b] = alphabet[(int) (random0 >>> 30 & 0b11111)]; + chars[0x0c] = alphabet[(int) (random0 >>> 25 & 0b11111)]; + chars[0x0d] = alphabet[(int) (random0 >>> 20 & 0b11111)]; + chars[0x0e] = alphabet[(int) (random0 >>> 15 & 0b11111)]; + chars[0x0f] = alphabet[(int) (random0 >>> 10 & 0b11111)]; + chars[0x10] = alphabet[(int) (random0 >>> 5 & 0b11111)]; + chars[0x11] = alphabet[(int) (random0 & 0b11111)]; - chars[0x12] = alphabet[(int) (random2 >>> 35 & 0b11111)]; - chars[0x13] = alphabet[(int) (random2 >>> 30 & 0b11111)]; - chars[0x14] = alphabet[(int) (random2 >>> 25 & 0b11111)]; - chars[0x15] = alphabet[(int) (random2 >>> 20 & 0b11111)]; - chars[0x16] = alphabet[(int) (random2 >>> 15 & 0b11111)]; - chars[0x17] = alphabet[(int) (random2 >>> 10 & 0b11111)]; - chars[0x18] = alphabet[(int) (random2 >>> 5 & 0b11111)]; - chars[0x19] = alphabet[(int) (random2 & 0b11111)]; + chars[0x12] = alphabet[(int) (random1 >>> 35 & 0b11111)]; + chars[0x13] = alphabet[(int) (random1 >>> 30 & 0b11111)]; + chars[0x14] = alphabet[(int) (random1 >>> 25 & 0b11111)]; + chars[0x15] = alphabet[(int) (random1 >>> 20 & 0b11111)]; + chars[0x16] = alphabet[(int) (random1 >>> 15 & 0b11111)]; + chars[0x17] = alphabet[(int) (random1 >>> 10 & 0b11111)]; + chars[0x18] = alphabet[(int) (random1 >>> 5 & 0b11111)]; + chars[0x19] = alphabet[(int) (random1 & 0b11111)]; return new String(chars); } - public static boolean isValidString(String string) { - return isValidArray(string == null ? null : string.toCharArray()); + protected static char[] toCharArray(String string) { + char[] chars = string == null ? null : string.toCharArray(); + if (!isValidCharArray(chars)) { + throw new IllegalArgumentException(String.format("Invalid ULID: \"%s\"", string)); + } + return chars; } /** @@ -439,15 +619,22 @@ public final class Ulid implements Serializable, Comparable { * @param chars a char array * @return boolean true if valid */ - protected static boolean isValidArray(final char[] chars) { + protected static boolean isValidCharArray(final char[] chars) { if (chars == null || chars.length != ULID_LENGTH) { return false; // null or wrong size! } - // the two extra bits added by base-32 encoding must be zero + // The time component has 48 bits. + // The base32 encoded time component has 50 bits. + // The time component cannot be greater than than 2^48-1. + // So the 2 first bits of the base32 decoded time component must be ZERO. + // As a consequence, the 1st char of the input string must be between 0 and 7. if ((ALPHABET_VALUES[chars[0]] & 0b11000) != 0) { - return false; // overflow! + // 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." + return false; // time overflow! } for (int i = 0; i < chars.length; i++) { @@ -458,12 +645,4 @@ public final class Ulid implements Serializable, Comparable { return true; // It seems to be OK. } - - protected static char[] toCharArray(String string) { - char[] chars = string == null ? new char[0] : string.toCharArray(); - if (!isValidArray(chars)) { - throw new IllegalArgumentException(String.format("Invalid ULID: \"%s\"", string)); - } - return chars; - } } diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java index 08ca495..8608ee6 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -24,43 +24,97 @@ package com.github.f4b6a3.ulid; -import com.github.f4b6a3.ulid.creator.MonotonicUlidSpecCreator; -import com.github.f4b6a3.ulid.creator.UlidSpecCreator; +import com.github.f4b6a3.ulid.creator.MonotonicUlidFactory; +import com.github.f4b6a3.ulid.creator.UlidFactory; +import com.github.f4b6a3.ulid.creator.DefaultUlidFactory; +/** + * Facade to the ULID factories. + * + * 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. + * + * The maximum ULIDs that can be generated per millisecond is 2^80. + */ public final class UlidCreator { private UlidCreator() { } + /** + * Returns a ULID. + * + * The random component is always reset to a new random value. + * + * @return a ULID + */ public static Ulid getUlid() { - return DefaultCreatorHolder.INSTANCE.create(); + return DefaultFactoryHolder.INSTANCE.create(); } + /** + * Returns a ULID with a specific time. + * + * @param time a specific time + * @return a ULID + */ public static Ulid getUlid(final long time) { - return DefaultCreatorHolder.INSTANCE.create(time); + return DefaultFactoryHolder.INSTANCE.create(time); } + /** + * Returns a Monotonic ULID. + * + * The random component is reset to a new value every time the millisecond + * changes. + * + * If more than one ULID is generated within the same millisecond, the random + * component is incremented by one. + * + * @return a ULID + */ public static Ulid getMonotonicUlid() { - return MonotonicCreatorHolder.INSTANCE.create(); + return MonotonicFactoryHolder.INSTANCE.create(); } + /** + * Returns a Monotonic ULID with a specific time. + * + * @param time a specific time + * @return a ULID + */ public static Ulid getMonotonicUlid(final long time) { - return MonotonicCreatorHolder.INSTANCE.create(time); + return MonotonicFactoryHolder.INSTANCE.create(time); } - public static UlidSpecCreator getUlidSpecCreator() { - return new UlidSpecCreator(); + /** + * Returns an instance of the Default ULID factory. + * + * @return a ULID factory + */ + public static UlidFactory getDefaultFactory() { + return new DefaultUlidFactory(); } - public static UlidSpecCreator getMonotonicUlidSpecCreator() { - return new MonotonicUlidSpecCreator(); + /** + * Returns an instance of the Monotonic ULID factory. + * + * @return a ULID factory + */ + public static UlidFactory getMonotonicFactory() { + return new MonotonicUlidFactory(); } - private static class DefaultCreatorHolder { - static final UlidSpecCreator INSTANCE = getUlidSpecCreator(); + private static class DefaultFactoryHolder { + static final UlidFactory INSTANCE = getDefaultFactory(); } - private static class MonotonicCreatorHolder { - static final UlidSpecCreator INSTANCE = getMonotonicUlidSpecCreator(); + private static class MonotonicFactoryHolder { + static final UlidFactory INSTANCE = getMonotonicFactory(); } } diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/DefaultUlidFactory.java similarity index 71% rename from src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreator.java rename to src/main/java/com/github/f4b6a3/ulid/creator/DefaultUlidFactory.java index 0d21d19..4d83c98 100644 --- a/src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/DefaultUlidFactory.java @@ -26,23 +26,25 @@ package com.github.f4b6a3.ulid.creator; import com.github.f4b6a3.ulid.Ulid; -public final class MonotonicUlidSpecCreator extends UlidSpecCreator { - - private long lastTime; - private Ulid lastUlid; +/** + * Factory that generates default ULIDs. + * + * The random component is always reset to a new random value. + * + * The maximum ULIDs that can be generated per millisecond is 2^80. + */ +public class DefaultUlidFactory extends UlidFactory { + /** + * Returns a ULID. + * + * @param time a specific time + * @return a ULID + */ @Override - public synchronized Ulid create(final long time) { - - if (time == this.lastTime) { - this.lastUlid = lastUlid.increment(); - } else { - final byte[] random = new byte[10]; - this.randomStrategy.nextBytes(random); - this.lastUlid = Ulid.of(time, random); - } - - this.lastTime = time; - return this.lastUlid; + public Ulid create(final long time) { + final byte[] random = new byte[Ulid.RANDOM_BYTES_LENGTH]; + this.randomGenerator.nextBytes(random); + return new Ulid(time, random); } } diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidFactory.java b/src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidFactory.java new file mode 100644 index 0000000..765e42b --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidFactory.java @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2020 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 com.github.f4b6a3.ulid.creator; + +import com.github.f4b6a3.ulid.Ulid; + +/** + * Factory that generates Monotonic ULIDs. + * + * The random component is reset to a new value every time the millisecond changes. + * + * If more than one ULID is generated within the same millisecond, the random + * component is incremented by one. + * + * The maximum ULIDs that can be generated per millisecond is 2^80. + */ +public final class MonotonicUlidFactory extends UlidFactory { + + private long lastTime = -1; + private Ulid lastUlid = null; + + /** + * Returns a ULID. + * + * @param time a specific time + * @return a ULID + */ + @Override + public synchronized Ulid create(final long time) { + + if (time == this.lastTime) { + this.lastUlid = lastUlid.increment(); + } else { + final byte[] random = new byte[Ulid.RANDOM_BYTES_LENGTH]; + this.randomGenerator.nextBytes(random); + this.lastUlid = new Ulid(time, random); + } + + this.lastTime = time; + return new Ulid(this.lastUlid); + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/UlidFactory.java similarity index 56% rename from src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java rename to src/main/java/com/github/f4b6a3/ulid/creator/UlidFactory.java index 8dc532c..100283b 100644 --- a/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/UlidFactory.java @@ -27,41 +27,55 @@ package com.github.f4b6a3.ulid.creator; import java.util.Random; import com.github.f4b6a3.ulid.Ulid; -import com.github.f4b6a3.ulid.strategy.DefaultRandomStrategy; -import com.github.f4b6a3.ulid.strategy.RandomStrategy; +import com.github.f4b6a3.ulid.random.DefaultRandomGenerator; +import com.github.f4b6a3.ulid.random.RandomGenerator; -public class UlidSpecCreator { +/** + * An abstract factory for generating ULIDs. + * + * The only method that must be implemented is {@link UlidFactory#create(long)}. + */ +public abstract class UlidFactory { - protected RandomStrategy randomStrategy; + protected RandomGenerator randomGenerator; - public UlidSpecCreator() { - this.randomStrategy = new DefaultRandomStrategy(); + public UlidFactory() { + this.randomGenerator = new DefaultRandomGenerator(); } + /** + * Returns a UUID. + * + * @return a ULID + */ public Ulid create() { return create(System.currentTimeMillis()); } - public Ulid create(final long time) { - final byte[] random = new byte[10]; - this.randomStrategy.nextBytes(random); - return Ulid.of(time, random); - } + /** + * Returns a UUID with a specific time. + * + * This method must be implemented by all subclasses. + * + * @param time a specific time + * @return a ULID + */ + public abstract Ulid create(final long time); /** - * Replaces the default random strategy with another. + * Replaces the default random generator with another. * - * The default random strategy uses {@link java.security.SecureRandom}. + * The default random generator uses {@link java.security.SecureRandom}. * * See {@link Random}. * - * @param random a random generator - * @param the type parameter - * @return {@link AbstractRandomBasedUuidCreator} + * @param the type parameter + * @param randomGenerator a random generator + * @return {@link UlidFactory} */ @SuppressWarnings("unchecked") - public synchronized T withRandomStrategy(RandomStrategy randomStrategy) { - this.randomStrategy = randomStrategy; + public synchronized T withRandomGenerator(RandomGenerator randomGenerator) { + this.randomGenerator = randomGenerator; return (T) this; } } diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/DefaultRandomStrategy.java b/src/main/java/com/github/f4b6a3/ulid/random/DefaultRandomGenerator.java similarity index 92% rename from src/main/java/com/github/f4b6a3/ulid/strategy/DefaultRandomStrategy.java rename to src/main/java/com/github/f4b6a3/ulid/random/DefaultRandomGenerator.java index eec3935..0f082b8 100644 --- a/src/main/java/com/github/f4b6a3/ulid/strategy/DefaultRandomStrategy.java +++ b/src/main/java/com/github/f4b6a3/ulid/random/DefaultRandomGenerator.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.strategy; +package com.github.f4b6a3.ulid.random; import java.security.SecureRandom; import java.util.Random; @@ -30,7 +30,7 @@ import java.util.Random; /** * It uses an instance of {@link java.security.SecureRandom}. */ -public final class DefaultRandomStrategy implements RandomStrategy { +public final class DefaultRandomGenerator implements RandomGenerator { private static final Random SECURE_RANDOM = new SecureRandom(); diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/RandomStrategy.java b/src/main/java/com/github/f4b6a3/ulid/random/RandomGenerator.java similarity index 94% rename from src/main/java/com/github/f4b6a3/ulid/strategy/RandomStrategy.java rename to src/main/java/com/github/f4b6a3/ulid/random/RandomGenerator.java index dd34e14..a58bce5 100644 --- a/src/main/java/com/github/f4b6a3/ulid/strategy/RandomStrategy.java +++ b/src/main/java/com/github/f4b6a3/ulid/random/RandomGenerator.java @@ -22,9 +22,9 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.strategy; +package com.github.f4b6a3.ulid.random; @FunctionalInterface -public interface RandomStrategy { +public interface RandomGenerator { void nextBytes(byte[] bytes); } diff --git a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java index fbbae66..894649e 100644 --- a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java +++ b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java @@ -3,13 +3,13 @@ package com.github.f4b6a3.ulid; import org.junit.runner.RunWith; import org.junit.runners.Suite; -import com.github.f4b6a3.ulid.creator.MonotonicUlidSpecCreatorTest; -import com.github.f4b6a3.ulid.creator.UlidSpecCreatorTest; +import com.github.f4b6a3.ulid.creator.MonotonicUlidFactoryTest; +import com.github.f4b6a3.ulid.creator.DefaultUlidFactoryTest; @RunWith(Suite.class) @Suite.SuiteClasses({ - MonotonicUlidSpecCreatorTest.class, - UlidSpecCreatorTest.class, + MonotonicUlidFactoryTest.class, + DefaultUlidFactoryTest.class, UlidTest.class, }) diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java index ed2a9b4..c22d851 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UlidTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java @@ -3,7 +3,10 @@ package com.github.f4b6a3.ulid; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.nio.ByteBuffer; +import java.time.Instant; import java.util.Random; import java.util.UUID; @@ -13,98 +16,428 @@ import com.github.f4b6a3.ulid.Ulid; public class UlidTest { - private static final int DEFAULT_LOOP_MAX = 10_000; + private static final int DEFAULT_LOOP_MAX = 1_000; + + protected static final long TIME_MASK = 0x0000ffffffffffffL; protected static final char[] ALPHABET_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray(); protected static final char[] ALPHABET_JAVA = "0123456789abcdefghijklmnopqrstuv".toCharArray(); // Long.parseUnsignedLong() - @Test - public void testOfAndToString() { - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - UUID uuid0 = UUID.randomUUID(); - String string0 = toString(uuid0); - String string1 = Ulid.of(string0).toString(); - assertEquals(string0, string1); - } - } - - @Test - public void testOfAndToUuid() { - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - UUID uuid0 = UUID.randomUUID(); - UUID uuid1 = Ulid.of(uuid0).toUuid(); - assertEquals(uuid0.toString(), uuid1.toString()); - } - } + private static final long VERSION_MASK = 0x000000000000f000L; + private static final long VARIANT_MASK = 0xc000000000000000L; @Test public void testConstructorLongs() { + Random random = new Random(); for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - Random random = new Random(); final long msb = random.nextLong(); final long lsb = random.nextLong(); - Ulid ulid0 = new Ulid(msb, lsb); // <-- under test - - assertEquals(msb, ulid0.toUuid().getMostSignificantBits()); - assertEquals(lsb, ulid0.toUuid().getLeastSignificantBits()); + Ulid ulid0 = new Ulid(msb, lsb); // <-- test Ulid(long, long) + assertEquals(msb, ulid0.getMostSignificantBits()); + assertEquals(lsb, ulid0.getLeastSignificantBits()); } } @Test - public void testConstructorString() { - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - Random random = new Random(); - final long random1 = random.nextLong(); - final long random2 = random.nextLong(); - Ulid ulid0 = new Ulid(random1, random2); + public void testConstructorTimeAndRandom() { + Random random = new Random(); - String string1 = toString(ulid0); - Ulid struct1 = Ulid.of(string1); // <-- under test - assertEquals(ulid0, struct1); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + final long msb = random.nextLong(); + final long lsb = random.nextLong(); + + // get the time + long time = msb >>> 16; + + // get the random bytes + ByteBuffer buffer = ByteBuffer.allocate(Ulid.RANDOM_BYTES_LENGTH); + buffer.put((byte) ((msb >>> 8) & 0xff)); + buffer.put((byte) (msb & 0xff)); + buffer.putLong(lsb); + byte[] bytes = buffer.array(); + + Ulid ulid0 = new Ulid(time, bytes); // <-- test Ulid(long, byte[]) + assertEquals(msb, ulid0.getMostSignificantBits()); + assertEquals(lsb, ulid0.getLeastSignificantBits()); + } + + try { + long time = 0x0000ffffffffffffL + 1; // greater than 2^48-1 + byte[] bytes = new byte[Ulid.RANDOM_BYTES_LENGTH]; + new Ulid(time, bytes); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + try { + long time = 0x1000000000000000L; // negative number + byte[] bytes = new byte[Ulid.RANDOM_BYTES_LENGTH]; + new Ulid(time, bytes); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + try { + long time = 0x0000000000000000L; + byte[] bytes = null; // null random component + new Ulid(time, bytes); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + try { + long time = 0x0000000000000000L; + byte[] bytes = new byte[Ulid.RANDOM_BYTES_LENGTH + 1]; // random component with invalid size + new Ulid(time, bytes); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success } } @Test - public void testConstructorUuid() { + public void testFromStrings() { + Random random = new Random(); for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - Random random = new Random(); final long msb = random.nextLong(); final long lsb = random.nextLong(); - final UUID uuid0 = new UUID(msb, lsb); - Ulid struct0 = Ulid.of(uuid0); // <-- under test - - UUID uuid1 = toUuid(struct0); - assertEquals(uuid0, uuid1); + Ulid ulid0 = new Ulid(msb, lsb); + String string0 = toString(ulid0); + Ulid ulid1 = Ulid.from(string0); // <- test Ulid.from(String) + assertEquals(ulid0, ulid1); } } @Test public void testToString() { - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - Random random = new Random(); - final long random1 = random.nextLong(); - final long random2 = random.nextLong(); - Ulid ulid0 = new Ulid(random1, random2); + UUID uuid0 = UUID.randomUUID(); + String string0 = toString(uuid0); + String string1 = Ulid.from(uuid0).toString(); // <- test Ulid.toString() + assertEquals(string0, string1); + } + } + + @Test + public void testToUpperCase() { + Random random = new Random(); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + final long msb = random.nextLong(); + final long lsb = random.nextLong(); + Ulid ulid0 = new Ulid(msb, lsb); String string1 = toString(ulid0); - String string2 = ulid0.toString(); // <-- under test + String string2 = ulid0.toUpperCase(); // <- test Ulid.toUpperCase() assertEquals(string1, string2); + + // RFC-4122 UUID v4 + UUID uuid0 = new UUID(msb, lsb); + String string3 = ulid0.toRfc4122().toUpperCase(); // <- test Ulid.toRfc4122().toUpperCase() + Ulid ulid3 = fromString(string3); + UUID uuid3 = new UUID(ulid3.getMostSignificantBits(), ulid3.getLeastSignificantBits()); + assertEquals(4, uuid3.version()); // check version + assertEquals(2, uuid3.variant()); // check variant + assertEquals(uuid0.getMostSignificantBits() & ~VERSION_MASK, + uuid3.getMostSignificantBits() & ~VERSION_MASK); // check the rest of MSB + assertEquals(uuid0.getLeastSignificantBits() & ~VARIANT_MASK, + uuid3.getLeastSignificantBits() & ~VARIANT_MASK); // check the rest of LSB + + } + } + + @Test + public void testToLowerCase() { + Random random = new Random(); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + final long msb = random.nextLong(); + final long lsb = random.nextLong(); + Ulid ulid0 = new Ulid(msb, lsb); + + String string1 = toString(ulid0).toLowerCase(); + String string2 = ulid0.toLowerCase(); // <- test Ulid.toLowerCase() + assertEquals(string1, string2); + + // RFC-4122 UUID v4 + UUID uuid0 = new UUID(msb, lsb); + String string3 = ulid0.toRfc4122().toLowerCase(); // <- test Ulid.toRfc4122().toLowerCase() + Ulid ulid3 = fromString(string3); + UUID uuid3 = new UUID(ulid3.getMostSignificantBits(), ulid3.getLeastSignificantBits()); + assertEquals(4, uuid3.version()); // check version + assertEquals(2, uuid3.variant()); // check variant + assertEquals(uuid0.getMostSignificantBits() & ~VERSION_MASK, + uuid3.getMostSignificantBits() & ~VERSION_MASK); // check the rest of MSB + assertEquals(uuid0.getLeastSignificantBits() & ~VARIANT_MASK, + uuid3.getLeastSignificantBits() & ~VARIANT_MASK); // check the rest of LSB + } + } + + @Test + public void testFromUUID() { + Random random = new Random(); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + final long msb = random.nextLong(); + final long lsb = random.nextLong(); + UUID uuid0 = new UUID(msb, lsb); + Ulid ulid0 = Ulid.from(uuid0); // <- test Ulid.from(UUID) + assertEquals(uuid0.getMostSignificantBits(), ulid0.getMostSignificantBits()); + assertEquals(uuid0.getLeastSignificantBits(), ulid0.getLeastSignificantBits()); } } @Test public void testToUuid() { - + Random random = new Random(); for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - Random random = new Random(); + final long random1 = random.nextLong(); final long random2 = random.nextLong(); Ulid ulid0 = new Ulid(random1, random2); UUID uuid1 = toUuid(ulid0); - UUID uuid2 = ulid0.toUuid(); // <-- under test + UUID uuid2 = ulid0.toUuid(); // <-- test Ulid.toUuid() assertEquals(uuid1, uuid2); + + // RFC-4122 UUID v4 + UUID uuid3 = ulid0.toRfc4122().toUuid(); // <-- test Ulid.toRfc4122().toUuid() + assertEquals(4, uuid3.version()); // check version + assertEquals(2, uuid3.variant()); // check variant + assertEquals(uuid1.getMostSignificantBits() & ~VERSION_MASK, + uuid3.getMostSignificantBits() & ~VERSION_MASK); // check the rest of MSB + assertEquals(uuid1.getLeastSignificantBits() & ~VARIANT_MASK, + uuid3.getLeastSignificantBits() & ~VARIANT_MASK); // check the rest of LSB + } + } + + @Test + public void testFromBytes() { + Random random = new Random(); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + byte[] bytes0 = new byte[Ulid.ULID_BYTES_LENGTH]; + random.nextBytes(bytes0); + + Ulid ulid0 = Ulid.from(bytes0); // <- test Ulid.from(UUID) + ByteBuffer buffer = ByteBuffer.allocate(Ulid.ULID_BYTES_LENGTH); + buffer.putLong(ulid0.getMostSignificantBits()); + buffer.putLong(ulid0.getLeastSignificantBits()); + byte[] bytes1 = buffer.array(); + + for (int j = 0; j < bytes0.length; j++) { + assertEquals(bytes0[j], bytes1[j]); + } + } + + try { + byte[] bytes = null; + Ulid.from(bytes); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + try { + byte[] bytes = new byte[Ulid.ULID_BYTES_LENGTH + 1]; + Ulid.from(bytes); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + } + + @Test + public void testToBytes() { + Random random = new Random(); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + byte[] bytes1 = new byte[16]; + random.nextBytes(bytes1); + Ulid ulid0 = Ulid.from(bytes1); + + byte[] bytes2 = ulid0.toBytes(); // <-- test Ulid.toBytes() + for (int j = 0; j < bytes1.length; j++) { + assertEquals(bytes1[j], bytes2[j]); + } + + // RFC-4122 UUID v4 + byte[] bytes3 = ulid0.toRfc4122().toBytes(); // <-- test Ulid.toBytes4() + assertEquals(0x40, bytes3[6] & 0b11110000); // check version + assertEquals(bytes1[6] & 0b00001111, bytes3[6] & 0b00001111); // check the other bits of 7th byte + assertEquals(0x80, bytes3[8] & 0b11000000); // check variant + assertEquals(bytes1[8] & 0b00111111, bytes3[8] & 0b00111111); // check the other bits of 9th byte + for (int j = 0; j < bytes1.length; j++) { + if (j == 6 || j == 8) + continue; + assertEquals(bytes1[j], bytes3[j]); // check the other bytes + } + } + } + + @Test + public void testGetTimeAndGetRandom() { + + long time = 0; + byte[] bytes = new byte[10]; + Random random = new Random(); + + for (int i = 0; i < 100; i++) { + + time = random.nextLong() & TIME_MASK; + random.nextBytes(bytes); + Ulid ulid = new Ulid(time, bytes); + + assertEquals(time, ulid.getTime()); // test Ulid.getTime() + assertEquals(Instant.ofEpochMilli(time), ulid.getInstant()); // test Ulid.getInstant() + for (int j = 0; j < bytes.length; j++) { + assertEquals(bytes[j], ulid.getRandom()[j]); // test Ulid.getRandom() + } + } + } + + @Test + public void testIncrement() { + + long msb; + long lsb; + Ulid ulid; + + final int loopMax = 100; + + msb = 0x0123456789abcdefL; + lsb = 0x0123456789abcdefL; + ulid = new Ulid(msb, lsb); + for (int i = 0; i < loopMax; i++) { + ulid = ulid.increment(); + } + assertEquals(msb, ulid.getMostSignificantBits()); + assertEquals(msb + loopMax, ulid.getLeastSignificantBits()); + + msb = 0x0123456789abcdefL; + lsb = 0xffffffffffffffffL - (loopMax / 2); + ulid = new Ulid(msb, lsb); + for (int i = 0; i < loopMax; i++) { + ulid = ulid.increment(); + } + assertEquals(msb + 1, ulid.getMostSignificantBits()); + assertEquals((loopMax / 2) - 1, ulid.getLeastSignificantBits()); + } + + @Test + public void testIsValidString() { + + String ulid = null; // Null + assertFalse("Null ULID should be invalid.", Ulid.isValid(ulid)); + + ulid = ""; // length: 0 + assertFalse("ULID with empty string should be invalid .", Ulid.isValid(ulid)); + + ulid = "0123456789ABCDEFGHJKMNPQRS"; // All upper case + assertTrue("ULID in upper case should valid.", Ulid.isValid(ulid)); + + ulid = "0123456789abcdefghjklmnpqr"; // All lower case + assertTrue("ULID in lower case should be valid.", Ulid.isValid(ulid)); + + ulid = "0123456789AbCdEfGhJkMnPqRs"; // Mixed case + assertTrue("Ulid in upper and lower case should valid.", Ulid.isValid(ulid)); + + ulid = "0123456789ABCDEFGHJKLMNPQ"; // length: 25 + assertFalse("ULID length lower than 26 should be invalid.", Ulid.isValid(ulid)); + + ulid = "0123456789ABCDEFGHJKMNPQZZZ"; // length: 27 + assertFalse("ULID length greater than 26 should be invalid.", Ulid.isValid(ulid)); + + ulid = "u123456789ABCDEFGHJKMNPQRS"; // Letter u + assertFalse("ULID with 'u' or 'U' should be invalid.", Ulid.isValid(ulid)); + + ulid = "0123456789ABCDEFGHJKMNPQR#"; // Special char + assertFalse("ULID with special chars should be invalid.", Ulid.isValid(ulid)); + + ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // time > (2^48)-1 + assertFalse("ULID with timestamp greater than (2^48)-1 should be invalid.", Ulid.isValid(ulid)); + } + + @Test + public void testToCharArray() { + + String ulid = null; // Null + try { + Ulid.toCharArray(ulid); + fail("Null ULID should be invalid."); + } catch (IllegalArgumentException e) { + // success + } + + ulid = ""; // length: 0 + try { + Ulid.toCharArray(ulid); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + ulid = "0123456789ABCDEFGHJKMNPQRS"; // All upper case + try { + Ulid.toCharArray(ulid); + } catch (IllegalArgumentException e) { + fail("Should not throw an exception"); + } + + ulid = "0123456789abcdefghjklmnpqr"; // All lower case + try { + Ulid.toCharArray(ulid); + } catch (IllegalArgumentException e) { + fail("Should not throw an exception"); + } + + ulid = "0123456789AbCdEfGhJkMnPqRs"; // Mixed case + try { + Ulid.toCharArray(ulid); + } catch (IllegalArgumentException e) { + fail("Should not throw an exception"); + } + + ulid = "0123456789ABCDEFGHJKLMNPQ"; // length: 25 + try { + Ulid.toCharArray(ulid); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + ulid = "0123456789ABCDEFGHJKMNPQZZZ"; // length: 27 + try { + Ulid.toCharArray(ulid); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + ulid = "u123456789ABCDEFGHJKMNPQRS"; // Letter u + try { + Ulid.toCharArray(ulid); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + ulid = "0123456789ABCDEFGHJKMNPQR@"; // Special char + try { + Ulid.toCharArray(ulid); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success + } + + ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // time > (2^48)-1 + try { + Ulid.toCharArray(ulid); + fail("Should throw an exception"); + } catch (IllegalArgumentException e) { + // success } } @@ -114,9 +447,9 @@ public class UlidTest { long random1 = 0; long random2 = 0; - String tm = string.substring(0, 10); - String r1 = string.substring(10, 18); - String r2 = string.substring(18, 26); + String tm = string.substring(0, 10).toUpperCase(); + String r1 = string.substring(10, 18).toUpperCase(); + String r2 = string.substring(18, 26).toUpperCase(); tm = transliterate(tm, ALPHABET_CROCKFORD, ALPHABET_JAVA); r1 = transliterate(r1, ALPHABET_CROCKFORD, ALPHABET_JAVA); @@ -193,40 +526,6 @@ public class UlidTest { return r1 + r2; } - - @Test - public void isValidString() { - - String ulid = null; // Null - assertFalse("Null ULID should be invalid.", Ulid.isValidString(ulid)); - - ulid = ""; // length: 0 - assertFalse("ULID with empty string should be invalid .", Ulid.isValidString(ulid)); - - ulid = "0123456789ABCDEFGHJKMNPQRS"; // All upper case - assertTrue("ULID in upper case should valid.", Ulid.isValidString(ulid)); - - ulid = "0123456789abcdefghjklmnpqr"; // All lower case - assertTrue("ULID in lower case should be valid.", Ulid.isValidString(ulid)); - - ulid = "0123456789AbCdEfGhJkMnPqRs"; // Mixed case - assertTrue("Ulid in upper and lower case should valid.", Ulid.isValidString(ulid)); - - ulid = "0123456789ABCDEFGHJKLMNPQ"; // length: 25 - assertFalse("ULID length lower than 26 should be invalid.", Ulid.isValidString(ulid)); - - ulid = "0123456789ABCDEFGHJKMNPQZZZ"; // length: 27 - assertFalse("ULID length greater than 26 should be invalid.", Ulid.isValidString(ulid)); - - ulid = "u123456789ABCDEFGHJKMNPQRS"; // Letter u - assertFalse("ULID with 'u' or 'U' should be invalid.", Ulid.isValidString(ulid)); - - ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char - assertFalse("ULID with special chars should be invalid.", Ulid.isValidString(ulid)); - - ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1 - assertFalse("ULID with timestamp greater than (2^48)-1 should be invalid.", Ulid.isValidString(ulid)); - } private static String transliterate(String string, char[] alphabet1, char[] alphabet2) { char[] output = string.toCharArray(); diff --git a/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java index 9899d33..dfd4761 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java @@ -2,7 +2,7 @@ package com.github.f4b6a3.ulid; import java.util.HashSet; import com.github.f4b6a3.ulid.UlidCreator; -import com.github.f4b6a3.ulid.creator.UlidSpecCreator; +import com.github.f4b6a3.ulid.creator.UlidFactory; /** * @@ -22,8 +22,8 @@ public class UniquenessTest { private boolean verbose; // Show progress or not - // ULID Spec creator - private UlidSpecCreator creator; + // ULID Spec factory + private UlidFactory factory; private long time = System.currentTimeMillis(); // fixed timestamp @@ -32,12 +32,12 @@ public class UniquenessTest { * * @param threadCount * @param requestCount - * @param creator + * @param factory */ - public UniquenessTest(int threadCount, int requestCount, UlidSpecCreator creator, boolean progress) { + public UniquenessTest(int threadCount, int requestCount, UlidFactory factory, boolean progress) { this.threadCount = threadCount; this.requestCount = requestCount; - this.creator = creator; + this.factory = factory; this.verbose = progress; this.initCache(); } @@ -90,13 +90,13 @@ public class UniquenessTest { for (int i = 0; i < max; i++) { - // Request a UUID - Ulid ulid = creator.create(time); + // Request a ULID + Ulid ulid = factory.create(time); if (verbose) { - // Calculate and show progress - progress = (int) ((i * 1.0 / max) * 100); - if (progress % 10 == 0) { + if (i % (max / 100) == 0) { + // Calculate and show progress + progress = (int) ((i * 1.0 / max) * 100); System.out.println(String.format("[Thread %06d] %s %s %s%%", id, ulid, i, (int) progress)); } } @@ -117,9 +117,9 @@ public class UniquenessTest { } public static void execute(boolean verbose, int threadCount, int requestCount) { - UlidSpecCreator creator = UlidCreator.getMonotonicUlidSpecCreator(); + UlidFactory factory = UlidCreator.getMonotonicFactory(); - UniquenessTest test = new UniquenessTest(threadCount, requestCount, creator, verbose); + UniquenessTest test = new UniquenessTest(threadCount, requestCount, factory, verbose); test.start(); } diff --git a/src/test/java/com/github/f4b6a3/ulid/bench/Benchmarks.java b/src/test/java/com/github/f4b6a3/ulid/bench/Benchmarks.java index 7bbd0f1..60f0816 100644 --- a/src/test/java/com/github/f4b6a3/ulid/bench/Benchmarks.java +++ b/src/test/java/com/github/f4b6a3/ulid/bench/Benchmarks.java @@ -31,6 +31,7 @@ import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; +import com.github.f4b6a3.ulid.Ulid; import com.github.f4b6a3.ulid.UlidCreator; @Threads(1) @@ -42,47 +43,47 @@ public class Benchmarks { @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) - public String getUlidStringThroughput() { - return UlidCreator.getUlidString(); + public UUID getUuid() { + return UUID.randomUUID(); } - @Benchmark - @BenchmarkMode(Mode.AverageTime) - @OutputTimeUnit(TimeUnit.NANOSECONDS) - public String getUlidStringAverage() { - return UlidCreator.getUlidString(); - } - @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) - public UUID getUlidThroughput() { - return UlidCreator.getUlid(); + public String getUuidString() { + return UUID.randomUUID().toString(); } @Benchmark - @BenchmarkMode(Mode.AverageTime) - @OutputTimeUnit(TimeUnit.NANOSECONDS) - public UUID getUlidAverage() { + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public Ulid getUlid() { return UlidCreator.getUlid(); } @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) - public UUID getRandomUUIDThroughput() { - return UUID.randomUUID(); + public String getUlidString() { + return UlidCreator.getUlid().toString(); } @Benchmark - @BenchmarkMode(Mode.AverageTime) - @OutputTimeUnit(TimeUnit.NANOSECONDS) - public UUID getRandomUUIDAverage() { - return UUID.randomUUID(); + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public Ulid getMonotonicUlid() { + return UlidCreator.getMonotonicUlid(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + public String getMonotonicUlidString() { + return UlidCreator.getMonotonicUlid().toString(); } public static void main(String[] args) throws RunnerException { - Options opt = new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).forks(1).build(); + Options opt = new OptionsBuilder().include(Benchmarks.class.getSimpleName()).forks(1).build(); new Runner(opt).run(); } } diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/DefaultUlidFactoryTest.java similarity index 88% rename from src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java rename to src/test/java/com/github/f4b6a3/ulid/creator/DefaultUlidFactoryTest.java index 4cf38c4..ad25361 100644 --- a/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/creator/DefaultUlidFactoryTest.java @@ -8,8 +8,9 @@ import com.github.f4b6a3.ulid.UlidCreator; import static org.junit.Assert.*; import java.util.HashSet; +import java.util.Random; -public class UlidSpecCreatorTest extends AbstractUlidSpecCreatorTest { +public class DefaultUlidFactoryTest extends UlidFactoryTest { @Test public void testGetUlid() { @@ -65,7 +66,9 @@ public class UlidSpecCreatorTest extends AbstractUlidSpecCreatorTest { // Instantiate and start many threads for (int i = 0; i < THREAD_TOTAL; i++) { - threads[i] = new TestThread(UlidCreator.getUlidSpecCreator(), DEFAULT_LOOP_MAX); + Random random = new Random(); + UlidFactory factory = UlidCreator.getDefaultFactory().withRandomGenerator(random::nextBytes); + threads[i] = new TestThread(factory, DEFAULT_LOOP_MAX); threads[i].start(); } diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidFactoryTest.java similarity index 89% rename from src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreatorTest.java rename to src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidFactoryTest.java index c8e1f2b..0823f64 100644 --- a/src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidFactoryTest.java @@ -9,8 +9,9 @@ import static org.junit.Assert.*; import java.util.Arrays; import java.util.HashSet; +import java.util.Random; -public class MonotonicUlidSpecCreatorTest extends AbstractUlidSpecCreatorTest { +public class MonotonicUlidFactoryTest extends UlidFactoryTest { @Test public void testGetUlid() { @@ -76,7 +77,9 @@ public class MonotonicUlidSpecCreatorTest extends AbstractUlidSpecCreatorTest { // Instantiate and start many threads for (int i = 0; i < THREAD_TOTAL; i++) { - threads[i] = new TestThread(UlidCreator.getMonotonicUlidSpecCreator(), DEFAULT_LOOP_MAX); + Random random = new Random(); + UlidFactory factory = UlidCreator.getMonotonicFactory().withRandomGenerator(random::nextBytes); + threads[i] = new TestThread(factory, DEFAULT_LOOP_MAX); threads[i].start(); } diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/AbstractUlidSpecCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/UlidFactoryTest.java similarity index 88% rename from src/test/java/com/github/f4b6a3/ulid/creator/AbstractUlidSpecCreatorTest.java rename to src/test/java/com/github/f4b6a3/ulid/creator/UlidFactoryTest.java index 09b70d8..46a26db 100644 --- a/src/test/java/com/github/f4b6a3/ulid/creator/AbstractUlidSpecCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/creator/UlidFactoryTest.java @@ -5,7 +5,7 @@ import java.util.Random; import java.util.Set; import java.util.UUID; -public abstract class AbstractUlidSpecCreatorTest { +public abstract class UlidFactoryTest { protected static final int DEFAULT_LOOP_MAX = 10_000; @@ -28,10 +28,10 @@ public abstract class AbstractUlidSpecCreatorTest { protected static class TestThread extends Thread { public static Set hashSet = new HashSet<>(); - private UlidSpecCreator creator; + private UlidFactory creator; private int loopLimit; - public TestThread(UlidSpecCreator creator, int loopLimit) { + public TestThread(UlidFactory creator, int loopLimit) { this.creator = creator; this.loopLimit = loopLimit; }