diff --git a/LICENSE b/LICENSE index 9426370..02d2a8f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 f4b6a3 +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 diff --git a/README.md b/README.md index 0bd9d56..5cce155 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Create a ULID as GUID: UUID ulid = UlidCreator.getUlid(); ``` -Create a ULID string: +Create a ULID as string: ```java String ulid = UlidCreator.getUlidString(); @@ -28,7 +28,7 @@ Add these lines to your `pom.xml`. com.github.f4b6a3 ulid-creator - 1.1.1 + 2.0.0 ``` See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). @@ -36,7 +36,45 @@ See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b Implementation ------------------------------------------------------ -### ULID string +### ULID as GUID + +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 a thread safe `java.security.SecureRandom`, but it's possible to use any RNG that extends `java.util.Random`. + +```java +// GUID based on ULID spec +UUID ulid = UlidCreator.getUlid(); +``` + +Examples of GUIDs based on ULID spec: + +```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 +``` + +### ULID as string The ULID is a 26 char sequence. See the [ULID specification](https://github.com/ulid/spec) for more information. @@ -72,66 +110,68 @@ Examples of ULIDs: milli randomness ``` -### Ulid-based GUID +#### How use the `UlidSpecCreator` directly -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`, but it's possible to use any RNG that extends `java.util.Random`. - -```java -// GUID based on ULID spec -UUID ulid = UlidCreator.getUlid(); -``` - -Examples of GUIDs based on ULID spec: - -```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 -``` - -#### How use the `UlidBasedGuidCreator` directly - -These are some examples of using the `UlidBasedGuidCreator` to create ULIDs strings: +These are some examples of using the `UlidSpecCreator` to create ULIDs strings: ```java // with your custom timestamp strategy TimestampStrategy customStrategy = new CustomTimestampStrategy(); -String ulid = UlidCreator.getUlidBasedGuidCreator() +String ulid = UlidCreator.getUlidSpecCreator() .withTimestampStrategy(customStrategy) .createString(); +// with your custom random strategy that wraps any random generator +RandomStrategy customStrategy = new CustomRandomStrategy(); +String ulid = UlidCreator.getUlidSpecCreator() + .withRandomStrategy(customStrategy) + .createString(); + // with `java.util.Random` number generator Random random = new Random(); -String ulid = UlidCreator.getUlidBasedGuidCreator() +String ulid = UlidCreator.getUlidSpecCreator() .withRandomGenerator(random) .createString(); - -// with fast random generator (the same as above) -String ulid = UlidCreator.getUlidBasedGuidCreator() - .withFastRandomGenerator() - .createString(); ``` + +Benchmark +------------------------------------------------------ + +This section shows benchmarks comparing `UlidCreator` to `java.util.UUID`. + +``` +--------------------------------------------------------------------------- +THROUGHPUT Mode Cnt Score Error Units +--------------------------------------------------------------------------- +Throughput.Java_RandomBased thrpt 5 2234,199 ± 2,844 ops/ms +Throughput.UlidCreator_Ulid thrpt 5 19155,742 ± 22,195 ops/ms +Throughput.UlidCreator_UlidString thrpt 5 4946,479 ± 22,800 ops/ms +--------------------------------------------------------------------------- +Total time: 00:06:41 +--------------------------------------------------------------------------- +``` + +``` +---------------------------------------------------------------------- +AVERAGE TIME Mode Cnt Score Error Units +---------------------------------------------------------------------- +AverageTime.Java_RandomBased avgt 5 449,641 ± 0,994 ns/op +AverageTime.UlidCreator_Ulid avgt 5 52,199 ± 0,185 ns/op +AverageTime.UlidCreator_UlidString avgt 5 202,014 ± 2,111 ns/op +---------------------------------------------------------------------- +Total time: 00:06:41 +---------------------------------------------------------------------- +``` + +System: CPU i5-3330, 8G RAM, Ubuntu 20.04. + +See: [uuid-creator-benchmark](https://github.com/fabiolimace/uuid-creator-benchmark) + +Links for 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) diff --git a/pom.xml b/pom.xml index 1ce9761..4a1982b 100644 --- a/pom.xml +++ b/pom.xml @@ -3,11 +3,11 @@ com.github.f4b6a3 ulid-creator - 1.1.2-SNAPSHOT + 2.0.0-SNAPSHOT jar ulid-creator - http://github.com/f4b6a3 + http://github.com/f4b6a3/ulid-creator A Java library for generating and handling ULIDs. @@ -26,17 +26,12 @@ UTF-8 - 11 + 8 ${jdk.version} ${jdk.version} - - com.github.f4b6a3 - util - 1.0.0 - junit junit @@ -46,10 +41,10 @@ - scm:git:git://github.com/dexecutor/dependent-tasks-executor.git - scm:git:git@github.com:dexecutor/dexecutor.git - https://github.com/dexecutor/dependent-tasks-executor - ulid-creator-1.0.0 + https://github.com/f4b6a3/ulid-creator + scm:git:ssh://git@github.com/f4b6a3/ulid-creator.git + scm:git:ssh://git@github.com/f4b6a3/ulid-creator.git + HEAD diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java index 940c82e..c0fced0 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -26,19 +26,29 @@ package com.github.f4b6a3.ulid; import java.util.UUID; -import com.github.f4b6a3.util.random.Xorshift128PlusRandom; -import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; +import com.github.f4b6a3.ulid.creator.UlidSpecCreator; +import com.github.f4b6a3.ulid.util.UlidConverter; /** * A factory for Universally Unique Lexicographically Sortable Identifiers. * * See the ULID spec: https://github.com/ulid/spec */ -public class UlidCreator { +public final class UlidCreator { private UlidCreator() { } + /** + * Returns a ULID as GUID from a string. + * + * @param ulid a ULID string + * @return a UUID + */ + public static UUID fromString(String ulid) { + return UlidConverter.fromString(ulid); + } + /** * Returns a ULID as GUID. * @@ -48,7 +58,7 @@ public class UlidCreator { * @return a UUID */ public static UUID getUlid() { - return UlidBasedGuidCreatorHolder.INSTANCE.create(); + return UlidSpecCreatorHolder.INSTANCE.create(); } /** @@ -62,49 +72,19 @@ public class UlidCreator { * @return a ULID */ public static String getUlidString() { - return UlidBasedGuidCreatorHolder.INSTANCE.createString(); - } - - /** - * Returns a ULID as GUID. - * - * The random component is generated by a fast random number generator: - * {@link Xorshift128PlusRandom}. - * - * @return a UUID - */ - public static UUID getFastUlid() { - return FastUlidBasedGuidCreatorHolder.INSTANCE.create(); - } - - /** - * Returns a fast ULID string. - * - * The returning string is encoded to Crockford's base32. - * - * The random component is generated by a fast random number generator: - * {@link Xorshift128PlusRandom}. - * - * @return a ULID - */ - public static String getFastUlidString() { - return FastUlidBasedGuidCreatorHolder.INSTANCE.createString(); + return UlidSpecCreatorHolder.INSTANCE.createString(); } /** * Return a GUID creator for direct use. * - * @return a {@link UlidBasedGuidCreator} + * @return a {@link UlidSpecCreator} */ - public static UlidBasedGuidCreator getUlidBasedCreator() { - return new UlidBasedGuidCreator(); + public static UlidSpecCreator getUlidSpecCreator() { + return new UlidSpecCreator(); } - private static class UlidBasedGuidCreatorHolder { - static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator(); - } - - private static class FastUlidBasedGuidCreatorHolder { - static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator().withFastRandomGenerator(); + private static class UlidSpecCreatorHolder { + static final UlidSpecCreator INSTANCE = getUlidSpecCreator(); } } diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java similarity index 69% rename from src/main/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreator.java rename to src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java index ac5efad..e61899f 100644 --- a/src/main/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java @@ -27,13 +27,14 @@ package com.github.f4b6a3.ulid.creator; import java.util.Random; import java.util.UUID; -import com.github.f4b6a3.ulid.util.UlidConverter; -import com.github.f4b6a3.util.random.Xorshift128PlusRandom; -import com.github.f4b6a3.util.FingerprintUtil; -import com.github.f4b6a3.util.RandomUtil; +import com.github.f4b6a3.ulid.strategy.RandomStrategy; +import com.github.f4b6a3.ulid.strategy.random.DefaultRandomStrategy; +import com.github.f4b6a3.ulid.strategy.random.OtherRandomStrategy; import com.github.f4b6a3.ulid.exception.UlidCreatorException; import com.github.f4b6a3.ulid.strategy.TimestampStrategy; import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy; +import com.github.f4b6a3.ulid.util.UlidConverter; +import com.github.f4b6a3.ulid.util.UlidUtil; /** * Factory that creates lexicographically sortable GUIDs, based on the ULID @@ -41,28 +42,28 @@ import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy; * * ULID specification: https://github.com/ulid/spec */ -public class UlidBasedGuidCreator { +public class UlidSpecCreator { - protected long randomMsb = 0; - protected long randomLsb = 0; + protected long random1 = 0; + protected long random2 = 0; - protected long randomLsbMax; - protected long randomMsbMax; + protected long randomMax2; + protected long randomMax1; protected static final long HALF_RANDOM_COMPONENT = 0x000000ffffffffffL; protected static final long INCREMENT_MAX = 0x0000010000000000L; protected long previousTimestamp; - protected Random random; - - protected static final String OVERRUN_MESSAGE = "The system overran the generator by requesting too many GUIDs."; + protected static final String OVERRUN_MESSAGE = "The system overran the generator by requesting too many ULIDs."; protected TimestampStrategy timestampStrategy; + protected RandomStrategy randomStrategy; - public UlidBasedGuidCreator() { - this.reset(); + public UlidSpecCreator() { this.timestampStrategy = new DefaultTimestampStrategy(); + this.randomStrategy = new DefaultRandomStrategy(); + this.reset(); } /** @@ -128,11 +129,11 @@ public class UlidBasedGuidCreator { final long timestamp = this.getTimestamp(); - final long randomHi = truncate(randomMsb); - final long randomLo = truncate(randomLsb); + final long rnd1 = random1 & HALF_RANDOM_COMPONENT; + final long rnd2 = random2 & HALF_RANDOM_COMPONENT; - final long msb = (timestamp << 16) | (randomHi >>> 24); - final long lsb = (randomHi << 40) | randomLo; + final long msb = (timestamp << 16) | (rnd1 >>> 24); + final long lsb = (rnd1 << 40) | rnd2; return new UUID(msb, lsb); } @@ -142,7 +143,10 @@ public class UlidBasedGuidCreator { * * The returning string is encoded to Crockford's base32. * - * @return a ULID string + * The random component is generated by a secure random number generator: + * {@link java.security.SecureRandom}. + * + * @return a ULID */ public synchronized String createString() { return UlidConverter.toString(create()); @@ -173,17 +177,13 @@ public class UlidBasedGuidCreator { protected synchronized void reset() { // Get random values - if (random == null) { - this.randomMsb = truncate(RandomUtil.get().nextLong()); - this.randomLsb = truncate(RandomUtil.get().nextLong()); - } else { - this.randomMsb = truncate(random.nextLong()); - this.randomLsb = truncate(random.nextLong()); - } - + final byte[] bytes = new byte[10]; + this.randomStrategy.nextBytes(bytes); + this.random1 = UlidUtil.toNumber(bytes, 0, 5); + this.random2 = UlidUtil.toNumber(bytes, 5, 10); // Save the random values - this.randomMsbMax = this.randomMsb | INCREMENT_MAX; - this.randomLsbMax = this.randomLsb | INCREMENT_MAX; + this.randomMax1 = this.random1 | INCREMENT_MAX; + this.randomMax2 = this.random2 | INCREMENT_MAX; } /** @@ -196,7 +196,7 @@ public class UlidBasedGuidCreator { */ protected synchronized void increment() { - if ((++this.randomLsb == this.randomLsbMax) && (++this.randomMsb == this.randomMsbMax)) { + if ((++this.random2 > this.randomMax2) && (++this.random1 > this.randomMax1)) { this.reset(); throw new UlidCreatorException(OVERRUN_MESSAGE); } @@ -206,73 +206,58 @@ public class UlidBasedGuidCreator { * Used for changing the timestamp strategy. * * @param timestampStrategy a timestamp strategy - * @return {@link UlidBasedGuidCreator} + * @return {@link UlidSpecCreator} */ @SuppressWarnings("unchecked") - public synchronized T withTimestampStrategy(TimestampStrategy timestampStrategy) { + public synchronized T withTimestampStrategy(TimestampStrategy timestampStrategy) { this.timestampStrategy = timestampStrategy; return (T) this; } /** - * Replace the default random generator, in a fluent way, to another that - * extends {@link Random}. + * Replaces the default random strategy with another. * - * The default random generator is {@link java.security.SecureRandom}. - * - * For other faster pseudo-random generators, see {@link XorshiftRandom} and its - * variations. + * The default random strategy uses {@link java.security.SecureRandom}. * * See {@link Random}. * * @param random a random generator - * @return {@link UlidBasedGuidCreator} + * @param the type parameter + * @return {@link AbstractRandomBasedUuidCreator} */ @SuppressWarnings("unchecked") - public synchronized T withRandomGenerator(Random random) { - this.random = random; + public synchronized T withRandomStrategy(RandomStrategy randomStrategy) { + this.randomStrategy = randomStrategy; return (T) this; } /** - * Replaces the default random generator with a faster one. + * Replaces the default random strategy with another that uses the input + * {@link Random} instance. * - * The host fingerprint is used to generate a seed for the random number - * generator. + * It replaces the internal {@link DefaultRandomStrategy} with + * {@link OtherRandomStrategy}. * - * See {@link Xorshift128PlusRandom} and - * {@link FingerprintUtil#getFingerprint()} - * - * @return {@link UlidBasedGuidCreator} + * @param random a random generator + * @return {@link UlidSpecCreator} */ @SuppressWarnings("unchecked") - public synchronized T withFastRandomGenerator() { - final int salt = (int) FingerprintUtil.getFingerprint(); - this.random = new Xorshift128PlusRandom(salt); + public synchronized T withRandomGenerator(Random random) { + this.randomStrategy = new OtherRandomStrategy(random); return (T) this; } - /** - * Truncate long to half random component. - * - * @param value a value to be truncated. - * @return truncated value - */ - protected synchronized long truncate(final long value) { - return (value & HALF_RANDOM_COMPONENT); - } - /** * For unit tests */ - protected long extractRandomLsb(UUID uuid) { + protected long extractRandom1(UUID uuid) { + return ((uuid.getMostSignificantBits() & 0x000000000000ffff) << 24) | (uuid.getLeastSignificantBits() >>> 40); + } + + /** + * For unit tests + */ + protected long extractRandom2(UUID uuid) { return uuid.getLeastSignificantBits() & HALF_RANDOM_COMPONENT; } - - /** - * For unit tests - */ - protected long extractRandomMsb(UUID uuid) { - return ((uuid.getMostSignificantBits() & 0xffff) << 24) | (uuid.getLeastSignificantBits() >>> 40); - } } diff --git a/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java b/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java index 7ff70a2..516dc87 100644 --- a/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java +++ b/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java @@ -24,7 +24,7 @@ package com.github.f4b6a3.ulid.exception; -public class InvalidUlidException extends RuntimeException { +public final class InvalidUlidException extends RuntimeException { private static final long serialVersionUID = 1L; diff --git a/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java b/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java index e6f2078..1cdbfaf 100644 --- a/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java +++ b/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java @@ -24,7 +24,7 @@ package com.github.f4b6a3.ulid.exception; -public class UlidCreatorException extends RuntimeException { +public final class UlidCreatorException extends RuntimeException { private static final long serialVersionUID = 1L; diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/RandomStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/RandomStrategy.java new file mode 100644 index 0000000..ab2ec58 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/RandomStrategy.java @@ -0,0 +1,29 @@ +/* + * MIT License + * + * Copyright (c) 2018-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.strategy; + +public interface RandomStrategy { + void nextBytes(byte[] bytes); +} diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/random/DefaultRandomStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/random/DefaultRandomStrategy.java new file mode 100644 index 0000000..987f2d0 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/random/DefaultRandomStrategy.java @@ -0,0 +1,43 @@ +/* + * MIT License + * + * Copyright (c) 2018-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.strategy.random; + +import java.security.SecureRandom; +import java.util.Random; + +import com.github.f4b6a3.ulid.strategy.RandomStrategy; + +/** + * It uses a thread local instance of {@link java.security.SecureRandom}. + */ +public final class DefaultRandomStrategy implements RandomStrategy { + + protected static final ThreadLocal THREAD_LOCAL_RANDOM = ThreadLocal.withInitial(SecureRandom::new); + + @Override + public void nextBytes(byte[] bytes) { + THREAD_LOCAL_RANDOM.get().nextBytes(bytes); + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/random/OtherRandomStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/random/OtherRandomStrategy.java new file mode 100644 index 0000000..be90846 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/random/OtherRandomStrategy.java @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2018-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.strategy.random; + +import java.util.Random; + +import com.github.f4b6a3.ulid.strategy.RandomStrategy; + +/** + * It uses an instance of {@link java.util.Random} injected by constructor. + */ +public final class OtherRandomStrategy implements RandomStrategy { + + private final Random random; + + public OtherRandomStrategy(Random random) { + this.random = random; + } + + @Override + public void nextBytes(byte[] bytes) { + this.random.nextBytes(bytes); + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/DefaultTimestampStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/DefaultTimestampStrategy.java index 2c9e514..eacceaf 100644 --- a/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/DefaultTimestampStrategy.java +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/DefaultTimestampStrategy.java @@ -26,10 +26,10 @@ package com.github.f4b6a3.ulid.strategy.timestamp; import com.github.f4b6a3.ulid.strategy.TimestampStrategy; -public class DefaultTimestampStrategy implements TimestampStrategy { +public final class DefaultTimestampStrategy implements TimestampStrategy { /** - * Returns the count of milliseconds since 01-01-1970. + * Returns the count of milliseconds since 1970-01-01 (Unix epoch). */ @Override public long getTimestamp() { diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/FixedTimestampStretegy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/FixedTimestampStretegy.java index 7b7e7a7..f8bbda0 100644 --- a/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/FixedTimestampStretegy.java +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/FixedTimestampStretegy.java @@ -26,9 +26,9 @@ package com.github.f4b6a3.ulid.strategy.timestamp; import com.github.f4b6a3.ulid.strategy.TimestampStrategy; -public class FixedTimestampStretegy implements TimestampStrategy { +public final class FixedTimestampStretegy implements TimestampStrategy { - protected long timestamp = 0; + private long timestamp = 0; public FixedTimestampStretegy(long timestamp) { this.timestamp = timestamp; diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java index 11d6f6a..91a59cb 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java @@ -26,10 +26,11 @@ package com.github.f4b6a3.ulid.util; import java.util.UUID; -import com.github.f4b6a3.util.Base32Util; -import com.github.f4b6a3.util.ByteUtil; +import com.github.f4b6a3.ulid.exception.InvalidUlidException; -public class UlidConverter { +import static com.github.f4b6a3.ulid.util.UlidUtil.*; + +public final class UlidConverter { private UlidConverter() { } @@ -39,8 +40,6 @@ public class UlidConverter { * * The returning string is encoded to Crockford's base32. * - * The timestamp and random components are encoded separated. - * * @param uuid a UUID * @return a ULID */ @@ -49,19 +48,20 @@ public class UlidConverter { final long msb = uuid.getMostSignificantBits(); final long lsb = uuid.getLeastSignificantBits(); - // Extract timestamp component - final long timeNumber = (msb >>> 16); - String timestampComponent = leftPad(Base32Util.toBase32Crockford(timeNumber)); + final long time = ((msb & 0xffffffffffff0000L) >>> 16); + final long random1 = ((msb & 0x000000000000ffffL) << 24) | ((lsb & 0xffffff0000000000L) >>> 40); + final long random2 = (lsb & 0x000000ffffffffffL); - // Extract randomness component - byte[] randBytes = new byte[10]; - randBytes[0] = (byte) (msb >>> 8); - randBytes[1] = (byte) (msb); - byte[] lsbBytes = ByteUtil.toBytes(lsb); - System.arraycopy(lsbBytes, 0, randBytes, 2, 8); - String randomnessComponent = Base32Util.toBase32Crockford(randBytes); + final char[] timeComponent = zerofill(toBase32Crockford(time), 10); + final char[] randomComponent1 = zerofill(toBase32Crockford(random1), 8); + final char[] randomComponent2 = zerofill(toBase32Crockford(random2), 8); - return timestampComponent + randomnessComponent; + char[] output = new char[ULID_CHAR_LENGTH]; + System.arraycopy(timeComponent, 0, output, 0, 10); + System.arraycopy(randomComponent1, 0, output, 10, 8); + System.arraycopy(randomComponent2, 0, output, 18, 8); + + return new String(output); } /** @@ -70,34 +70,33 @@ public class UlidConverter { * The input string must be encoded to Crockford's base32, following the ULID * specification. * - * The timestamp and random components are decoded separated. - * * An exception is thrown if the ULID string is invalid. * * @param ulid a ULID * @return a UUID if valid + * @throws InvalidUlidException if invalid */ public static UUID fromString(final String ulid) { UlidValidator.validate(ulid); + + final char[] input = ulid.toCharArray(); + final char[] timeComponent = new char[10]; + final char[] randomComponent1 = new char[8]; + final char[] randomComponent2 = new char[8]; + + System.arraycopy(input, 0, timeComponent, 0, 10); + System.arraycopy(input, 10, randomComponent1, 0, 8); + System.arraycopy(input, 18, randomComponent2, 0, 8); - // Extract timestamp component - final String timestampComponent = ulid.substring(0, 10); - final long timeNumber = Base32Util.fromBase32CrockfordAsLong(timestampComponent); + final long time = fromBase32Crockford(timeComponent); + final long random1 = fromBase32Crockford(randomComponent1); + final long random2 = fromBase32Crockford(randomComponent2); - // Extract randomness component - final String randomnessComponent = ulid.substring(10, 26); - byte[] randBytes = Base32Util.fromBase32Crockford(randomnessComponent); - byte[] lsbBytes = new byte[8]; - System.arraycopy(randBytes, 2, lsbBytes, 0, 8); - - final long msb = (timeNumber << 16) | ((randBytes[0] << 8) & 0x0000ff00L) | ((randBytes[1]) & 0x000000ffL); - final long lsb = ByteUtil.toNumber(lsbBytes); + final long msb = ((time & 0x0000ffffffffffffL) << 16) | ((random1 & 0x000000ffff000000L) >>> 24); + final long lsb = ((random1 & 0x0000000000ffffffL) << 40) | (random2 & 0x000000ffffffffffL); return new UUID(msb, lsb); } - private static String leftPad(String unpadded) { - return "0000000000".substring(unpadded.length()) + unpadded; - } } diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java index 0abe55e..71a3e06 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java @@ -26,9 +26,14 @@ package com.github.f4b6a3.ulid.util; import java.time.Instant; -import com.github.f4b6a3.util.Base32Util; +public final class UlidUtil { -public class UlidUtil { + protected static final int BASE_32 = 32; + + protected static final int ULID_CHAR_LENGTH = 26; + + // Include 'O'->ZERO, 'I'->ONE and 'L'->ONE + protected static final char[] ALPHABET_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZOIL".toCharArray(); private UlidUtil() { } @@ -50,11 +55,196 @@ public class UlidUtil { public static String extractRandomnessComponent(String ulid) { UlidValidator.validate(ulid); - return ulid.substring(10, 26); + return ulid.substring(10, ULID_CHAR_LENGTH); } protected static long extractUnixMilliseconds(String ulid) { - String milliseconds = ulid.substring(0, 10); - return Base32Util.fromBase32CrockfordAsLong(milliseconds); + return fromBase32Crockford(extractTimestampComponent(ulid).toCharArray()); + } + + /** + * Get a number from a given array of bytes. + * + * @param bytes a byte array + * @return a long + */ + public static long toNumber(final byte[] bytes) { + return toNumber(bytes, 0, bytes.length); + } + + public static long toNumber(final byte[] bytes, final int start, final int end) { + long result = 0; + for (int i = start; i < end; i++) { + result = (result << 8) | (bytes[i] & 0xff); + } + return result; + } + + /** + * Get an array of bytes from a given number. + * + * @param number a long value + * @return a byte array + */ + protected static byte[] toBytes(final long number) { + return new byte[] { (byte) (number >>> 56), (byte) (number >>> 48), (byte) (number >>> 40), + (byte) (number >>> 32), (byte) (number >>> 24), (byte) (number >>> 16), (byte) (number >>> 8), + (byte) (number) }; + } + + protected static char[] removeHyphens(final char[] input) { + + int count = 0; + char[] buffer = new char[input.length]; + + for (int i = 0; i < input.length; i++) { + if ((input[i] != '-')) { + buffer[count++] = input[i]; + } + } + + char[] output = new char[count]; + System.arraycopy(buffer, 0, output, 0, count); + + return output; + } + + public static char[] toBase32Crockford(long number) { + return encode(number, UlidUtil.ALPHABET_CROCKFORD); + } + + public static long fromBase32Crockford(char[] chars) { + return decode(chars, UlidUtil.ALPHABET_CROCKFORD); + } + + protected static boolean isCrockfordBase32(final char[] chars) { + char[] input = toUpperCase(chars); + for (int i = 0; i < input.length; i++) { + if (!isCrockfordBase32(input[i])) { + return false; + } + } + return true; + } + + protected static boolean isCrockfordBase32(char c) { + for (int j = 0; j < ALPHABET_CROCKFORD.length; j++) { + if (c == ALPHABET_CROCKFORD[j]) { + return true; + } + } + return false; + } + + protected static char[] zerofill(char[] chars, int length) { + return lpad(chars, length, '0'); + } + + protected static char[] lpad(char[] chars, int length, char fill) { + + int delta = 0; + int limit = 0; + + if (length > chars.length) { + delta = length - chars.length; + limit = length; + } else { + delta = 0; + limit = chars.length; + } + + char[] output = new char[chars.length + delta]; + for (int i = 0; i < limit; i++) { + if (i < delta) { + output[i] = fill; + } else { + output[i] = chars[i - delta]; + } + } + return output; + } + + protected static char[] transliterate(char[] chars, char[] alphabet1, char[] alphabet2) { + char[] output = chars.clone(); + for (int i = 0; i < output.length; i++) { + for (int j = 0; j < alphabet1.length; j++) { + if (output[i] == alphabet1[j]) { + output[i] = alphabet2[j]; + break; + } + } + } + return output; + } + + protected static char[] toUpperCase(final char[] chars) { + char[] output = new char[chars.length]; + for (int i = 0; i < output.length; i++) { + if (chars[i] >= 0x61 && chars[i] <= 0x7a) { + output[i] = (char) ((int) chars[i] & 0xffffffdf); + } else { + output[i] = chars[i]; + } + } + return output; + } + + /** + * Encode a long number to base 32 char array. + * + * @param number a long number + * @param alphabet an alphabet + * @return a base32 encoded char array + */ + protected static char[] encode(long number, char[] alphabet) { + + final int CHARS_MAX = 13; // 13 * 5 = 65 + + if (number < 0) { + throw new IllegalArgumentException(String.format("Number '%d' is not a positive integer.", number)); + } + + long n = number; + char[] buffer = new char[CHARS_MAX]; + char[] output; + + int count = CHARS_MAX; + while (n > 0) { + buffer[--count] = alphabet[(int) (n % BASE_32)]; + n = n / BASE_32; + } + + output = new char[buffer.length - count]; + System.arraycopy(buffer, count, output, 0, output.length); + + return output; + } + + /** + * Decode a base 32 char array to a long number. + * + * @param chars a base 32 encoded char array + * @param alphabet an alphabet + * @return a long number + */ + protected static long decode(char[] chars, char[] alphabet) { + + long n = 0; + + for (int i = 0; i < chars.length; i++) { + int d = chr(chars[i], alphabet); + n = BASE_32 * n + d; + } + + return n; + } + + private static int chr(char c, char[] alphabet) { + for (int i = 0; i < alphabet.length; i++) { + if (alphabet[i] == c) { + return (byte) i; + } + } + return (byte) '0'; } } diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java index 1d4d916..49f913b 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java @@ -26,11 +26,11 @@ package com.github.f4b6a3.ulid.util; import com.github.f4b6a3.ulid.exception.InvalidUlidException; -public class UlidValidator { +import static com.github.f4b6a3.ulid.util.UlidUtil.*; - protected static final String ULID_PATTERN = "^[0-9a-tv-zA-TV-Z]{26}$"; +public final class UlidValidator { - // Date: 10889-08-02T05:31:50.655Z + // Date: 10889-08-02T05:31:50.655Z (epoch time: 281474976710655) protected static final long TIMESTAMP_MAX = (long) Math.pow(2, 48) - 1; private UlidValidator() { @@ -42,14 +42,12 @@ public class UlidValidator { * A valid ULID string is a sequence of 26 characters from Crockford's base 32 * alphabet. * - * Dashes are ignored by this validator. - * *
 	 * Examples of valid ULID strings:
-	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU)
-	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, except uU)
-	 * - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU)
-	 * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, except uU, with dashes)
+	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except U)
+	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, including OIL, except U)
+	 * - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except U, with hyphens)
+	 * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, including OIL, except U, with hyphens)
 	 * 
* * @param ulid a ULID @@ -57,16 +55,20 @@ public class UlidValidator { */ public static boolean isValid(String ulid) { - if (ulid == null || ulid.isEmpty()) { + if (ulid == null) { return false; } - String u = ulid.replaceAll("-", ""); - if (!u.matches(ULID_PATTERN)) { + char[] chars = removeHyphens(ulid.toCharArray()); + if (chars.length != ULID_CHAR_LENGTH || !isCrockfordBase32(chars)) { return false; } - long timestamp = UlidUtil.extractUnixMilliseconds(ulid); + // Extract time component + final char[] timestampComponent = new char[10]; + System.arraycopy(chars, 0, timestampComponent, 0, 10); + final long timestamp = fromBase32Crockford(timestampComponent); + return timestamp >= 0 && timestamp <= TIMESTAMP_MAX; } diff --git a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java index c5cdfee..bc99110 100644 --- a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java +++ b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java @@ -3,7 +3,7 @@ package com.github.f4b6a3.ulid; import org.junit.runner.RunWith; import org.junit.runners.Suite; -import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreatorTest; +import com.github.f4b6a3.ulid.creator.UlidSpecCreatorTest; import com.github.f4b6a3.ulid.ulid.UlidCreatorTest; import com.github.f4b6a3.ulid.util.UlidConverterTest; import com.github.f4b6a3.ulid.util.UlidUtilTest; @@ -12,7 +12,7 @@ import com.github.f4b6a3.ulid.util.UlidValidatorTest; @RunWith(Suite.class) @Suite.SuiteClasses({ UlidCreatorTest.class, - UlidBasedGuidCreatorTest.class, + UlidSpecCreatorTest.class, UlidConverterTest.class, UlidUtilTest.class, UlidValidatorTest.class, diff --git a/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java index dbb33c4..0cfba13 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java @@ -4,7 +4,7 @@ import java.util.HashSet; import java.util.UUID; import com.github.f4b6a3.ulid.UlidCreator; -import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; +import com.github.f4b6a3.ulid.creator.UlidSpecCreator; import com.github.f4b6a3.ulid.exception.UlidCreatorException; import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; @@ -27,7 +27,7 @@ public class UniquenessTest { private boolean verbose; // Show progress or not // GUID creator based on ULID spec - private UlidBasedGuidCreator creator; + private UlidSpecCreator creator; /** * Initialize the test. @@ -36,7 +36,7 @@ public class UniquenessTest { * @param requestCount * @param creator */ - public UniquenessTest(int threadCount, int requestCount, UlidBasedGuidCreator creator, boolean progress) { + public UniquenessTest(int threadCount, int requestCount, UlidSpecCreator creator, boolean progress) { this.threadCount = threadCount; this.requestCount = requestCount; this.creator = creator; @@ -125,7 +125,7 @@ public class UniquenessTest { } public static void execute(boolean verbose, int threadCount, int requestCount) { - UlidBasedGuidCreator creator = UlidCreator.getUlidBasedCreator() + UlidSpecCreator creator = UlidCreator.getUlidSpecCreator() .withTimestampStrategy(new FixedTimestampStretegy(System.currentTimeMillis())); UniquenessTest test = new UniquenessTest(threadCount, requestCount, creator, verbose); diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorMock.java b/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorMock.java deleted file mode 100644 index 98587b5..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorMock.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.f4b6a3.ulid.creator; - -import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; - -class UlidBasedGuidCreatorMock extends UlidBasedGuidCreator { - - public UlidBasedGuidCreatorMock(long previousTimestamp) { - super(); - this.previousTimestamp = previousTimestamp; - } - - public UlidBasedGuidCreatorMock(long randomMsb, long randomLsb, long randomMsbMax, long randomLsbMax, long previousTimestamp) { - - this.randomMsb = randomMsb; - this.randomLsb = randomLsb; - - this.randomMsbMax = randomMsbMax; - this.randomLsbMax = randomLsbMax; - - this.previousTimestamp = previousTimestamp; - } - - public long getRandomMsb() { - return this.randomMsb; - } - - public long getRandomLsb() { - return this.randomLsb; - } - - public long getRandomHiMax() { - return this.randomMsb; - } - - public long getRandomLoMax() { - return this.randomLsb; - } -} \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorTest.java deleted file mode 100644 index 16fff10..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorTest.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.github.f4b6a3.ulid.creator; - -import java.util.Random; -import java.util.UUID; - -import org.junit.Test; - -import com.github.f4b6a3.util.random.Xorshift128PlusRandom; -import com.github.f4b6a3.ulid.exception.UlidCreatorException; -import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; - -import static org.junit.Assert.*; - -public class UlidBasedGuidCreatorTest { - - private static final long DEFAULT_LOOP_MAX = 1_000_000; - - private static final long TIMESTAMP = System.currentTimeMillis(); - - private static final Random RANDOM = new Xorshift128PlusRandom(); - - @Test - public void testRandomMostSignificantBits() { - - UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP); - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); - - UUID uuid = creator.create(); - long firstMsb = creator.extractRandomMsb(uuid); - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - uuid = creator.create(); - - } - - long lastMsb = creator.extractRandomMsb(uuid); - long expectedMsb = firstMsb; - assertEquals(String.format("The last MSB should be iqual to the first %s.", expectedMsb), expectedMsb, lastMsb); - - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); - uuid = creator.create(); - lastMsb = uuid.getMostSignificantBits(); - assertNotEquals("The last MSB should be random after timestamp changed.", firstMsb, lastMsb); - } - - @Test - public void testRandomLeastSignificantBits() { - - UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP); - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); - - UUID uuid = creator.create(); - long firstLsb = creator.extractRandomLsb(uuid); - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - uuid = creator.create(); - } - - long lastLsb = creator.extractRandomLsb(uuid); - long expected = firstLsb + DEFAULT_LOOP_MAX; - assertEquals(String.format("The last LSB should be iqual to %s.", expected), expected, lastLsb); - - long notExpected = firstLsb + DEFAULT_LOOP_MAX + 1; - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); - uuid = creator.create(); - lastLsb = uuid.getLeastSignificantBits(); - assertNotEquals("The last LSB should be random after timestamp changed.", notExpected, lastLsb); - } - - @Test - public void testIncrementOfRandomLeastSignificantBits() { - - UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP); - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); - - long lsb = creator.getRandomLsb(); - - UUID uuid = new UUID(0, 0); - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - uuid = creator.create(); - } - - long expectedLsb = lsb + DEFAULT_LOOP_MAX; - long randomLsb = creator.getRandomLsb(); - assertEquals("Wrong LSB after loop.", expectedLsb, randomLsb); - - randomLsb = creator.extractRandomLsb(uuid); - assertEquals("Wrong LSB after loop.", expectedLsb, randomLsb); - } - - @Test - public void testIncrementOfRandomMostSignificantBits() { - - UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP); - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); - - long msb = creator.getRandomMsb(); - - UUID uuid = new UUID(0, 0); - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - uuid = creator.create(); - } - - long expectedMsb = msb; - long randomMsb = creator.getRandomMsb(); - assertEquals("Wrong MSB after loop.", expectedMsb, randomMsb); - - randomMsb = creator.extractRandomMsb(uuid); - assertEquals("Wrong MSB after loop.", expectedMsb, randomMsb); - } - - @Test - public void testShouldThrowOverflowException1() { - - long msbMax = 0x000001ffffffffffL; - long lsbMax = 0x000001ffffffffffL; - - long msb = msbMax - 1; - long lsb = lsbMax - DEFAULT_LOOP_MAX; - - UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(msb, lsb, msbMax, lsbMax, TIMESTAMP); - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); - - for (int i = 0; i < DEFAULT_LOOP_MAX - 1; i++) { - creator.create(); - } - - try { - creator.create(); - fail("It should throw an overflow exception."); - } catch (UlidCreatorException e) { - // success - } - } - - @Test - public void testShouldThrowOverflowException2() { - - long msbMax = (RANDOM.nextLong() & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT) - | UlidBasedGuidCreatorMock.INCREMENT_MAX; - long lsbMax = (RANDOM.nextLong() & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT) - | UlidBasedGuidCreatorMock.INCREMENT_MAX; - - long msb = msbMax - 1; - long lsb = lsbMax - DEFAULT_LOOP_MAX; - - UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(msb, lsb, msbMax, lsbMax, TIMESTAMP); - creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); - - UUID uuid = new UUID(0, 0); - for (int i = 0; i < DEFAULT_LOOP_MAX - 1; i++) { - uuid = creator.create(); - } - - long expectedLsb = (lsbMax - 1) & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT; - long randomLsb = creator.extractRandomLsb(uuid); - assertEquals("Incorrect LSB after loop.", expectedLsb, randomLsb); - - long expectedMsb = (msbMax - 1) & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT; - long randomMsb = creator.extractRandomMsb(uuid); - assertEquals("Incorrect MSB after loop.", expectedMsb, randomMsb); - - try { - creator.create(); - fail("It should throw an overflow exception."); - } catch (UlidCreatorException e) { - // success - } - } -} diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorMock.java b/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorMock.java new file mode 100644 index 0000000..4cbbd41 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorMock.java @@ -0,0 +1,38 @@ +package com.github.f4b6a3.ulid.creator; + +import com.github.f4b6a3.ulid.creator.UlidSpecCreator; + +class UlidSpecCreatorMock extends UlidSpecCreator { + + public UlidSpecCreatorMock(long previousTimestamp) { + super(); + this.previousTimestamp = previousTimestamp; + } + + public UlidSpecCreatorMock(long random1, long random2, long randomMax1, long randomMax2, long previousTimestamp) { + + this.random1 = random1; + this.random2 = random2; + + this.randomMax1 = randomMax1; + this.randomMax2 = randomMax2; + + this.previousTimestamp = previousTimestamp; + } + + public long getRandom1() { + return this.random1; + } + + public long getRandom2() { + return this.random2; + } + + public long getRandomMax1() { + return this.random1; + } + + public long getRandomMax2() { + return this.random2; + } +} \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java new file mode 100644 index 0000000..d630bc6 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java @@ -0,0 +1,255 @@ +package com.github.f4b6a3.ulid.creator; + +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +import org.junit.Test; + +import com.github.f4b6a3.ulid.UlidCreator; +import com.github.f4b6a3.ulid.exception.UlidCreatorException; +import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; + +import static org.junit.Assert.*; + +public class UlidSpecCreatorTest { + + private static final int DEFAULT_LOOP_MAX = 1_000_000; + + private static final long TIMESTAMP = System.currentTimeMillis(); + + private static final Random RANDOM = new Random(); + + protected static final String DUPLICATE_UUID_MSG = "A duplicate ULID was created"; + + protected static final int THREAD_TOTAL = availableProcessors(); + + private static int availableProcessors() { + int processors = Runtime.getRuntime().availableProcessors(); + if (processors < 4) { + processors = 4; + } + return processors; + } + + @Test + public void testRandomMostSignificantBits() { + + UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = creator.create(); + long firstRand1 = creator.extractRandom1(uuid); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + uuid = creator.create(); + + } + + long lastRand1 = creator.extractRandom1(uuid); + long expected1 = firstRand1; + assertEquals(String.format("The last high random should be iqual to the first %s.", expected1), expected1, + lastRand1); + + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); + uuid = creator.create(); + lastRand1 = uuid.getMostSignificantBits(); + assertNotEquals("The last high random should be random after timestamp changed.", firstRand1, lastRand1); + } + + @Test + public void testRandomLeastSignificantBits() { + + UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = creator.create(); + long firstRnd2 = creator.extractRandom2(uuid); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + uuid = creator.create(); + } + + long lastRand2 = creator.extractRandom2(uuid); + long expected = firstRnd2 + DEFAULT_LOOP_MAX; + assertEquals(String.format("The last low random should be iqual to %s.", expected), expected, lastRand2); + + long notExpected = firstRnd2 + DEFAULT_LOOP_MAX + 1; + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); + uuid = creator.create(); + lastRand2 = uuid.getLeastSignificantBits(); + assertNotEquals("The last low random should be random after timestamp changed.", notExpected, lastRand2); + } + + @Test + public void testIncrementOfRandomLeastSignificantBits() { + + UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + long random2 = creator.getRandom2(); + + UUID uuid = new UUID(0, 0); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + uuid = creator.create(); + } + + long expected2 = random2 + DEFAULT_LOOP_MAX; + long rand2 = creator.getRandom2(); + assertEquals("Wrong low random after loop.", expected2, rand2); + + rand2 = creator.extractRandom2(uuid); + assertEquals("Wrong low random after loop.", expected2, rand2); + } + + @Test + public void testIncrementOfRandomMostSignificantBits() { + + UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + long random1 = creator.getRandom1(); + + UUID uuid = new UUID(0, 0); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + uuid = creator.create(); + } + + long expected1 = random1; + long rand1 = creator.getRandom1(); + assertEquals("Wrong high random after loop.", expected1, rand1); + + rand1 = creator.extractRandom1(uuid); + assertEquals("Wrong high random after loop.", expected1, rand1); + } + + @Test + public void testShouldThrowOverflowException1() { + + long random1 = 0x000000ffffffffffL; + long random2 = 0x000000ffffffffffL; + + long max1 = random1 | UlidSpecCreatorMock.INCREMENT_MAX; + long max2 = random2 | UlidSpecCreatorMock.INCREMENT_MAX; + + random1 = max1; + random2 = max2 - DEFAULT_LOOP_MAX; + + UlidSpecCreatorMock creator = new UlidSpecCreatorMock(random1, random2, max1, max2, TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = new UUID(0, 0); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + uuid = creator.create(); + } + + long hi1 = random1 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT; + long lo1 = random2 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT; + String concat1 = (Long.toHexString(hi1) + Long.toHexString(lo1)); + BigInteger bigint1 = new BigInteger(concat1, 16); + long hi2 = creator.extractRandom1(uuid); + long lo2 = creator.extractRandom2(uuid); + String concat2 = (Long.toHexString(hi2) + Long.toHexString(lo2)); + BigInteger bigint2 = new BigInteger(concat2, 16); + assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2); + + try { + uuid = creator.create(); + fail("It should throw an overflow exception."); + } catch (UlidCreatorException e) { + // success + } + } + + @Test + public void testShouldThrowOverflowException2() { + + long random1 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT); + long random2 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT); + + long max1 = random1 | UlidSpecCreatorMock.INCREMENT_MAX; + long max2 = random2 | UlidSpecCreatorMock.INCREMENT_MAX; + + random1 = max1; + random2 = max2 - DEFAULT_LOOP_MAX; + + UlidSpecCreatorMock creator = new UlidSpecCreatorMock(random1, random2, max1, max2, TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = new UUID(0, 0); + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + uuid = creator.create(); + } + + long rand1 = creator.extractRandom1(uuid); + long expected1 = (max1 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT); + assertEquals("Incorrect high random after loop.", expected1, rand1); + + long rand2 = creator.extractRandom2(uuid); + long expected2 = (max2 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT); + assertEquals("Incorrect low random after loop.", expected2, rand2); + + long hi1 = random1 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT; + long lo1 = random2 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT; + String concat1 = (Long.toHexString(hi1) + Long.toHexString(lo1)); + BigInteger bigint1 = new BigInteger(concat1, 16); + long hi2 = creator.extractRandom1(uuid); + long lo2 = creator.extractRandom2(uuid); + String concat2 = (Long.toHexString(hi2) + Long.toHexString(lo2)); + BigInteger bigint2 = new BigInteger(concat2, 16); + assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2); + + try { + creator.create(); + fail("It should throw an overflow exception."); + } catch (UlidCreatorException e) { + // success + } + } + + @Test + public void testGetUlidParallelGeneratorsShouldCreateUniqueUlids() throws InterruptedException { + + Thread[] threads = new Thread[THREAD_TOTAL]; + TestThread.clearHashSet(); + + // Instantiate and start many threads + for (int i = 0; i < THREAD_TOTAL; i++) { + threads[i] = new TestThread(UlidCreator.getUlidSpecCreator(), DEFAULT_LOOP_MAX); + threads[i].start(); + } + + // Wait all the threads to finish + for (Thread thread : threads) { + thread.join(); + } + + // Check if the quantity of unique UUIDs is correct + assertEquals(DUPLICATE_UUID_MSG, TestThread.hashSet.size(), (DEFAULT_LOOP_MAX * THREAD_TOTAL)); + } + + public static class TestThread extends Thread { + + public static Set hashSet = new HashSet<>(); + private UlidSpecCreator creator; + private int loopLimit; + + public TestThread(UlidSpecCreator creator, int loopLimit) { + this.creator = creator; + this.loopLimit = loopLimit; + } + + public static void clearHashSet() { + hashSet = new HashSet<>(); + } + + @Override + public void run() { + for (int i = 0; i < loopLimit; i++) { + synchronized (hashSet) { + hashSet.add(creator.create()); + } + } + } + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorTest.java index 0d732f5..dec8c16 100644 --- a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorTest.java @@ -1,37 +1,20 @@ package com.github.f4b6a3.ulid.ulid; -import org.junit.BeforeClass; import org.junit.Test; import com.github.f4b6a3.ulid.UlidCreator; -import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; import com.github.f4b6a3.ulid.util.UlidUtil; import com.github.f4b6a3.ulid.util.UlidValidator; import static org.junit.Assert.*; import java.util.Arrays; import java.util.HashSet; -import java.util.Set; -import java.util.UUID; public class UlidCreatorTest { - private static int processors; - private static final int ULID_LENGTH = 26; private static final int DEFAULT_LOOP_MAX = 100_000; - private static final String DUPLICATE_UUID_MSG = "A duplicate ULID was created"; - - @BeforeClass - public static void beforeClass() { - - processors = Runtime.getRuntime().availableProcessors(); - if (processors < 4) { - processors = 4; - } - } - @Test public void testGetUlid() { String[] list = new String[DEFAULT_LOOP_MAX]; @@ -52,9 +35,9 @@ public class UlidCreatorTest { private void checkNullOrInvalid(String[] list) { for (String ulid : list) { - assertTrue("ULID is null", ulid != null); + assertNotNull("ULID is null", ulid); assertTrue("ULID is empty", !ulid.isEmpty()); - assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); + assertEquals("ULID length is wrong", ULID_LENGTH, ulid.length()); assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); } } @@ -67,7 +50,7 @@ public class UlidCreatorTest { assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid)); } - assertTrue("There are duplicated ULIDs", set.size() == list.length); + assertEquals("There are duplicated ULIDs", set.size(), list.length); } private void checkCreationTime(String[] list, long startTime, long endTime) { @@ -87,53 +70,7 @@ public class UlidCreatorTest { Arrays.sort(other); for (int i = 0; i < list.length; i++) { - assertTrue("The ULID list is not ordered", list[i].equals(other[i])); - } - } - - @Test - public void testGetUlidBasedGuidParallelGeneratorsShouldCreateUniqueUuids() throws InterruptedException { - - Thread[] threads = new Thread[processors]; - TestThread.clearHashSet(); - - // Instantiate and start many threads - for (int i = 0; i < processors; i++) { - threads[i] = new TestThread(UlidCreator.getUlidBasedCreator(), DEFAULT_LOOP_MAX); - threads[i].start(); - } - - // Wait all the threads to finish - for (Thread thread : threads) { - thread.join(); - } - - // Check if the quantity of unique UUIDs is correct - assertTrue(DUPLICATE_UUID_MSG, TestThread.hashSet.size() == (DEFAULT_LOOP_MAX * processors)); - } - - private static class TestThread extends Thread { - - private static Set hashSet = new HashSet<>(); - private UlidBasedGuidCreator creator; - private int loopLimit; - - public TestThread(UlidBasedGuidCreator creator, int loopLimit) { - this.creator = creator; - this.loopLimit = loopLimit; - } - - public static void clearHashSet() { - hashSet = new HashSet<>(); - } - - @Override - public void run() { - for (int i = 0; i < loopLimit; i++) { - synchronized (hashSet) { - hashSet.add(creator.create()); - } - } + assertEquals("The ULID list is not ordered", list[i], other[i]); } } } diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java index 7199d76..2bfda7e 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java @@ -22,9 +22,9 @@ public class UlidConverterTest { UUID uuid1 = UlidCreator.getUlid(); String ulid = UlidConverter.toString(uuid1); - assertTrue("ULID is null", ulid != null); + assertNotNull("ULID is null", ulid); assertTrue("ULID is empty", !ulid.isEmpty()); - assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); + assertEquals("ULID length is wrong ", ULID_LENGTH, ulid.length()); assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); UUID uuid2 = UlidConverter.fromString(ulid); diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java index 45da9ff..195190e 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java @@ -5,10 +5,8 @@ import java.time.Instant; import org.junit.Test; -import com.github.f4b6a3.util.Base32Util; -import com.github.f4b6a3.util.ByteUtil; import com.github.f4b6a3.ulid.exception.InvalidUlidException; -import com.github.f4b6a3.ulid.util.UlidUtil; +import static com.github.f4b6a3.ulid.util.UlidUtil.*; public class UlidUtilTest { @@ -21,20 +19,43 @@ public class UlidUtilTest { private static final String[] EXAMPLE_DATES = { "1970-01-01T00:00:00.000Z", "1985-10-26T01:16:00.123Z", "2001-09-09T01:46:40.456Z", "2020-01-15T14:30:33.789Z", "2038-01-19T03:14:07.321Z" }; + private static final int[] NUMBERS = { 102685630, 725393777, 573697669, 614668535, 790665079, 728958755, 966150230, + 410015018, 605266173, 946077566, 214051168, 775737014, 723003700, 391609366, 147844737, 514081413, + 488279622, 550860813, 611087782, 223492126, 706308515, 158990768, 549042286, 26926303, 775714134, 602886016, + 27282100, 675097356, 641101167, 515280699, 454184468, 371424784, 633917378, 887459583, 792903202, 168552040, + 824806922, 696445335, 653338746, 357696553, 353677217, 972662902, 400738139, 537701151, 202077579, + 110209145, 356152341, 168702810, 684185451, 419840003, 480132486, 308833881, 997154252, 918202260, + 103304091, 328467776, 648729690, 733655121, 645189051, 342500864, 560919543, 509761384, 626871960, + 429248550, 319025067, 507317265, 348303729, 256009160, 660250872, 85224414, 414490625, 355994979, 318005886, + 326093128, 492813589, 569014099, 503350412, 168303553, 801566586, 800368918, 742601973, 395588591, + 257341245, 722366808, 501878988, 200718306, 184948029, 149469829, 992401543, 240364551, 976817281, + 161998068, 515579566, 275182272, 376045488, 899163436, 941443452, 974372015, 934795357, 958806784 }; + + private static final String[] NUMBERS_BASE_32_CROCKFORD = { "31XPXY", "NKS8BH", "H33VM5", "JA667Q", "QJ15VQ", + "NQ61S3", "WSCJ2P", "C70N9A", "J1787X", "W67ZVY", "6C4AB0", "Q3SKNP", "NHGA9M", "BNEZ0P", "4CZVM1", + "FA8GM5", "EHN3J6", "GDAY0D", "J6RXD6", "6N4E0Y", "N1JTD3", "4QM0DG", "GBKE3E", "SNQ6Z", "Q3RXAP", "HYYKW0", + "T0JNM", "M3TARC", "K3CVBF", "FBD3SV", "DH4KGM", "B26ZGG", "JWHKY2", "TEB3QZ", "QM5FH2", "50QSK8", "RJK3GA", + "MR5TCQ", "KF2A3T", "AN4119", "AH9BX1", "WZKA3P", "BY5HTV", "G0SARZ", "60PXCB", "393A3S", "AKMX0N", + "50WCTT", "MCFNVB", "CGCG03", "E9WFC6", "96GVJS", "XPYQEC", "VBN9WM", "32GJWV", "9S81A0", "KANN2T", + "NVNC2H", "K79KDV", "A6M9G0", "GPXWZQ", "F64NV8", "JNTKMR", "CSBM16", "9G7VXB", "F3T30H", "AC5CBH", + "7M4RY8", "KNN87R", "2H8TYY", "CB9801", "AKG3B3", "9F8RKY", "9PZJA8", "ENZF8N", "GYMXTK", "F0114C", + "50G6Y1", "QWDVVT", "QV9A8P", "P46D7N", "BS8CZF", "7NDDSX", "NGWWAR", "EYM46C", "5ZDDZ2", "5GC59X", + "4EHEM5", "XJDP47", "757B07", "X3J341", "4TFS7M", "FBP7NE", "86DWP0", "B6KZXG", "TSG99C", "W1TJBW", + "X17F5F", "VVFP2X", "WJCER0" }; + @Test(expected = InvalidUlidException.class) public void testExtractTimestamp() { String ulid = "0000000000" + EXAMPLE_RANDOMNESS; - long milliseconds = UlidUtil.extractTimestamp(ulid); + long milliseconds = extractTimestamp(ulid); assertEquals(0, milliseconds); ulid = "7ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; - milliseconds = UlidUtil.extractTimestamp(ulid); + milliseconds = extractTimestamp(ulid); assertEquals(TIMESTAMP_MAX, milliseconds); ulid = "8ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; - UlidUtil.extractTimestamp(ulid); - fail("Should throw exception: invalid ULID"); + extractTimestamp(ulid); } @Test @@ -43,12 +64,11 @@ public class UlidUtilTest { String randomnessComponent = EXAMPLE_RANDOMNESS; for (String i : EXAMPLE_DATES) { - long milliseconds = Instant.parse(i).toEpochMilli(); - String timestampComponent = leftPad(Base32Util.toBase32Crockford(milliseconds)); + String timestampComponent = new String(UlidUtil.zerofill(toBase32Crockford(milliseconds), 10)); String ulid = timestampComponent + randomnessComponent; - long result = UlidUtil.extractTimestamp(ulid); + long result = extractTimestamp(ulid); assertEquals(milliseconds, result); } @@ -65,11 +85,11 @@ public class UlidUtilTest { long milliseconds = Instant.parse(i).toEpochMilli(); byte[] bytes = new byte[6]; - System.arraycopy(ByteUtil.toBytes(milliseconds), 2, bytes, 0, 6); + System.arraycopy(toBytes(milliseconds), 2, bytes, 0, 6); - String timestampComponent = leftPad(Base32Util.toBase32Crockford(milliseconds)); + String timestampComponent = new String(UlidUtil.zerofill(toBase32Crockford(milliseconds), 10)); String ulid = timestampComponent + randomnessComponent; - Instant result = UlidUtil.extractInstant(ulid); + Instant result = extractInstant(ulid); assertEquals(instant, result); } @@ -79,7 +99,7 @@ public class UlidUtilTest { public void testExtractTimestampComponent() { String ulid = EXAMPLE_ULID; String expected = EXAMPLE_TIMESTAMP; - String result = UlidUtil.extractTimestampComponent(ulid); + String result = extractTimestampComponent(ulid); assertEquals(expected, result); } @@ -87,11 +107,157 @@ public class UlidUtilTest { public void testExtractRandomnessComponent() { String ulid = EXAMPLE_ULID; String expected = EXAMPLE_RANDOMNESS; - String result = UlidUtil.extractRandomnessComponent(ulid); + String result = extractRandomnessComponent(ulid); assertEquals(expected, result); } - private String leftPad(String unpadded) { - return "0000000000".substring(unpadded.length()) + unpadded; + @Test + public void testToUpperCase() { + String string = "Aq7zmxKxPc61QKiGRu8Y3PdYMer64lrRxfb9A5JAJuDeEhXSrbsxsaUoHrFzmEJUYBKJPgV+1rAd"; + char[] chars1 = string.toCharArray(); + char[] chars2 = UlidUtil.toUpperCase(chars1); + assertEquals(new String(string).toUpperCase(), new String(chars2)); + + string = "kL9zTzZfzlwKYCEmWKPxFYxINf6JZCSmSqykyG5ONWZcFkJG2WGc7gq71YCEzt2hYcsTvfQqEmn0"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.toUpperCase(chars1); + assertEquals(new String(string).toUpperCase(), new String(chars2)); + + string = "XXEOUV3jJb3f+wpRPDVke9NgWwEgdkzChnKnpZZWS/mCSqTi757GmmqYdzuDGOa5ftqHI3/zqKrS"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.toUpperCase(chars1); + assertEquals(new String(string).toUpperCase(), new String(chars2)); + + string = "t9LVRQZbCxTQgaxlajNE/VYpLpKiHtKt7jHrtxSDIJ2hrHaJI2UPF1zA7I35m9cKz01lHYD1IXlM"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.toUpperCase(chars1); + assertEquals(new String(string).toUpperCase(), new String(chars2)); + + string = "jyS52J42LLT6GY+Zywo1R4tQv4bTfAqpFB6aiKEuA3yDxFkuXzuKe8PaGlUTaXD5WgRFMnO9nRLU"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.toUpperCase(chars1); + assertEquals(new String(string).toUpperCase(), new String(chars2)); + } + + @Test + public void testZerofill() { + assertEquals("001", new String(UlidUtil.zerofill("1".toCharArray(), 3))); + assertEquals("000123", new String(UlidUtil.zerofill("123".toCharArray(), 6))); + assertEquals("0000000000", new String(UlidUtil.zerofill("".toCharArray(), 10))); + assertEquals("9876543210", new String(UlidUtil.zerofill("9876543210".toCharArray(), 10))); + assertEquals("0000000000123456", new String(UlidUtil.zerofill("123456".toCharArray(), 16))); + } + + @Test + public void testLpad() { + + String string = ""; + char[] chars1 = string.toCharArray(); + char[] chars2 = UlidUtil.lpad(chars1, 8, 'x'); + assertEquals("xxxxxxxx", new String(chars2)); + + string = ""; + chars1 = string.toCharArray(); + chars2 = UlidUtil.lpad(chars1, 12, 'W'); + assertEquals("WWWWWWWWWWWW", new String(chars2)); + + string = "TCgpYATMlK9BmSzX"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.lpad(chars1, 13, '0'); + assertEquals(string, new String(chars2)); + + string = "2kgy3m9U646L6TJ5"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.lpad(chars1, 16, '0'); + assertEquals(string, new String(chars2)); + + string = "2kgy3m9U646L6TJ5"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.lpad(chars1, 17, '0'); + assertEquals("0" + string, new String(chars2)); + + string = "LH6hfYcGJu06xSNF"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.lpad(chars1, 25, '0'); + assertEquals("000000000" + string, new String(chars2)); + + string = "t9LVRQZbCxTQgaxlajNE/VYpLpKiHtKt7jHrtxSDIJ2hrHaJI2UPF1zA7I35m9cKz01lHYD1IXlM"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.lpad(chars1, 80, '0'); + assertEquals("0000" + string, new String(chars2)); + } + + @Test + public void testRemoveHyphens() { + String string = "-ZGQ8yCsza-RFxlYyA-FaXa4wd-k4Owa-/ITDvqWOl4-Do3/NwW--Lawx6GcO-LmSRDsd3af3Zt-VMNnvLgIw9-"; + char[] chars1 = string.toCharArray(); + char[] chars2 = UlidUtil.removeHyphens(chars1); + assertEquals(string.replace("-", ""), new String(chars2)); + + string = "qi3q-EMvc1-Kk7XzYMj----SUnwf-lp0K7-Ucj-W-cDplP-2dG-3x+5y-r9JBc-ZT-0e--cRHoMbU/lBzZsJ6rcJ5zT/J"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.removeHyphens(chars1); + assertEquals(string.replace("-", ""), new String(chars2)); + + string = "RXMD0DJV---Zf3Jqcv39uGjzBuiLkLNL-IvPnTyfMteEet-I7u-Z8oyE+BIUBf/OPi30iICP1TnQpMve4j"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.removeHyphens(chars1); + assertEquals(string.replace("-", ""), new String(chars2)); + + string = "---dFH-b-ylQPA60-kuRxZ9-6q5MLd1-qLTKdma-rF2yEABt-t6mJg0U-ibIYcVnt-Guqdn-z-G-43Ob-W/-Gxah1+53a-"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.removeHyphens(chars1); + assertEquals(string.replace("-", ""), new String(chars2)); + + string = "-------------------------------"; + chars1 = string.toCharArray(); + chars2 = UlidUtil.removeHyphens(chars1); + assertEquals(string.replace("-", ""), new String(chars2)); + } + + @Test + public void testTransliterate() { + + char[] alphabetCrockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray(); + char[] alphabetDefault = "0123456789abcdefghijklmnopqrstuv".toCharArray(); + + char[] chars2 = UlidUtil.transliterate(alphabetCrockford, alphabetCrockford, alphabetDefault); + assertEquals(new String(alphabetDefault), new String(chars2)); + + chars2 = UlidUtil.transliterate(alphabetDefault, alphabetDefault, alphabetCrockford); + assertEquals(new String(alphabetCrockford), new String(chars2)); + } + + @Test + public void testIsBase32Crockford() { + assertTrue(UlidUtil.isCrockfordBase32("16JD".toCharArray())); + assertTrue(UlidUtil.isCrockfordBase32("01BX5ZZKBKACTAV9WEVGEMMVRY".toCharArray())); + assertTrue(UlidUtil.isCrockfordBase32(UlidUtil.ALPHABET_CROCKFORD)); + assertFalse(UlidUtil.isCrockfordBase32("U6JD".toCharArray())); + assertFalse(UlidUtil.isCrockfordBase32("*1BX5ZZKBKACTAV9WEVGEMMVRY".toCharArray())); + assertFalse(UlidUtil.isCrockfordBase32("u".toCharArray())); + assertFalse(UlidUtil.isCrockfordBase32("U".toCharArray())); + } + + @Test + public void testToBase32Crockford() { + assertEquals("7ZZZZZZZZZ", new String(UlidUtil.toBase32Crockford(281474976710655L))); + // Encode from long to base 32 + for (int i = 0; i < NUMBERS.length; i++) { + String result = new String(UlidUtil.toBase32Crockford(NUMBERS[i])); + assertEquals(NUMBERS_BASE_32_CROCKFORD[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32_CROCKFORD[i], result); + } + } + + @Test + public void testFromBase32Crockford() { + assertEquals(281474976710655L, UlidUtil.fromBase32Crockford("7ZZZZZZZZZ".toCharArray())); + // Decode from base 32 to long + long number = 0; + for (int i = 0; i < NUMBERS.length; i++) { + number = UlidUtil.fromBase32Crockford((NUMBERS_BASE_32_CROCKFORD[i]).toCharArray()); + assertEquals(NUMBERS[i], number); + } } } diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java index 936ab01..05e917c 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java @@ -9,7 +9,7 @@ import com.github.f4b6a3.ulid.util.UlidValidator; public class UlidValidatorTest { @Test - public void testIsValidStrict() { + public void testIsValid() { String ulid = null; // Null assertFalse("Null ULID should be invalid.", UlidValidator.isValid(ulid)); @@ -38,7 +38,7 @@ public class UlidValidatorTest { ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char assertFalse("ULID with special chars should be invalid.", UlidValidator.isValid(ulid)); - ulid = "01234-56789-ABCDEFGHJKMNPQRS"; // Hyphens + ulid = "01234-56789-ABCDEFGHJKM---NPQRS"; // Hyphens assertTrue("ULID with hiphens should be valid.", UlidValidator.isValid(ulid)); ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1