Preparing v1.1.0

This commit is contained in:
Fabio Lima 2020-04-18 02:43:18 -03:00
parent 1826fd960e
commit c59d1c61f0
21 changed files with 412 additions and 491 deletions

View File

@ -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`.
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>1.0.2</version>
<version>1.1.0</version>
</dependency>
```
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();
```

View File

@ -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();
}
}

View File

@ -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.
*/

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -22,7 +22,7 @@
* SOFTWARE.
*/
package com.github.f4b6a3.ulid.timestamp;
package com.github.f4b6a3.ulid.strategy;
public interface TimestampStrategy {
long getTimestamp();

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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.
*
* <pre>
* 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)
* </pre>
*
* @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);
}
}
}

View File

@ -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.
*
* <pre>
* 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)
* </pre>
*
* @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));
}
}
}

View File

@ -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,
})
/**

View File

@ -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;
/**
*

View File

@ -1,4 +1,4 @@
package com.github.f4b6a3;
package com.github.f4b6a3.ulid.bench;
// Add theese dependencies to pom.xml:
//

View File

@ -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.*;

View File

@ -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++) {

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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));
}
}