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 # ULID Creator
A Java library for generating and handling ULIDs - _Universally Unique Lexicographically Sortable Identifiers_. A Java library for generating ULIDs.
How to Use How to Use
------------------------------------------------------ ------------------------------------------------------
Create a ULID: Create a ULID as GUID:
```java ```java
String ulid = UlidCreator.getUlid(); UUID ulid = UlidCreator.getUlid();
``` ```
Create a ULID as GUID object: Create a ULID string:
```java ```java
UUID ulid = UlidCreator.getGuid(); String ulid = UlidCreator.getUlidString();
``` ```
### Maven dependency ### Maven dependency
Add these lines to your `pom.xml`. Add these lines to your `pom.xml`.
@ -27,7 +28,7 @@ Add these lines to your `pom.xml`.
<dependency> <dependency>
<groupId>com.github.f4b6a3</groupId> <groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId> <artifactId>ulid-creator</artifactId>
<version>1.0.2</version> <version>1.1.0</version>
</dependency> </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). 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 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. See the section on GUIDs to know how the 128 bits are generated in this library.
```java ```java
// ULIDs // ULIDs
String ulid = UlidCreator.getUlid(); String ulid = UlidCreator.getUlidString();
``` ```
Examples of ULIDs: Examples of ULIDs:
@ -71,17 +72,17 @@ Examples of ULIDs:
milli randomness 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. 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. 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 ```java
// GUID based on ULID spec // GUID based on ULID spec
UUID guid = UlidCreator.getGuid(); UUID ulid = UlidCreator.getUlid();
``` ```
Examples of GUIDs based on ULID spec: Examples of GUIDs based on ULID spec:
@ -109,38 +110,28 @@ Examples of GUIDs based on ULID spec:
millisecs randomness 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 ```java
// with fixed timestamp strategy (for test cases)
String ulid = UlidCreator.getGuidCreator()
.withTimestampStrategy(new FixedTimestampStretegy())
.createUlid();
// with your custom timestamp strategy // with your custom timestamp strategy
String ulid = UlidCreator.getGuidCreator() TimestampStrategy customStrategy = new CustomTimestampStrategy();
.withTimestampStrategy(new MyCustomTimestampStrategy()) String ulid = UlidCreator.getUlidBasedGuidCreator()
.createUlid(); .withTimestampStrategy(customStrategy)
.createString();
// with your custom random number generator // with `java.util.Random` number generator
String ulid = UlidCreator.getGuidCreator() Random random = new Random();
.withRandomGenerator(new MyCustomRandom()) String ulid = UlidCreator.getUlidBasedGuidCreator()
.createUlid();
// with fast random generator (Xorshift128Plus with salt)
int salt = (int) FingerprintUtil.getFingerprint();
Random random = new Xorshift128PlusRandom(salt);
String ulid = UlidCreator.getGuidCreator()
.withRandomGenerator(random) .withRandomGenerator(random)
.createUlid(); .createString();
// with fast random generator (the same as above) // with fast random generator (the same as above)
String ulid = UlidCreator.getGuidCreator() String ulid = UlidCreator.getUlidBasedGuidCreator()
.withFastRandomGenerator() .withFastRandomGenerator()
.createUlid(); .createString();
``` ```

View File

@ -26,8 +26,8 @@ package com.github.f4b6a3.ulid;
import java.util.UUID; import java.util.UUID;
import com.github.f4b6a3.commons.random.Xorshift128PlusRandom;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator;
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
/** /**
* A factory for Universally Unique Lexicographically Sortable Identifiers. * 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() { 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 * The returning string is encoded to Crockford's base32.
*/ *
public static UUID getFastUlid() { * The random component is generated by a secure random number generator:
return FastGuidCreatorLazyHolder.INSTANCE.create(); * {@link java.security.SecureRandom}.
}
/**
* Returns a ULID.
* *
* @return a ULID * @return a ULID
*/ */
public static String getUlidString() { 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 * @return a ULID
*/ */
public static String getFastUlidString() { public static String getFastUlidString() {
return FastGuidCreatorLazyHolder.INSTANCE.createString(); return FastUlidBasedGuidCreatorHolder.INSTANCE.createString();
} }
/** /**
* Return a GUID creator for direct use. * 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} * @return a {@link UlidBasedGuidCreator}
*/ */
public static UlidBasedGuidCreator getUlidBasedCreator() { public static UlidBasedGuidCreator getUlidBasedCreator() {
return new UlidBasedGuidCreator(); return new UlidBasedGuidCreator();
} }
private static class GuidCreatorLazyHolder { private static class UlidBasedGuidCreatorHolder {
static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator(); static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator();
} }
private static class FastGuidCreatorLazyHolder { private static class FastUlidBasedGuidCreatorHolder {
static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator().withFastRandomGenerator(); 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.Random;
import java.util.UUID; import java.util.UUID;
import com.github.f4b6a3.ulid.timestamp.TimestampStrategy; import com.github.f4b6a3.ulid.util.UlidConverter;
import com.github.f4b6a3.ulid.util.UlidUtil;
import com.github.f4b6a3.commons.random.Xorshift128PlusRandom; import com.github.f4b6a3.commons.random.Xorshift128PlusRandom;
import com.github.f4b6a3.commons.util.FingerprintUtil; import com.github.f4b6a3.commons.util.FingerprintUtil;
import com.github.f4b6a3.commons.util.RandomUtil; import com.github.f4b6a3.commons.util.RandomUtil;
import com.github.f4b6a3.ulid.exception.UlidCreatorException; 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 * 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. * 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 * 1. A part of 48 bits that represent the amount of milliseconds since Unix
* Epoch, 1 January 1970. * Epoch, 1 January 1970.
@ -85,6 +85,9 @@ public class UlidBasedGuidCreator {
* *
* The maximum GUIDs that can be generated per millisecond is 2^80. * 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 * ### Specification of Universally Unique Lexicographically Sortable ID
* *
* #### Components * #### Components
@ -116,7 +119,7 @@ public class UlidBasedGuidCreator {
* 2^80 ULIDs within the same millisecond, or cause the random component to * 2^80 ULIDs within the same millisecond, or cause the random component to
* overflow with less, the generation will fail. * 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 * @throws UlidCreatorException an overrun exception if too many requests are
* made within the same millisecond. * 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 * @return a ULID string
*/ */
public synchronized String createString() { public synchronized String createString() {
UUID guid = create(); return UlidConverter.toString(create());
return UlidUtil.fromUuidToUlid(guid);
} }
/** /**
@ -185,7 +189,8 @@ public class UlidBasedGuidCreator {
/** /**
* Increment the random part of the GUID. * 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. * @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 { public class UlidCreatorException extends RuntimeException {
private static final long serialVersionUID = 6755381080404981234L; private static final long serialVersionUID = 1L;
public UlidCreatorException(String message) { public UlidCreatorException(String message) {
super(message); super(message);
} }

View File

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

View File

@ -22,7 +22,9 @@
* SOFTWARE. * 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 { public class DefaultTimestampStrategy implements TimestampStrategy {

View File

@ -22,7 +22,9 @@
* SOFTWARE. * 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 { 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; package com.github.f4b6a3.ulid.util;
import java.time.Instant; import java.time.Instant;
import java.util.UUID;
import com.github.f4b6a3.commons.util.Base32Util; import com.github.f4b6a3.commons.util.Base32Util;
import com.github.f4b6a3.commons.util.ByteUtil;
public class UlidUtil { 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() { 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) { public static long extractTimestamp(String ulid) {
UlidUtil.validate(ulid); UlidValidator.validate(ulid);
return extractUnixMilliseconds(ulid); return extractUnixMilliseconds(ulid);
} }
@ -265,12 +44,12 @@ public class UlidUtil {
} }
public static String extractTimestampComponent(String ulid) { public static String extractTimestampComponent(String ulid) {
UlidUtil.validate(ulid); UlidValidator.validate(ulid);
return ulid.substring(0, 10); return ulid.substring(0, 10);
} }
public static String extractRandomnessComponent(String ulid) { public static String extractRandomnessComponent(String ulid) {
UlidUtil.validate(ulid); UlidValidator.validate(ulid);
return ulid.substring(10, 26); return ulid.substring(10, 26);
} }
@ -278,16 +57,4 @@ public class UlidUtil {
String milliseconds = ulid.substring(0, 10); String milliseconds = ulid.substring(0, 10);
return Base32Util.fromBase32CrockfordAsLong(milliseconds); 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.runner.RunWith;
import org.junit.runners.Suite; import org.junit.runners.Suite;
import com.github.f4b6a3.ulid.UlidCreatorTest;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreatorTest; 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.UlidUtilTest;
import com.github.f4b6a3.ulid.util.UlidValidatorTest;
@RunWith(Suite.class) @RunWith(Suite.class)
@Suite.SuiteClasses({ @Suite.SuiteClasses({
DefaultTimestampStrategyTest.class,
UlidBasedGuidCreatorTest.class,
UlidUtilTest.class,
UlidCreatorTest.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.HashSet;
import java.util.UUID; import java.util.UUID;
@ -6,7 +6,7 @@ import java.util.UUID;
import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.UlidCreator;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator;
import com.github.f4b6a3.ulid.exception.UlidCreatorException; 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: // 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.commons.random.Xorshift128PlusRandom;
import com.github.f4b6a3.ulid.exception.UlidCreatorException; 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.*; 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; import com.github.f4b6a3.ulid.UlidCreator;
@ -10,7 +10,7 @@ public class DemoTest {
int max = 100; int max = 100;
System.out.println(HORIZONTAL_LINE); System.out.println(HORIZONTAL_LINE);
System.out.println("### ULID"); System.out.println("### ULID string");
System.out.println(HORIZONTAL_LINE); System.out.println(HORIZONTAL_LINE);
for (int i = 0; i < max; i++) { for (int i = 0; i < max; i++) {
@ -18,7 +18,7 @@ public class DemoTest {
} }
System.out.println(HORIZONTAL_LINE); System.out.println(HORIZONTAL_LINE);
System.out.println("### GUID"); System.out.println("### ULID-based GUID");
System.out.println(HORIZONTAL_LINE); System.out.println(HORIZONTAL_LINE);
for (int i = 0; i < max; i++) { 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.BeforeClass;
import org.junit.Test; import org.junit.Test;
@ -6,6 +6,7 @@ import org.junit.Test;
import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.UlidCreator;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator;
import com.github.f4b6a3.ulid.util.UlidUtil; import com.github.f4b6a3.ulid.util.UlidUtil;
import com.github.f4b6a3.ulid.util.UlidValidator;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.util.Arrays; import java.util.Arrays;
@ -54,7 +55,7 @@ public class UlidCreatorTest {
assertTrue("ULID is null", ulid != null); assertTrue("ULID is null", ulid != null);
assertTrue("ULID is empty", !ulid.isEmpty()); assertTrue("ULID is empty", !ulid.isEmpty());
assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); 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 static org.junit.Assert.*;
import java.time.Instant; import java.time.Instant;
import java.util.UUID;
import org.junit.Test; 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.Base32Util;
import com.github.f4b6a3.commons.util.ByteUtil; 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 { public class UlidUtilTest {
@ -19,13 +18,10 @@ public class UlidUtilTest {
private static final long TIMESTAMP_MAX = 281474976710655l; // 2^48 - 1 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", 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" }; "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() { public void testExtractTimestamp() {
String ulid = "0000000000" + EXAMPLE_RANDOMNESS; String ulid = "0000000000" + EXAMPLE_RANDOMNESS;
@ -95,155 +91,6 @@ public class UlidUtilTest {
assertEquals(expected, result); 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) { private String leftPad(String unpadded) {
return "0000000000".substring(unpadded.length()) + 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));
}
}