From c59d1c61f0bd3bc64563b6c84cfb5d2dc47e7b3a Mon Sep 17 00:00:00 2001 From: Fabio Lima Date: Sat, 18 Apr 2020 02:43:18 -0300 Subject: [PATCH] Preparing v1.1.0 --- README.md | 59 ++--- .../com/github/f4b6a3/ulid/UlidCreator.java | 58 +++-- .../ulid/creator/UlidBasedGuidCreator.java | 23 +- .../ulid/exception/InvalidUlidException.java | 34 +++ .../ulid/exception/UlidCreatorException.java | 4 +- .../TimestampStrategy.java | 2 +- .../timestamp/DefaultTimestampStrategy.java | 4 +- .../timestamp/FixedTimestampStretegy.java | 4 +- .../f4b6a3/ulid/util/UlidConverter.java | 103 ++++++++ .../com/github/f4b6a3/ulid/util/UlidUtil.java | 239 +----------------- .../f4b6a3/ulid/util/UlidValidator.java | 87 +++++++ .../github/f4b6a3/{ => ulid}/TestSuite.java | 14 +- .../f4b6a3/{ => ulid}/UniquenessTest.java | 4 +- .../f4b6a3/{ => ulid/bench}/Benchmarks.java | 2 +- .../creator/UlidBasedGuidCreatorTest.java | 2 +- .../f4b6a3/{ => ulid}/demo/DemoTest.java | 6 +- .../DefaultTimestampStrategyTest.java | 12 - .../ulid/{ => ulid}/UlidCreatorTest.java | 5 +- .../f4b6a3/ulid/util/UlidConverterTest.java | 35 +++ .../github/f4b6a3/ulid/util/UlidUtilTest.java | 159 +----------- .../f4b6a3/ulid/util/UlidValidatorTest.java | 47 ++++ 21 files changed, 412 insertions(+), 491 deletions(-) create mode 100644 src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java rename src/main/java/com/github/f4b6a3/ulid/{timestamp => strategy}/TimestampStrategy.java (96%) rename src/main/java/com/github/f4b6a3/ulid/{ => strategy}/timestamp/DefaultTimestampStrategy.java (92%) rename src/main/java/com/github/f4b6a3/ulid/{ => strategy}/timestamp/FixedTimestampStretegy.java (92%) create mode 100644 src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java create mode 100644 src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java rename src/test/java/com/github/f4b6a3/{ => ulid}/TestSuite.java (63%) rename src/test/java/com/github/f4b6a3/{ => ulid}/UniquenessTest.java (97%) rename src/test/java/com/github/f4b6a3/{ => ulid/bench}/Benchmarks.java (98%) rename src/test/java/com/github/f4b6a3/{ => ulid}/demo/DemoTest.java (83%) delete mode 100644 src/test/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategyTest.java rename src/test/java/com/github/f4b6a3/ulid/{ => ulid}/UlidCreatorTest.java (95%) create mode 100644 src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java create mode 100644 src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java diff --git a/README.md b/README.md index d9f51d3..1c7bd26 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,24 @@ # ULID Creator -A Java library for generating and handling ULIDs - _Universally Unique Lexicographically Sortable Identifiers_. +A Java library for generating ULIDs. How to Use ------------------------------------------------------ -Create a ULID: +Create a ULID as GUID: ```java -String ulid = UlidCreator.getUlid(); +UUID ulid = UlidCreator.getUlid(); ``` -Create a ULID as GUID object: +Create a ULID string: ```java -UUID ulid = UlidCreator.getGuid(); +String ulid = UlidCreator.getUlidString(); ``` + ### Maven dependency Add these lines to your `pom.xml`. @@ -27,7 +28,7 @@ Add these lines to your `pom.xml`. com.github.f4b6a3 ulid-creator - 1.0.2 + 1.1.0 ``` See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator) and [mvnrepository.com](https://mvnrepository.com/artifact/com.github.f4b6a3/ulid-creator). @@ -35,15 +36,15 @@ See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b Implementation ------------------------------------------------------ -### ULID +### ULID string -The ULID is a unique and sortable 26 char sequence. See the [ULID specification](https://github.com/ulid/spec) for more information. +The ULID is a 26 char sequence. 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 // ULIDs -String ulid = UlidCreator.getUlid(); +String ulid = UlidCreator.getUlidString(); ``` Examples of ULIDs: @@ -71,17 +72,17 @@ Examples of ULIDs: milli randomness ``` -### GUID +### Ulid-based 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 `SecureRandom`, but it's possible to use any RNG that extends `Random`. +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 guid = UlidCreator.getGuid(); +UUID ulid = UlidCreator.getUlid(); ``` Examples of GUIDs based on ULID spec: @@ -109,38 +110,28 @@ Examples of GUIDs based on ULID spec: millisecs randomness ``` -#### How use the `GuidCreator` directly +#### How use the `UlidBasedGuidCreator` directly -These are some examples of using the `GuidCreator` to create ULIDs: +These are some examples of using the `UlidBasedGuidCreator` to create ULIDs strings: ```java - -// with fixed timestamp strategy (for test cases) -String ulid = UlidCreator.getGuidCreator() - .withTimestampStrategy(new FixedTimestampStretegy()) - .createUlid(); // with your custom timestamp strategy -String ulid = UlidCreator.getGuidCreator() - .withTimestampStrategy(new MyCustomTimestampStrategy()) - .createUlid(); +TimestampStrategy customStrategy = new CustomTimestampStrategy(); +String ulid = UlidCreator.getUlidBasedGuidCreator() + .withTimestampStrategy(customStrategy) + .createString(); -// with your custom random number generator -String ulid = UlidCreator.getGuidCreator() - .withRandomGenerator(new MyCustomRandom()) - .createUlid(); - -// with fast random generator (Xorshift128Plus with salt) -int salt = (int) FingerprintUtil.getFingerprint(); -Random random = new Xorshift128PlusRandom(salt); -String ulid = UlidCreator.getGuidCreator() +// with `java.util.Random` number generator +Random random = new Random(); +String ulid = UlidCreator.getUlidBasedGuidCreator() .withRandomGenerator(random) - .createUlid(); + .createString(); // with fast random generator (the same as above) -String ulid = UlidCreator.getGuidCreator() +String ulid = UlidCreator.getUlidBasedGuidCreator() .withFastRandomGenerator() - .createUlid(); + .createString(); ``` diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java index 7e08321..2f4934a 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -26,8 +26,8 @@ package com.github.f4b6a3.ulid; import java.util.UUID; +import com.github.f4b6a3.commons.random.Xorshift128PlusRandom; import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; -import com.github.f4b6a3.ulid.exception.UlidCreatorException; /** * A factory for Universally Unique Lexicographically Sortable Identifiers. @@ -40,61 +40,71 @@ public class UlidCreator { } /** - * Returns ULID as GUID object. + * Returns a ULID as GUID. * - * @return a GUID + * The random component is generated by a secure random number generator: + * {@link java.security.SecureRandom}. + * + * @return a UUID */ public static UUID getUlid() { - return GuidCreatorLazyHolder.INSTANCE.create(); + return UlidBasedGuidCreatorHolder.INSTANCE.create(); } /** - * Returns fast ULID as GUID object. + * Returns a ULID string. * - * @return a GUID - */ - public static UUID getFastUlid() { - return FastGuidCreatorLazyHolder.INSTANCE.create(); - } - - /** - * Returns a ULID. + * The returning string is encoded to Crockford's base32. + * + * The random component is generated by a secure random number generator: + * {@link java.security.SecureRandom}. * * @return a ULID */ public static String getUlidString() { - return GuidCreatorLazyHolder.INSTANCE.createString(); + return UlidBasedGuidCreatorHolder.INSTANCE.createString(); } /** - * Returns a fast ULID. + * 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 FastGuidCreatorLazyHolder.INSTANCE.createString(); + return FastUlidBasedGuidCreatorHolder.INSTANCE.createString(); } /** * Return a GUID creator for direct use. * - * This library uses the {@link UlidBasedGuidCreator} internally to generate - * ULIDs. - * - * The {@link UlidBasedGuidCreator} throws a {@link UlidCreatorException} when - * too many values are requested in the same millisecond. - * * @return a {@link UlidBasedGuidCreator} */ public static UlidBasedGuidCreator getUlidBasedCreator() { return new UlidBasedGuidCreator(); } - private static class GuidCreatorLazyHolder { + private static class UlidBasedGuidCreatorHolder { static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator(); } - private static class FastGuidCreatorLazyHolder { + private static class FastUlidBasedGuidCreatorHolder { static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator().withFastRandomGenerator(); } } diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreator.java index febe3e1..5a001dd 100644 --- a/src/main/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreator.java @@ -27,13 +27,13 @@ package com.github.f4b6a3.ulid.creator; import java.util.Random; import java.util.UUID; -import com.github.f4b6a3.ulid.timestamp.TimestampStrategy; -import com.github.f4b6a3.ulid.util.UlidUtil; +import com.github.f4b6a3.ulid.util.UlidConverter; import com.github.f4b6a3.commons.random.Xorshift128PlusRandom; import com.github.f4b6a3.commons.util.FingerprintUtil; import com.github.f4b6a3.commons.util.RandomUtil; import com.github.f4b6a3.ulid.exception.UlidCreatorException; -import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategy; +import com.github.f4b6a3.ulid.strategy.TimestampStrategy; +import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy; /** * Factory that creates lexicographically sortable GUIDs, based on the ULID @@ -69,7 +69,7 @@ public class UlidBasedGuidCreator { * * Return a GUID based on the ULID specification. * - * It has two parts: + * A ULID has two parts: * * 1. A part of 48 bits that represent the amount of milliseconds since Unix * Epoch, 1 January 1970. @@ -85,6 +85,9 @@ public class UlidBasedGuidCreator { * * The maximum GUIDs that can be generated per millisecond is 2^80. * + * The random part is generated by a secure random number generator: + * {@link java.security.SecureRandom}. + * * ### Specification of Universally Unique Lexicographically Sortable ID * * #### Components @@ -116,7 +119,7 @@ public class UlidBasedGuidCreator { * 2^80 ULIDs within the same millisecond, or cause the random component to * overflow with less, the generation will fail. * - * @return {@link UUID} a UUID value + * @return {@link UUID} a GUID value * * @throws UlidCreatorException an overrun exception if too many requests are * made within the same millisecond. @@ -135,13 +138,14 @@ public class UlidBasedGuidCreator { } /** - * Return a ULID. + * Returns a ULID string. + * + * The returning string is encoded to Crockford's base32. * * @return a ULID string */ public synchronized String createString() { - UUID guid = create(); - return UlidUtil.fromUuidToUlid(guid); + return UlidConverter.toString(create()); } /** @@ -185,7 +189,8 @@ public class UlidBasedGuidCreator { /** * Increment the random part of the GUID. * - * An exception is thrown when more than 2^80 increment operations are made. + * An exception is thrown when more than 2^80 increment operations are made, + * although it's extremely unlikely to occur. * * @throws UlidCreatorException if an overrun happens. */ diff --git a/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java b/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java new file mode 100644 index 0000000..7ff70a2 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java @@ -0,0 +1,34 @@ +/* + * 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.exception; + +public class InvalidUlidException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public InvalidUlidException(String message) { + super(message); + } +} 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 8740403..e6f2078 100644 --- a/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java +++ b/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java @@ -26,8 +26,8 @@ package com.github.f4b6a3.ulid.exception; public class UlidCreatorException extends RuntimeException { - private static final long serialVersionUID = 6755381080404981234L; - + private static final long serialVersionUID = 1L; + public UlidCreatorException(String message) { super(message); } diff --git a/src/main/java/com/github/f4b6a3/ulid/timestamp/TimestampStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/TimestampStrategy.java similarity index 96% rename from src/main/java/com/github/f4b6a3/ulid/timestamp/TimestampStrategy.java rename to src/main/java/com/github/f4b6a3/ulid/strategy/TimestampStrategy.java index 57f42f9..e6640b5 100644 --- a/src/main/java/com/github/f4b6a3/ulid/timestamp/TimestampStrategy.java +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/TimestampStrategy.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.timestamp; +package com.github.f4b6a3.ulid.strategy; public interface TimestampStrategy { long getTimestamp(); diff --git a/src/main/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/DefaultTimestampStrategy.java similarity index 92% rename from src/main/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategy.java rename to src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/DefaultTimestampStrategy.java index d802ec0..2c9e514 100644 --- a/src/main/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategy.java +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/DefaultTimestampStrategy.java @@ -22,7 +22,9 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.timestamp; +package com.github.f4b6a3.ulid.strategy.timestamp; + +import com.github.f4b6a3.ulid.strategy.TimestampStrategy; public class DefaultTimestampStrategy implements TimestampStrategy { diff --git a/src/main/java/com/github/f4b6a3/ulid/timestamp/FixedTimestampStretegy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/FixedTimestampStretegy.java similarity index 92% rename from src/main/java/com/github/f4b6a3/ulid/timestamp/FixedTimestampStretegy.java rename to src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/FixedTimestampStretegy.java index 0d568f6..7b7e7a7 100644 --- a/src/main/java/com/github/f4b6a3/ulid/timestamp/FixedTimestampStretegy.java +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/timestamp/FixedTimestampStretegy.java @@ -22,7 +22,9 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.timestamp; +package com.github.f4b6a3.ulid.strategy.timestamp; + +import com.github.f4b6a3.ulid.strategy.TimestampStrategy; public class FixedTimestampStretegy implements TimestampStrategy { diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java new file mode 100644 index 0000000..fecc005 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java @@ -0,0 +1,103 @@ +/* + * 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.util; + +import java.util.UUID; + +import com.github.f4b6a3.commons.util.Base32Util; +import com.github.f4b6a3.commons.util.ByteUtil; + +public class UlidConverter { + + private UlidConverter() { + } + + /** + * Convert a UUID to ULID string + * + * The returning string is encoded to Crockford's base32. + * + * The timestamp and random components are encoded separated. + * + * @param uuid a UUID + * @return a ULID + */ + public static String toString(UUID uuid) { + + final long msb = uuid.getMostSignificantBits(); + final long lsb = uuid.getLeastSignificantBits(); + + // Extract timestamp component + final long timeNumber = (msb >>> 16); + String timestampComponent = leftPad(Base32Util.toBase32Crockford(timeNumber)); + + // 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); + + return timestampComponent + randomnessComponent; + } + + /** + * Converts a ULID string to a UUID. + * + * 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 + */ + public static UUID fromString(final String ulid) { + + UlidValidator.validate(ulid); + + // Extract timestamp component + final String timestampComponent = ulid.substring(0, 10); + final long timeNumber = Base32Util.fromBase32CrockfordAsLong(timestampComponent); + + // 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); + + 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 c40091d..57658eb 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java @@ -25,237 +25,16 @@ package com.github.f4b6a3.ulid.util; import java.time.Instant; -import java.util.UUID; import com.github.f4b6a3.commons.util.Base32Util; -import com.github.f4b6a3.commons.util.ByteUtil; public class UlidUtil { - // Date: 10889-08-02T05:31:50.655Z - protected static final long TIMESTAMP_MAX = (long) Math.pow(2, 48) - 1; - - protected static final String ULID_PATTERN_STRICT = "^[0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{26}$"; - protected static final String ULID_PATTERN_LOOSE = "^[0-9a-tv-zA-TV-Z]{26}$"; - private UlidUtil() { } - /** - * Convert a UUID to ULID string - * - * @param uuid - * a UUID - * @return a ULID - */ - public static String fromUuidToUlid(UUID uuid) { - - final long msb = uuid.getMostSignificantBits(); - final long lsb = uuid.getLeastSignificantBits(); - - // Extract timestamp component - final long timeNumber = (msb >>> 16); - String timestampComponent = leftPad(Base32Util.toBase32Crockford(timeNumber)); - - // 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); - - return timestampComponent + randomnessComponent; - } - - /** - * Converts a ULID string to a UUID. - * - * An exception is thrown if the ULID string is invalid. - * - * @param ulid - * a ULID - * @return a UUID if valid - */ - public static UUID fromUlidToUuid(final String ulid) { - - UlidUtil.validate(ulid); - - // Extract timestamp component - final String timestampComponent = ulid.substring(0, 10); - final long timeNumber = Base32Util.fromBase32CrockfordAsLong(timestampComponent); - - // 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); - - return new UUID(msb, lsb); - } - - /** - * Get the array of bytes from a UUID. - * - * @param uuid - * a UUID - * @return an array of bytes - */ - public static byte[] fromUuidToBytes(final UUID uuid) { - final long msb = uuid.getMostSignificantBits(); - final long lsb = uuid.getLeastSignificantBits(); - final byte[] msbBytes = ByteUtil.toBytes(msb); - final byte[] lsbBytes = ByteUtil.toBytes(lsb); - return ByteUtil.concat(msbBytes, lsbBytes); - } - - /** - * Get a UUID from an array of bytes; - * - * @param bytes - * an array of bytes - * @return a UUID - */ - public static UUID fromBytesToUuid(byte[] bytes) { - byte[] msbBytes = new byte[8]; - System.arraycopy(bytes, 0, msbBytes, 0, 8); - byte[] lsbBytes = new byte[8]; - System.arraycopy(bytes, 8, lsbBytes, 0, 8); - final long msb = ByteUtil.toNumber(msbBytes); - final long lsb = ByteUtil.toNumber(lsbBytes); - return new UUID(msb, lsb); - } - - /** - * Convert an array of bytes to a ULID string. - * - * @param bytes - * a byte array - * @return a ULID string - */ - public static String fromBytesToUlid(byte[] bytes) { - - byte[] timeBytes = new byte[6]; - System.arraycopy(bytes, 0, timeBytes, 0, 6); - final long timeNumber = ByteUtil.toNumber(timeBytes); - final String timestampComponent = leftPad(Base32Util.toBase32Crockford(timeNumber)); - - byte[] randBytes = new byte[10]; - System.arraycopy(bytes, 6, randBytes, 0, 10); - final String randomnessComponent = Base32Util.toBase32Crockford(randBytes); - - return timestampComponent + randomnessComponent; - } - - /** - * Convert a ULID string to an array of bytes. - * - * @param ulid - * a ULID string - * @return an array of bytes - */ - public static byte[] fromUlidToBytes(final String ulid) { - UlidUtil.validate(ulid); - byte[] bytes = new byte[16]; - - final String timestampComponent = ulid.substring(0, 10); - final long timeNumber = Base32Util.fromBase32CrockfordAsLong(timestampComponent); - byte[] timeBytes = ByteUtil.toBytes(timeNumber); - System.arraycopy(timeBytes, 2, bytes, 0, 6); - - final String randomnessComponent = ulid.substring(10, 26); - byte[] randBytes = Base32Util.fromBase32Crockford(randomnessComponent); - System.arraycopy(randBytes, 0, bytes, 6, 10); - - return bytes; - } - - /** - * Checks if the ULID string is a valid. - * - * The validation mode is not strict. - * - * See {@link UlidUtil#validate(String, boolean)}. - * - * @param ulid - * a ULID - */ - protected static void validate(String ulid) { - validate(ulid, false); - } - - /** - * Checks if the ULID string is a valid. - * - * See {@link UlidUtil#validate(String, boolean)}. - * - * @param ulid - * a ULID - */ - protected static void validate(String ulid, boolean strict) { - if (!isValid(ulid, strict)) { - throw new UlidUtilException(String.format("Invalid ULID: %s.", ulid)); - } - } - - /** - * Checks if the string is a valid ULID. - * - * The validation mode is not strict. - * - * See {@link UlidUtil#validate(String, boolean)}. - */ - public static boolean isValid(String ulid) { - return isValid(ulid, false); - } - - /** - * Checks if the string is a valid ULID. - * - *
-	 * Strict validation: checks if the string is in the ULID specification format:
-	 * 
-	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU)
-	 * 
-	 * Loose validation: checks if the string is in one of these formats:
-	 *
-	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, except uU)
-	 * 
- * - * @param ulid - * a ULID - * @param strict - * true for strict validation, false for loose validation - * @return boolean true if valid - */ - public static boolean isValid(String ulid, boolean strict) { - - if (ulid == null || ulid.isEmpty()) { - return false; - } - - boolean matches = false; - - if (strict) { - matches = ulid.matches(ULID_PATTERN_STRICT); - } else { - String u = ulid.replaceAll("-", ""); - matches = u.matches(ULID_PATTERN_LOOSE); - } - - if (!matches) { - return false; - } - - long timestamp = extractUnixMilliseconds(ulid); - return timestamp >= 0 && timestamp <= TIMESTAMP_MAX; - } - public static long extractTimestamp(String ulid) { - UlidUtil.validate(ulid); + UlidValidator.validate(ulid); return extractUnixMilliseconds(ulid); } @@ -265,12 +44,12 @@ public class UlidUtil { } public static String extractTimestampComponent(String ulid) { - UlidUtil.validate(ulid); + UlidValidator.validate(ulid); return ulid.substring(0, 10); } public static String extractRandomnessComponent(String ulid) { - UlidUtil.validate(ulid); + UlidValidator.validate(ulid); return ulid.substring(10, 26); } @@ -278,16 +57,4 @@ public class UlidUtil { String milliseconds = ulid.substring(0, 10); return Base32Util.fromBase32CrockfordAsLong(milliseconds); } - - private static String leftPad(String unpadded) { - return "0000000000".substring(unpadded.length()) + unpadded; - } - - public static class UlidUtilException extends RuntimeException { - private static final long serialVersionUID = 1L; - - public UlidUtilException(String message) { - super(message); - } - } } diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java new file mode 100644 index 0000000..1d4d916 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java @@ -0,0 +1,87 @@ +/* + * 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.util; + +import com.github.f4b6a3.ulid.exception.InvalidUlidException; + +public class UlidValidator { + + protected static final String ULID_PATTERN = "^[0-9a-tv-zA-TV-Z]{26}$"; + + // Date: 10889-08-02T05:31:50.655Z + protected static final long TIMESTAMP_MAX = (long) Math.pow(2, 48) - 1; + + private UlidValidator() { + } + + /** + * Checks if the string is a valid ULID. + * + * 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)
+	 * 
+ * + * @param ulid a ULID + * @return boolean true if valid + */ + public static boolean isValid(String ulid) { + + if (ulid == null || ulid.isEmpty()) { + return false; + } + + String u = ulid.replaceAll("-", ""); + if (!u.matches(ULID_PATTERN)) { + return false; + } + + long timestamp = UlidUtil.extractUnixMilliseconds(ulid); + return timestamp >= 0 && timestamp <= TIMESTAMP_MAX; + + } + + /** + * Checks if the ULID string is a valid. + * + * See {@link TsidValidator#isValid(String)}. + * + * @param ulid a ULID string + * @throws InvalidUlidException if invalid + */ + protected static void validate(String ulid) { + if (!isValid(ulid)) { + throw new InvalidUlidException(String.format("Invalid ULID: %s.", ulid)); + } + } +} diff --git a/src/test/java/com/github/f4b6a3/TestSuite.java b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java similarity index 63% rename from src/test/java/com/github/f4b6a3/TestSuite.java rename to src/test/java/com/github/f4b6a3/ulid/TestSuite.java index 1b8b62f..c5cdfee 100644 --- a/src/test/java/com/github/f4b6a3/TestSuite.java +++ b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java @@ -1,19 +1,21 @@ -package com.github.f4b6a3; +package com.github.f4b6a3.ulid; import org.junit.runner.RunWith; import org.junit.runners.Suite; -import com.github.f4b6a3.ulid.UlidCreatorTest; import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreatorTest; -import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategyTest; +import com.github.f4b6a3.ulid.ulid.UlidCreatorTest; +import com.github.f4b6a3.ulid.util.UlidConverterTest; import com.github.f4b6a3.ulid.util.UlidUtilTest; +import com.github.f4b6a3.ulid.util.UlidValidatorTest; @RunWith(Suite.class) @Suite.SuiteClasses({ - DefaultTimestampStrategyTest.class, - UlidBasedGuidCreatorTest.class, - UlidUtilTest.class, UlidCreatorTest.class, + UlidBasedGuidCreatorTest.class, + UlidConverterTest.class, + UlidUtilTest.class, + UlidValidatorTest.class, }) /** diff --git a/src/test/java/com/github/f4b6a3/UniquenessTest.java b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java similarity index 97% rename from src/test/java/com/github/f4b6a3/UniquenessTest.java rename to src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java index ea00a26..dbb33c4 100644 --- a/src/test/java/com/github/f4b6a3/UniquenessTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java @@ -1,4 +1,4 @@ -package com.github.f4b6a3; +package com.github.f4b6a3.ulid; import java.util.HashSet; import java.util.UUID; @@ -6,7 +6,7 @@ import java.util.UUID; import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; import com.github.f4b6a3.ulid.exception.UlidCreatorException; -import com.github.f4b6a3.ulid.timestamp.FixedTimestampStretegy; +import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; /** * diff --git a/src/test/java/com/github/f4b6a3/Benchmarks.java b/src/test/java/com/github/f4b6a3/ulid/bench/Benchmarks.java similarity index 98% rename from src/test/java/com/github/f4b6a3/Benchmarks.java rename to src/test/java/com/github/f4b6a3/ulid/bench/Benchmarks.java index 2eddf2e..7bbd0f1 100644 --- a/src/test/java/com/github/f4b6a3/Benchmarks.java +++ b/src/test/java/com/github/f4b6a3/ulid/bench/Benchmarks.java @@ -1,4 +1,4 @@ -package com.github.f4b6a3; +package com.github.f4b6a3.ulid.bench; // Add theese dependencies to pom.xml: // diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorTest.java index 9068805..af6794f 100644 --- a/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/creator/UlidBasedGuidCreatorTest.java @@ -7,7 +7,7 @@ import org.junit.Test; import com.github.f4b6a3.commons.random.Xorshift128PlusRandom; import com.github.f4b6a3.ulid.exception.UlidCreatorException; -import com.github.f4b6a3.ulid.timestamp.FixedTimestampStretegy; +import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; import static org.junit.Assert.*; diff --git a/src/test/java/com/github/f4b6a3/demo/DemoTest.java b/src/test/java/com/github/f4b6a3/ulid/demo/DemoTest.java similarity index 83% rename from src/test/java/com/github/f4b6a3/demo/DemoTest.java rename to src/test/java/com/github/f4b6a3/ulid/demo/DemoTest.java index c46baca..ed951e2 100644 --- a/src/test/java/com/github/f4b6a3/demo/DemoTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/demo/DemoTest.java @@ -1,4 +1,4 @@ -package com.github.f4b6a3.demo; +package com.github.f4b6a3.ulid.demo; import com.github.f4b6a3.ulid.UlidCreator; @@ -10,7 +10,7 @@ public class DemoTest { int max = 100; System.out.println(HORIZONTAL_LINE); - System.out.println("### ULID"); + System.out.println("### ULID string"); System.out.println(HORIZONTAL_LINE); for (int i = 0; i < max; i++) { @@ -18,7 +18,7 @@ public class DemoTest { } System.out.println(HORIZONTAL_LINE); - System.out.println("### GUID"); + System.out.println("### ULID-based GUID"); System.out.println(HORIZONTAL_LINE); for (int i = 0; i < max; i++) { diff --git a/src/test/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategyTest.java b/src/test/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategyTest.java deleted file mode 100644 index 5b8d251..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategyTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.f4b6a3.ulid.timestamp; - -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -public class DefaultTimestampStrategyTest { - @Test - public void testVoid() { - assertTrue("void test", true); - } -} diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorTest.java similarity index 95% rename from src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java rename to src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorTest.java index c16389d..0d732f5 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorTest.java @@ -1,4 +1,4 @@ -package com.github.f4b6a3.ulid; +package com.github.f4b6a3.ulid.ulid; import org.junit.BeforeClass; import org.junit.Test; @@ -6,6 +6,7 @@ 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; @@ -54,7 +55,7 @@ public class UlidCreatorTest { assertTrue("ULID is null", ulid != null); assertTrue("ULID is empty", !ulid.isEmpty()); assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); - assertTrue("ULID is not valid", UlidUtil.isValid(ulid, /* strict */ true)); + assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); } } diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java new file mode 100644 index 0000000..7199d76 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java @@ -0,0 +1,35 @@ +package com.github.f4b6a3.ulid.util; + +import static org.junit.Assert.*; +import java.util.UUID; + +import org.junit.Test; + +import com.github.f4b6a3.ulid.UlidCreator; +import com.github.f4b6a3.ulid.util.UlidConverter; +import com.github.f4b6a3.ulid.util.UlidValidator; + +public class UlidConverterTest { + + private static final int ULID_LENGTH = 26; + private static final int DEFAULT_LOOP_MAX = 100_000; + + @Test + public void testToAndFromUlid() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + UUID uuid1 = UlidCreator.getUlid(); + String ulid = UlidConverter.toString(uuid1); + + assertTrue("ULID is null", ulid != null); + assertTrue("ULID is empty", !ulid.isEmpty()); + assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); + assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); + + UUID uuid2 = UlidConverter.fromString(ulid); + assertEquals("Result ULID is different from original ULID", uuid1, uuid2); + + } + } +} 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 e41b7a1..173af03 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java @@ -2,14 +2,13 @@ package com.github.f4b6a3.ulid.util; import static org.junit.Assert.*; import java.time.Instant; -import java.util.UUID; import org.junit.Test; -import com.github.f4b6a3.ulid.util.UlidUtil.UlidUtilException; import com.github.f4b6a3.commons.util.Base32Util; import com.github.f4b6a3.commons.util.ByteUtil; -import com.github.f4b6a3.ulid.UlidCreator; +import com.github.f4b6a3.ulid.exception.InvalidUlidException; +import com.github.f4b6a3.ulid.util.UlidUtil; public class UlidUtilTest { @@ -19,13 +18,10 @@ public class UlidUtilTest { private static final long TIMESTAMP_MAX = 281474976710655l; // 2^48 - 1 - private static final int ULID_LENGTH = 26; - private static final int DEFAULT_LOOP_MAX = 100_000; - 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" }; - @Test(expected = UlidUtilException.class) + @Test(expected = InvalidUlidException.class) public void testExtractTimestamp() { String ulid = "0000000000" + EXAMPLE_RANDOMNESS; @@ -95,155 +91,6 @@ public class UlidUtilTest { assertEquals(expected, result); } - @Test - public void testIsValidLoose() { - - String ulid = null; // Null - assertFalse("Null ULID should be invalid.", UlidUtil.isValid(ulid)); - - ulid = ""; // length: 0 - assertFalse("ULID with empty string should be invalid.", UlidUtil.isValid(ulid)); - - ulid = EXAMPLE_ULID; // All upper case - assertTrue("Ulid in upper case should valid.", UlidUtil.isValid(ulid)); - - ulid = "0123456789abcdefghjklmnpqr"; // All lower case - assertTrue("ULID in lower case should be valid.", UlidUtil.isValid(ulid)); - - ulid = "0123456789AbCdEfGhJkMnPqRs"; // Mixed case - assertTrue("Ulid in upper and lower case should valid.", UlidUtil.isValid(ulid)); - - ulid = "0123456789ABCDEFGHJKLMNPQ"; // length: 25 - assertFalse("ULID length lower than 26 should be invalid.", UlidUtil.isValid(ulid)); - - ulid = "0123456789ABCDEFGHJKMNPQZZZ"; // length: 27 - assertFalse("ULID length greater than 26 should be invalid.", UlidUtil.isValid(ulid)); - - ulid = "u123456789ABCDEFGHJKMNPQRS"; // Letter u - assertFalse("ULID with 'u' or 'U' should be invalid.", UlidUtil.isValid(ulid)); - - ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char - assertFalse("ULID with special chars should be invalid.", UlidUtil.isValid(ulid)); - - ulid = "01234-56789-ABCDEFGHJKMNPQRS"; // Hiphens - assertTrue("ULID with hiphens should be valid.", UlidUtil.isValid(ulid)); - - ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1 - assertFalse("ULID with timestamp greater than (2^48)-1 should be invalid.", UlidUtil.isValid(ulid)); - } - - @Test - public void testIsValidStrict() { - boolean strict = true; - - String ulid = null; // Null - assertFalse("Null ULID should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = ""; // length: 0 - assertFalse("ULID with empty string should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = EXAMPLE_ULID; // All upper case - assertTrue("ULID in upper case should valid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "0123456789abcdefghjkmnpqrs"; // All lower case - assertTrue("ULID in lower case should be valid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "0123456789AbCdEfGhJkMnPqRs"; // Mixed case - assertTrue("ULID in upper and lower case should valid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "0123456789ABCDEFGHJKLMNPQ"; // length: 25 - assertFalse("ULID length lower than 26 should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "0123456789ABCDEFGHJKMNPQZZZ"; // length: 27 - assertFalse("ULID length greater than 26 should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "i123456789ABCDEFGHJKMNPQRS"; // Letter i - assertFalse("ULID with 'i' or 'I' should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "L123456789ABCDEFGHJKMNPQRS"; // letter L - assertFalse("ULID with 'l' or 'L' should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "o123456789ABCDEFGHJKMNPQRS"; // letter o - assertFalse("ULID with 'o' or 'O' should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "u123456789ABCDEFGHJKMNPQRS"; // letter u - assertFalse("ULID with 'u' or 'U' should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char - assertFalse("ULID with special chars should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "01234-56789-ABCDEFGHJKMNPQRS"; // Hyphens - assertFalse("ULID with hiphens should be invalid in strict mode.", UlidUtil.isValid(ulid, strict)); - - ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1 - assertFalse("ULID with timestamp greater than (2^48)-1 should be invalid.", UlidUtil.isValid(ulid)); - } - - @Test - public void testToAndFromUlid() { - - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - - UUID uuid1 = UlidCreator.getUlid(); - String ulid = UlidUtil.fromUuidToUlid(uuid1); - - assertTrue("ULID is null", ulid != null); - assertTrue("ULID is empty", !ulid.isEmpty()); - assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); - assertTrue("ULID is not valid", UlidUtil.isValid(ulid, /* strict */ - true)); - - UUID uuid2 = UlidUtil.fromUlidToUuid(ulid); - assertEquals("Result ULID is different from original ULID", uuid1, uuid2); - - } - } - - @Test - public void testToAndFromBytes() { - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - String ulid1 = UlidCreator.getUlidString(); - byte[] bytes = UlidUtil.fromUlidToBytes(ulid1); - String ulid2 = UlidUtil.fromBytesToUlid(bytes); - - // Check ULID 1 - assertTrue(ulid1 != null); - assertTrue(!ulid1.isEmpty()); - assertTrue(ulid1.length() == ULID_LENGTH); - assertTrue(UlidUtil.isValid(ulid1, /* strict */ true)); - - // Check ULID 2 - assertTrue(ulid2 != null); - assertTrue(!ulid2.isEmpty()); - assertTrue(ulid2.length() == ULID_LENGTH); - assertTrue(UlidUtil.isValid(ulid2, /* strict */ true)); - - assertEquals(ulid1, ulid2); - } - } - - @Test - public void testFromUuidToBytes() { - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - UUID uuid1 = UlidCreator.getUlid(); - byte[] bytes = UlidUtil.fromUuidToBytes(uuid1); - long msb = ByteUtil.toNumber(ByteUtil.copy(bytes, 0, 8)); - long lsb = ByteUtil.toNumber(ByteUtil.copy(bytes, 8, 16)); - UUID uuid2 = new UUID(msb, lsb); - assertEquals(uuid1, uuid2); - } - } - - @Test - public void testFromBytesToUuid() { - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - UUID uuid1 = UlidCreator.getUlid(); - byte[] bytes = UlidUtil.fromUuidToBytes(uuid1); - UUID uuid2 = UlidUtil.fromBytesToUuid(bytes); - assertEquals(uuid1, uuid2); - } - } - private String leftPad(String unpadded) { return "0000000000".substring(unpadded.length()) + unpadded; } diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java new file mode 100644 index 0000000..936ab01 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java @@ -0,0 +1,47 @@ +package com.github.f4b6a3.ulid.util; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import com.github.f4b6a3.ulid.util.UlidValidator; + +public class UlidValidatorTest { + + @Test + public void testIsValidStrict() { + + String ulid = null; // Null + assertFalse("Null ULID should be invalid.", UlidValidator.isValid(ulid)); + + ulid = ""; // length: 0 + assertFalse("ULID with empty string should be invalid .", UlidValidator.isValid(ulid)); + + ulid = "0123456789ABCDEFGHJKMNPQRS"; // All upper case + assertTrue("ULID in upper case should valid.", UlidValidator.isValid(ulid)); + + ulid = "0123456789abcdefghjklmnpqr"; // All lower case + assertTrue("ULID in lower case should be valid.", UlidValidator.isValid(ulid)); + + ulid = "0123456789AbCdEfGhJkMnPqRs"; // Mixed case + assertTrue("Ulid in upper and lower case should valid.", UlidValidator.isValid(ulid)); + + ulid = "0123456789ABCDEFGHJKLMNPQ"; // length: 25 + assertFalse("ULID length lower than 26 should be invalid.", UlidValidator.isValid(ulid)); + + ulid = "0123456789ABCDEFGHJKMNPQZZZ"; // length: 27 + assertFalse("ULID length greater than 26 should be invalid.", UlidValidator.isValid(ulid)); + + ulid = "u123456789ABCDEFGHJKMNPQRS"; // Letter u + assertFalse("ULID with 'u' or 'U' should be invalid.", UlidValidator.isValid(ulid)); + + ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char + assertFalse("ULID with special chars should be invalid.", UlidValidator.isValid(ulid)); + + ulid = "01234-56789-ABCDEFGHJKMNPQRS"; // Hyphens + assertTrue("ULID with hiphens should be valid.", UlidValidator.isValid(ulid)); + + ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1 + assertFalse("ULID with timestamp greater than (2^48)-1 should be invalid.", UlidValidator.isValid(ulid)); + } +}