diff --git a/src/main/java/com/github/f4b6a3/ulid/Ulid.java b/src/main/java/com/github/f4b6a3/ulid/Ulid.java index a21c505..d811e93 100644 --- a/src/main/java/com/github/f4b6a3/ulid/Ulid.java +++ b/src/main/java/com/github/f4b6a3/ulid/Ulid.java @@ -28,8 +28,6 @@ import java.io.Serializable; import java.time.Instant; import java.util.UUID; -import com.github.f4b6a3.ulid.util.UlidValidator; - /** * This class represents a ULID. */ @@ -48,81 +46,89 @@ public final class Ulid implements Serializable, Comparable { public static final int TIME_BYTES_LENGTH = 6; public static final int RANDOM_BYTES_LENGTH = 10; - protected static final char[] ENCODING_CHARS = // + // 0xffffffffffffffffL + 1 = 0x0000000000000000L + private static final long INCREMENT_OVERFLOW = 0x0000000000000000L; + + protected static final char[] ALPHABET_UPPERCASE = // { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', // 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' }; - protected static final long[] ENCODING_VALUES = new long[128]; + protected static final char[] ALPHABET_LOWERCASE = // + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', // + 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' }; + + protected static final long[] ALPHABET_VALUES = new long[128]; static { - for (int i = 0; i < ENCODING_VALUES.length; i++) { - ENCODING_VALUES[i] = -1; + for (int i = 0; i < ALPHABET_VALUES.length; i++) { + ALPHABET_VALUES[i] = -1; } // Numbers - ENCODING_VALUES['0'] = 0x00; - ENCODING_VALUES['1'] = 0x01; - ENCODING_VALUES['2'] = 0x02; - ENCODING_VALUES['3'] = 0x03; - ENCODING_VALUES['4'] = 0x04; - ENCODING_VALUES['5'] = 0x05; - ENCODING_VALUES['6'] = 0x06; - ENCODING_VALUES['7'] = 0x07; - ENCODING_VALUES['8'] = 0x08; - ENCODING_VALUES['9'] = 0x09; + ALPHABET_VALUES['0'] = 0x00; + ALPHABET_VALUES['1'] = 0x01; + ALPHABET_VALUES['2'] = 0x02; + ALPHABET_VALUES['3'] = 0x03; + ALPHABET_VALUES['4'] = 0x04; + ALPHABET_VALUES['5'] = 0x05; + ALPHABET_VALUES['6'] = 0x06; + ALPHABET_VALUES['7'] = 0x07; + ALPHABET_VALUES['8'] = 0x08; + ALPHABET_VALUES['9'] = 0x09; // Lower case - ENCODING_VALUES['a'] = 0x0a; - ENCODING_VALUES['b'] = 0x0b; - ENCODING_VALUES['c'] = 0x0c; - ENCODING_VALUES['d'] = 0x0d; - ENCODING_VALUES['e'] = 0x0e; - ENCODING_VALUES['f'] = 0x0f; - ENCODING_VALUES['g'] = 0x10; - ENCODING_VALUES['h'] = 0x11; - ENCODING_VALUES['j'] = 0x12; - ENCODING_VALUES['k'] = 0x13; - ENCODING_VALUES['m'] = 0x14; - ENCODING_VALUES['n'] = 0x15; - ENCODING_VALUES['p'] = 0x16; - ENCODING_VALUES['q'] = 0x17; - ENCODING_VALUES['r'] = 0x18; - ENCODING_VALUES['s'] = 0x19; - ENCODING_VALUES['t'] = 0x1a; - ENCODING_VALUES['v'] = 0x1b; - ENCODING_VALUES['w'] = 0x1c; - ENCODING_VALUES['x'] = 0x1d; - ENCODING_VALUES['y'] = 0x1e; - ENCODING_VALUES['z'] = 0x1f; + ALPHABET_VALUES['a'] = 0x0a; + ALPHABET_VALUES['b'] = 0x0b; + ALPHABET_VALUES['c'] = 0x0c; + ALPHABET_VALUES['d'] = 0x0d; + ALPHABET_VALUES['e'] = 0x0e; + ALPHABET_VALUES['f'] = 0x0f; + ALPHABET_VALUES['g'] = 0x10; + ALPHABET_VALUES['h'] = 0x11; + ALPHABET_VALUES['j'] = 0x12; + ALPHABET_VALUES['k'] = 0x13; + ALPHABET_VALUES['m'] = 0x14; + ALPHABET_VALUES['n'] = 0x15; + ALPHABET_VALUES['p'] = 0x16; + ALPHABET_VALUES['q'] = 0x17; + ALPHABET_VALUES['r'] = 0x18; + ALPHABET_VALUES['s'] = 0x19; + ALPHABET_VALUES['t'] = 0x1a; + ALPHABET_VALUES['v'] = 0x1b; + ALPHABET_VALUES['w'] = 0x1c; + ALPHABET_VALUES['x'] = 0x1d; + ALPHABET_VALUES['y'] = 0x1e; + ALPHABET_VALUES['z'] = 0x1f; // Lower case OIL - ENCODING_VALUES['o'] = 0x00; - ENCODING_VALUES['i'] = 0x01; - ENCODING_VALUES['l'] = 0x01; + ALPHABET_VALUES['o'] = 0x00; + ALPHABET_VALUES['i'] = 0x01; + ALPHABET_VALUES['l'] = 0x01; // Upper case - ENCODING_VALUES['A'] = 0x0a; - ENCODING_VALUES['B'] = 0x0b; - ENCODING_VALUES['C'] = 0x0c; - ENCODING_VALUES['D'] = 0x0d; - ENCODING_VALUES['E'] = 0x0e; - ENCODING_VALUES['F'] = 0x0f; - ENCODING_VALUES['G'] = 0x10; - ENCODING_VALUES['H'] = 0x11; - ENCODING_VALUES['J'] = 0x12; - ENCODING_VALUES['K'] = 0x13; - ENCODING_VALUES['M'] = 0x14; - ENCODING_VALUES['N'] = 0x15; - ENCODING_VALUES['P'] = 0x16; - ENCODING_VALUES['Q'] = 0x17; - ENCODING_VALUES['R'] = 0x18; - ENCODING_VALUES['S'] = 0x19; - ENCODING_VALUES['T'] = 0x1a; - ENCODING_VALUES['V'] = 0x1b; - ENCODING_VALUES['W'] = 0x1c; - ENCODING_VALUES['X'] = 0x1d; - ENCODING_VALUES['Y'] = 0x1e; - ENCODING_VALUES['Z'] = 0x1f; + ALPHABET_VALUES['A'] = 0x0a; + ALPHABET_VALUES['B'] = 0x0b; + ALPHABET_VALUES['C'] = 0x0c; + ALPHABET_VALUES['D'] = 0x0d; + ALPHABET_VALUES['E'] = 0x0e; + ALPHABET_VALUES['F'] = 0x0f; + ALPHABET_VALUES['G'] = 0x10; + ALPHABET_VALUES['H'] = 0x11; + ALPHABET_VALUES['J'] = 0x12; + ALPHABET_VALUES['K'] = 0x13; + ALPHABET_VALUES['M'] = 0x14; + ALPHABET_VALUES['N'] = 0x15; + ALPHABET_VALUES['P'] = 0x16; + ALPHABET_VALUES['Q'] = 0x17; + ALPHABET_VALUES['R'] = 0x18; + ALPHABET_VALUES['S'] = 0x19; + ALPHABET_VALUES['T'] = 0x1a; + ALPHABET_VALUES['V'] = 0x1b; + ALPHABET_VALUES['W'] = 0x1c; + ALPHABET_VALUES['X'] = 0x1d; + ALPHABET_VALUES['Y'] = 0x1e; + ALPHABET_VALUES['Z'] = 0x1f; // Upper case OIL - ENCODING_VALUES['O'] = 0x00; - ENCODING_VALUES['I'] = 0x01; - ENCODING_VALUES['L'] = 0x01; + ALPHABET_VALUES['O'] = 0x00; + ALPHABET_VALUES['I'] = 0x01; + ALPHABET_VALUES['L'] = 0x01; } @@ -133,6 +139,14 @@ public final class Ulid implements Serializable, Comparable { this.lsb = leastSignificantBits; } + public static Ulid of(Ulid ulid) { + return new Ulid(ulid.getMostSignificantBits(), ulid.getLeastSignificantBits()); + } + + public static Ulid of(UUID uuid) { + return new Ulid(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); + } + // TODO: test public static Ulid of(byte[] bytes) { @@ -165,43 +179,42 @@ public final class Ulid implements Serializable, Comparable { } // TODO: optimize - public static Ulid of(String ulid) { + public static Ulid of(String string) { - final char[] chars = ulid == null ? new char[0] : ulid.toCharArray(); - UlidValidator.validate(chars); + final char[] chars = toCharArray(string); long tm = 0; long r1 = 0; long r2 = 0; - tm |= ENCODING_VALUES[chars[0x00]] << 45; - tm |= ENCODING_VALUES[chars[0x01]] << 40; - tm |= ENCODING_VALUES[chars[0x02]] << 35; - tm |= ENCODING_VALUES[chars[0x03]] << 30; - tm |= ENCODING_VALUES[chars[0x04]] << 25; - tm |= ENCODING_VALUES[chars[0x05]] << 20; - tm |= ENCODING_VALUES[chars[0x06]] << 15; - tm |= ENCODING_VALUES[chars[0x07]] << 10; - tm |= ENCODING_VALUES[chars[0x08]] << 5; - tm |= ENCODING_VALUES[chars[0x09]]; + tm |= ALPHABET_VALUES[chars[0x00]] << 45; + tm |= ALPHABET_VALUES[chars[0x01]] << 40; + tm |= ALPHABET_VALUES[chars[0x02]] << 35; + tm |= ALPHABET_VALUES[chars[0x03]] << 30; + tm |= ALPHABET_VALUES[chars[0x04]] << 25; + tm |= ALPHABET_VALUES[chars[0x05]] << 20; + tm |= ALPHABET_VALUES[chars[0x06]] << 15; + tm |= ALPHABET_VALUES[chars[0x07]] << 10; + tm |= ALPHABET_VALUES[chars[0x08]] << 5; + tm |= ALPHABET_VALUES[chars[0x09]]; - r1 |= ENCODING_VALUES[chars[0x0a]] << 35; - r1 |= ENCODING_VALUES[chars[0x0b]] << 30; - r1 |= ENCODING_VALUES[chars[0x0c]] << 25; - r1 |= ENCODING_VALUES[chars[0x0d]] << 20; - r1 |= ENCODING_VALUES[chars[0x0e]] << 15; - r1 |= ENCODING_VALUES[chars[0x0f]] << 10; - r1 |= ENCODING_VALUES[chars[0x10]] << 5; - r1 |= ENCODING_VALUES[chars[0x11]]; + r1 |= ALPHABET_VALUES[chars[0x0a]] << 35; + r1 |= ALPHABET_VALUES[chars[0x0b]] << 30; + r1 |= ALPHABET_VALUES[chars[0x0c]] << 25; + r1 |= ALPHABET_VALUES[chars[0x0d]] << 20; + r1 |= ALPHABET_VALUES[chars[0x0e]] << 15; + r1 |= ALPHABET_VALUES[chars[0x0f]] << 10; + r1 |= ALPHABET_VALUES[chars[0x10]] << 5; + r1 |= ALPHABET_VALUES[chars[0x11]]; - r2 |= ENCODING_VALUES[chars[0x12]] << 35; - r2 |= ENCODING_VALUES[chars[0x13]] << 30; - r2 |= ENCODING_VALUES[chars[0x14]] << 25; - r2 |= ENCODING_VALUES[chars[0x15]] << 20; - r2 |= ENCODING_VALUES[chars[0x16]] << 15; - r2 |= ENCODING_VALUES[chars[0x17]] << 10; - r2 |= ENCODING_VALUES[chars[0x18]] << 5; - r2 |= ENCODING_VALUES[chars[0x19]]; + r2 |= ALPHABET_VALUES[chars[0x12]] << 35; + r2 |= ALPHABET_VALUES[chars[0x13]] << 30; + r2 |= ALPHABET_VALUES[chars[0x14]] << 25; + r2 |= ALPHABET_VALUES[chars[0x15]] << 20; + r2 |= ALPHABET_VALUES[chars[0x16]] << 15; + r2 |= ALPHABET_VALUES[chars[0x17]] << 10; + r2 |= ALPHABET_VALUES[chars[0x18]] << 5; + r2 |= ALPHABET_VALUES[chars[0x19]]; final long msb = (tm << 16) | (r1 >>> 24); final long lsb = (r1 << 40) | (r2 & 0xffffffffffL); @@ -209,10 +222,6 @@ public final class Ulid implements Serializable, Comparable { return new Ulid(msb, lsb); } - public static Ulid of(UUID uuid) { - return new Ulid(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); - } - public static Ulid of(long time, byte[] random) { if ((time & 0xffff000000000000L) != 0) { @@ -241,6 +250,17 @@ public final class Ulid implements Serializable, Comparable { return new Ulid(msb, lsb); } + public UUID toUuid() { + return new UUID(this.msb, this.lsb); + } + + // TODO: test + public UUID toUuid4() { + final long msb4 = (this.msb & 0xffffffffffff0fffL) | 0x0000000000004000L; // apply version 4 + final long lsb4 = (this.lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // apply variant RFC-4122 + return new UUID(msb4, lsb4); + } + // TODO: test public byte[] toBytes() { @@ -272,64 +292,29 @@ public final class Ulid implements Serializable, Comparable { return Ulid.of(this.toUuid4()).toBytes(); } - // TODO: optimize @Override public String toString() { - - final char[] chars = new char[ULID_LENGTH]; - long long0 = this.msb; - long long1 = this.lsb; - - long time = long0 >>> 16; - long random1 = ((long0 & 0xffffL) << 24) | (long1 >>> 40); - long random2 = (long1 & 0xffffffffffL); - - chars[0x00] = ENCODING_CHARS[(int) (time >>> 45 & 0b11111)]; - chars[0x01] = ENCODING_CHARS[(int) (time >>> 40 & 0b11111)]; - chars[0x02] = ENCODING_CHARS[(int) (time >>> 35 & 0b11111)]; - chars[0x03] = ENCODING_CHARS[(int) (time >>> 30 & 0b11111)]; - chars[0x04] = ENCODING_CHARS[(int) (time >>> 25 & 0b11111)]; - chars[0x05] = ENCODING_CHARS[(int) (time >>> 20 & 0b11111)]; - chars[0x06] = ENCODING_CHARS[(int) (time >>> 15 & 0b11111)]; - chars[0x07] = ENCODING_CHARS[(int) (time >>> 10 & 0b11111)]; - chars[0x08] = ENCODING_CHARS[(int) (time >>> 5 & 0b11111)]; - chars[0x09] = ENCODING_CHARS[(int) (time & 0b11111)]; - - chars[0x0a] = ENCODING_CHARS[(int) (random1 >>> 35 & 0b11111)]; - chars[0x0b] = ENCODING_CHARS[(int) (random1 >>> 30 & 0b11111)]; - chars[0x0c] = ENCODING_CHARS[(int) (random1 >>> 25 & 0b11111)]; - chars[0x0d] = ENCODING_CHARS[(int) (random1 >>> 20 & 0b11111)]; - chars[0x0e] = ENCODING_CHARS[(int) (random1 >>> 15 & 0b11111)]; - chars[0x0f] = ENCODING_CHARS[(int) (random1 >>> 10 & 0b11111)]; - chars[0x10] = ENCODING_CHARS[(int) (random1 >>> 5 & 0b11111)]; - chars[0x11] = ENCODING_CHARS[(int) (random1 & 0b11111)]; - - chars[0x12] = ENCODING_CHARS[(int) (random2 >>> 35 & 0b11111)]; - chars[0x13] = ENCODING_CHARS[(int) (random2 >>> 30 & 0b11111)]; - chars[0x14] = ENCODING_CHARS[(int) (random2 >>> 25 & 0b11111)]; - chars[0x15] = ENCODING_CHARS[(int) (random2 >>> 20 & 0b11111)]; - chars[0x16] = ENCODING_CHARS[(int) (random2 >>> 15 & 0b11111)]; - chars[0x17] = ENCODING_CHARS[(int) (random2 >>> 10 & 0b11111)]; - chars[0x18] = ENCODING_CHARS[(int) (random2 >>> 5 & 0b11111)]; - chars[0x19] = ENCODING_CHARS[(int) (random2 & 0b11111)]; - - return new String(chars); + return this.toUpperCase(); } // TODO: test - public String toString4() { - return Ulid.of(this.toUuid4()).toString(); - } - - public UUID toUuid() { - return new UUID(this.msb, this.lsb); + public String toUpperCase() { + return toString(ALPHABET_UPPERCASE); } // TODO: test - public UUID toUuid4() { - final long msb4 = (this.msb & 0xffffffffffff0fffL) | 0x0000000000004000L; // apply version 4 - final long lsb4 = (this.lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // apply variant RFC-4122 - return new UUID(msb4, lsb4); + public String toUpperCase4() { + return Ulid.of(this.toUuid4()).toUpperCase(); + } + + // TODO: test + public String toLowerCase() { + return toString(ALPHABET_LOWERCASE); + } + + // TODO: test + public String toLowerCase4() { + return Ulid.of(this.toUuid4()).toLowerCase(); } public long getTime() { @@ -348,6 +333,20 @@ public final class Ulid implements Serializable, Comparable { return this.lsb; } + // TODO: test + public Ulid increment() { + + long msb1 = this.msb; + long lsb1 = this.lsb + 1; // Increment the LSB + + if (lsb1 == INCREMENT_OVERFLOW) { + // Increment the random bits of the MSB + msb1 = (msb1 & 0xffffffffffff0000L) | ((msb1 + 1) & 0x000000000000ffffL); + } + + return new Ulid(msb1, lsb1); + } + @Override public int hashCode() { final int prime = 31; @@ -385,4 +384,86 @@ public final class Ulid implements Serializable, Comparable { return 1; return 0; } + + // TODO: optimize + protected String toString(char[] alphabet) { + + final char[] chars = new char[ULID_LENGTH]; + + long time = this.msb >>> 16; + long random1 = ((this.msb & 0xffffL) << 24) | (this.lsb >>> 40); + long random2 = (this.lsb & 0xffffffffffL); + + chars[0x00] = alphabet[(int) (time >>> 45 & 0b11111)]; + chars[0x01] = alphabet[(int) (time >>> 40 & 0b11111)]; + chars[0x02] = alphabet[(int) (time >>> 35 & 0b11111)]; + chars[0x03] = alphabet[(int) (time >>> 30 & 0b11111)]; + chars[0x04] = alphabet[(int) (time >>> 25 & 0b11111)]; + chars[0x05] = alphabet[(int) (time >>> 20 & 0b11111)]; + chars[0x06] = alphabet[(int) (time >>> 15 & 0b11111)]; + chars[0x07] = alphabet[(int) (time >>> 10 & 0b11111)]; + chars[0x08] = alphabet[(int) (time >>> 5 & 0b11111)]; + chars[0x09] = alphabet[(int) (time & 0b11111)]; + + chars[0x0a] = alphabet[(int) (random1 >>> 35 & 0b11111)]; + chars[0x0b] = alphabet[(int) (random1 >>> 30 & 0b11111)]; + chars[0x0c] = alphabet[(int) (random1 >>> 25 & 0b11111)]; + chars[0x0d] = alphabet[(int) (random1 >>> 20 & 0b11111)]; + chars[0x0e] = alphabet[(int) (random1 >>> 15 & 0b11111)]; + chars[0x0f] = alphabet[(int) (random1 >>> 10 & 0b11111)]; + chars[0x10] = alphabet[(int) (random1 >>> 5 & 0b11111)]; + chars[0x11] = alphabet[(int) (random1 & 0b11111)]; + + chars[0x12] = alphabet[(int) (random2 >>> 35 & 0b11111)]; + chars[0x13] = alphabet[(int) (random2 >>> 30 & 0b11111)]; + chars[0x14] = alphabet[(int) (random2 >>> 25 & 0b11111)]; + chars[0x15] = alphabet[(int) (random2 >>> 20 & 0b11111)]; + chars[0x16] = alphabet[(int) (random2 >>> 15 & 0b11111)]; + chars[0x17] = alphabet[(int) (random2 >>> 10 & 0b11111)]; + chars[0x18] = alphabet[(int) (random2 >>> 5 & 0b11111)]; + chars[0x19] = alphabet[(int) (random2 & 0b11111)]; + + return new String(chars); + } + + public static boolean isValidString(String string) { + return isValidArray(string == null ? null : string.toCharArray()); + } + + /** + * Checks if the string is a valid ULID. + * + * A valid ULID string is a sequence of 26 characters from Crockford's base 32 + * alphabet. + * + * @param chars a char array + * @return boolean true if valid + */ + protected static boolean isValidArray(final char[] chars) { + + if (chars == null || chars.length != ULID_LENGTH) { + return false; // null or wrong size! + } + + // the two extra bits added by base-32 encoding must be zero + if ((ALPHABET_VALUES[chars[0]] & 0b11000) != 0) { + return false; // overflow! + } + + for (int i = 0; i < chars.length; i++) { + if (ALPHABET_VALUES[chars[i]] == -1) { + return false; // invalid character! + } + } + + return true; // It seems to be OK. + } + + protected static char[] toCharArray(String string) { + char[] chars = string == null ? new char[0] : string.toCharArray(); + if (!isValidArray(chars)) { + throw new IllegalArgumentException(String.format("Invalid ULID: \"%s\"", string)); + } + return chars; + } } diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java index 2dacd47..08ca495 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -24,9 +24,8 @@ package com.github.f4b6a3.ulid; +import com.github.f4b6a3.ulid.creator.MonotonicUlidSpecCreator; import com.github.f4b6a3.ulid.creator.UlidSpecCreator; -import com.github.f4b6a3.ulid.creator.impl.DefaultUlidSpecCreator; -import com.github.f4b6a3.ulid.creator.impl.MonotonicUlidSpecCreator; public final class UlidCreator { @@ -49,19 +48,19 @@ public final class UlidCreator { return MonotonicCreatorHolder.INSTANCE.create(time); } - public static DefaultUlidSpecCreator getDefaultCreator() { - return new DefaultUlidSpecCreator(); + public static UlidSpecCreator getUlidSpecCreator() { + return new UlidSpecCreator(); } - public static MonotonicUlidSpecCreator getMonotonicCreator() { + public static UlidSpecCreator getMonotonicUlidSpecCreator() { return new MonotonicUlidSpecCreator(); } private static class DefaultCreatorHolder { - static final UlidSpecCreator INSTANCE = getDefaultCreator(); + static final UlidSpecCreator INSTANCE = getUlidSpecCreator(); } private static class MonotonicCreatorHolder { - static final UlidSpecCreator INSTANCE = getMonotonicCreator(); + static final UlidSpecCreator INSTANCE = getMonotonicUlidSpecCreator(); } } diff --git a/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java b/src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreator.java similarity index 68% rename from src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java rename to src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreator.java index 516dc87..0d21d19 100644 --- a/src/main/java/com/github/f4b6a3/ulid/exception/InvalidUlidException.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreator.java @@ -22,13 +22,27 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.exception; +package com.github.f4b6a3.ulid.creator; -public final class InvalidUlidException extends RuntimeException { +import com.github.f4b6a3.ulid.Ulid; - private static final long serialVersionUID = 1L; +public final class MonotonicUlidSpecCreator extends UlidSpecCreator { - public InvalidUlidException(String message) { - super(message); + private long lastTime; + private Ulid lastUlid; + + @Override + public synchronized Ulid create(final long time) { + + if (time == this.lastTime) { + this.lastUlid = lastUlid.increment(); + } else { + final byte[] random = new byte[10]; + this.randomStrategy.nextBytes(random); + this.lastUlid = Ulid.of(time, random); + } + + this.lastTime = time; + return this.lastUlid; } } diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java index b487a88..8dc532c 100644 --- a/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java @@ -27,11 +27,10 @@ package com.github.f4b6a3.ulid.creator; import java.util.Random; import com.github.f4b6a3.ulid.Ulid; +import com.github.f4b6a3.ulid.strategy.DefaultRandomStrategy; import com.github.f4b6a3.ulid.strategy.RandomStrategy; -import com.github.f4b6a3.ulid.strategy.random.DefaultRandomStrategy; -import com.github.f4b6a3.ulid.strategy.random.OtherRandomStrategy; -public abstract class UlidSpecCreator { +public class UlidSpecCreator { protected RandomStrategy randomStrategy; @@ -39,11 +38,15 @@ public abstract class UlidSpecCreator { this.randomStrategy = new DefaultRandomStrategy(); } - public synchronized Ulid create() { + public Ulid create() { return create(System.currentTimeMillis()); } - public abstract Ulid create(final long time); + public Ulid create(final long time) { + final byte[] random = new byte[10]; + this.randomStrategy.nextBytes(random); + return Ulid.of(time, random); + } /** * Replaces the default random strategy with another. @@ -61,20 +64,4 @@ public abstract class UlidSpecCreator { this.randomStrategy = randomStrategy; return (T) this; } - - /** - * Replaces the default random strategy with another that uses the input - * {@link Random} instance. - * - * It replaces the internal {@link DefaultRandomStrategy} with - * {@link OtherRandomStrategy}. - * - * @param random a random generator - * @return {@link UlidSpecCreator} - */ - @SuppressWarnings("unchecked") - public synchronized T withRandomGenerator(Random random) { - this.randomStrategy = new OtherRandomStrategy(random); - return (T) this; - } } diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/impl/DefaultUlidSpecCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/impl/DefaultUlidSpecCreator.java deleted file mode 100644 index 1c5803d..0000000 --- a/src/main/java/com/github/f4b6a3/ulid/creator/impl/DefaultUlidSpecCreator.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020 Fabio Lima - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.github.f4b6a3.ulid.creator.impl; - -import com.github.f4b6a3.ulid.Ulid; -import com.github.f4b6a3.ulid.creator.UlidSpecCreator; - -public final class DefaultUlidSpecCreator extends UlidSpecCreator { - - @Override - public synchronized Ulid create(final long time) { - - long msb = 0; - long lsb = 0; - - final byte[] bytes = new byte[10]; - this.randomStrategy.nextBytes(bytes); - - msb |= time << 16; - msb |= (long) (bytes[0x0] & 0xff) << 8; - msb |= (long) (bytes[0x1] & 0xff); - - lsb |= (long) (bytes[0x2] & 0xff) << 56; - lsb |= (long) (bytes[0x3] & 0xff) << 48; - lsb |= (long) (bytes[0x4] & 0xff) << 40; - lsb |= (long) (bytes[0x5] & 0xff) << 32; - lsb |= (long) (bytes[0x6] & 0xff) << 24; - lsb |= (long) (bytes[0x7] & 0xff) << 16; - lsb |= (long) (bytes[0x8] & 0xff) << 8; - lsb |= (long) (bytes[0x9] & 0xff); - - return new Ulid(msb, lsb); - } -} diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/impl/MonotonicUlidSpecCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/impl/MonotonicUlidSpecCreator.java deleted file mode 100644 index 2ec38ce..0000000 --- a/src/main/java/com/github/f4b6a3/ulid/creator/impl/MonotonicUlidSpecCreator.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2020 Fabio Lima - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.github.f4b6a3.ulid.creator.impl; - -import com.github.f4b6a3.ulid.Ulid; -import com.github.f4b6a3.ulid.creator.UlidSpecCreator; - -public final class MonotonicUlidSpecCreator extends UlidSpecCreator { - - private long msb = 0; - private long lsb = 0; - - private long lastTime; - - // 0xffffffffffffffffL + 1 = 0x0000000000000000L - private static final long UNSIGNED_OVERFLOW = 0x0000000000000000L; - - @Override - public synchronized Ulid create(final long time) { - - // TODO: test - if (time == this.lastTime) { - if (++this.lsb == UNSIGNED_OVERFLOW) { - // Increment the random bits of the MSB - this.msb = (this.msb & 0xffffffffffff0000L) | ((this.msb + 1) & 0x000000000000ffffL); - } - } else { - - this.msb = 0; - this.lsb = 0; - - final byte[] bytes = new byte[10]; - this.randomStrategy.nextBytes(bytes); - - this.msb |= time << 16; - this.msb |= (long) (bytes[0x0] & 0xff) << 8; - this.msb |= (long) (bytes[0x1] & 0xff); - - this.lsb |= (long) (bytes[0x2] & 0xff) << 56; - this.lsb |= (long) (bytes[0x3] & 0xff) << 48; - this.lsb |= (long) (bytes[0x4] & 0xff) << 40; - this.lsb |= (long) (bytes[0x5] & 0xff) << 32; - this.lsb |= (long) (bytes[0x6] & 0xff) << 24; - this.lsb |= (long) (bytes[0x7] & 0xff) << 16; - this.lsb |= (long) (bytes[0x8] & 0xff) << 8; - this.lsb |= (long) (bytes[0x9] & 0xff); - } - - this.lastTime = time; - return new Ulid(msb, lsb); - } -} diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/random/DefaultRandomStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/DefaultRandomStrategy.java similarity index 93% rename from src/main/java/com/github/f4b6a3/ulid/strategy/random/DefaultRandomStrategy.java rename to src/main/java/com/github/f4b6a3/ulid/strategy/DefaultRandomStrategy.java index 5f54dc2..eec3935 100644 --- a/src/main/java/com/github/f4b6a3/ulid/strategy/random/DefaultRandomStrategy.java +++ b/src/main/java/com/github/f4b6a3/ulid/strategy/DefaultRandomStrategy.java @@ -22,13 +22,11 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.strategy.random; +package com.github.f4b6a3.ulid.strategy; import java.security.SecureRandom; import java.util.Random; -import com.github.f4b6a3.ulid.strategy.RandomStrategy; - /** * It uses an instance of {@link java.security.SecureRandom}. */ diff --git a/src/main/java/com/github/f4b6a3/ulid/strategy/random/OtherRandomStrategy.java b/src/main/java/com/github/f4b6a3/ulid/strategy/random/OtherRandomStrategy.java deleted file mode 100644 index be90846..0000000 --- a/src/main/java/com/github/f4b6a3/ulid/strategy/random/OtherRandomStrategy.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018-2020 Fabio Lima - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.github.f4b6a3.ulid.strategy.random; - -import java.util.Random; - -import com.github.f4b6a3.ulid.strategy.RandomStrategy; - -/** - * It uses an instance of {@link java.util.Random} injected by constructor. - */ -public final class OtherRandomStrategy implements RandomStrategy { - - private final Random random; - - public OtherRandomStrategy(Random random) { - this.random = random; - } - - @Override - public void nextBytes(byte[] bytes) { - this.random.nextBytes(bytes); - } -} diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java deleted file mode 100644 index ef2822d..0000000 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * 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 final class UlidValidator { - - protected static final int STRING_LENGTH = 26; - - protected static final long[] BASE32_VALUES = new long[128]; - static { - for (int i = 0; i < BASE32_VALUES.length; i++) { - BASE32_VALUES[i] = -1; - } - // Numbers - BASE32_VALUES['0'] = 0x00; - BASE32_VALUES['1'] = 0x01; - BASE32_VALUES['2'] = 0x02; - BASE32_VALUES['3'] = 0x03; - BASE32_VALUES['4'] = 0x04; - BASE32_VALUES['5'] = 0x05; - BASE32_VALUES['6'] = 0x06; - BASE32_VALUES['7'] = 0x07; - BASE32_VALUES['8'] = 0x08; - BASE32_VALUES['9'] = 0x09; - // Lower case - BASE32_VALUES['a'] = 0x0a; - BASE32_VALUES['b'] = 0x0b; - BASE32_VALUES['c'] = 0x0c; - BASE32_VALUES['d'] = 0x0d; - BASE32_VALUES['e'] = 0x0e; - BASE32_VALUES['f'] = 0x0f; - BASE32_VALUES['g'] = 0x10; - BASE32_VALUES['h'] = 0x11; - BASE32_VALUES['j'] = 0x12; - BASE32_VALUES['k'] = 0x13; - BASE32_VALUES['m'] = 0x14; - BASE32_VALUES['n'] = 0x15; - BASE32_VALUES['p'] = 0x16; - BASE32_VALUES['q'] = 0x17; - BASE32_VALUES['r'] = 0x18; - BASE32_VALUES['s'] = 0x19; - BASE32_VALUES['t'] = 0x1a; - BASE32_VALUES['v'] = 0x1b; - BASE32_VALUES['w'] = 0x1c; - BASE32_VALUES['x'] = 0x1d; - BASE32_VALUES['y'] = 0x1e; - BASE32_VALUES['z'] = 0x1f; - // Lower case OIL - BASE32_VALUES['o'] = 0x00; - BASE32_VALUES['i'] = 0x01; - BASE32_VALUES['l'] = 0x01; - // Upper case - BASE32_VALUES['A'] = 0x0a; - BASE32_VALUES['B'] = 0x0b; - BASE32_VALUES['C'] = 0x0c; - BASE32_VALUES['D'] = 0x0d; - BASE32_VALUES['E'] = 0x0e; - BASE32_VALUES['F'] = 0x0f; - BASE32_VALUES['G'] = 0x10; - BASE32_VALUES['H'] = 0x11; - BASE32_VALUES['J'] = 0x12; - BASE32_VALUES['K'] = 0x13; - BASE32_VALUES['M'] = 0x14; - BASE32_VALUES['N'] = 0x15; - BASE32_VALUES['P'] = 0x16; - BASE32_VALUES['Q'] = 0x17; - BASE32_VALUES['R'] = 0x18; - BASE32_VALUES['S'] = 0x19; - BASE32_VALUES['T'] = 0x1a; - BASE32_VALUES['V'] = 0x1b; - BASE32_VALUES['W'] = 0x1c; - BASE32_VALUES['X'] = 0x1d; - BASE32_VALUES['Y'] = 0x1e; - BASE32_VALUES['Z'] = 0x1f; - // Upper case OIL - BASE32_VALUES['O'] = 0x00; - BASE32_VALUES['I'] = 0x01; - BASE32_VALUES['L'] = 0x01; - - } - - 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. - * - * It also checks if the timestamp is between 0 and 2^48-1. - * - *
-	 * Examples of valid ULID strings:
-	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except U)
-	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, including OIL, except U)
-	 * - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except U, with hyphens)
-	 * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, including OIL, except U, with hyphens)
-	 * 
- * - * @param ulid a ULID - * @return boolean true if valid - */ - public static boolean isValid(String ulid) { - return (ulid != null && ulid.length() != 0 && isValidString(ulid.toCharArray())); - } - - /** - * Checks if the char array is a valid ULID. - * - * A valid ULID string is a sequence of 26 characters from Crockford's base 32 - * alphabet. - * - * It also checks if the timestamp is between 0 and 2^48-1. - * - *
-	 * Examples of valid ULID strings:
-	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except U)
-	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, including OIL, except U)
-	 * - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except U, with hyphens)
-	 * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, including OIL, except U, with hyphens)
-	 * 
- * - * @param ulid a ULID char array - * @return boolean true if valid - */ - public static boolean isValid(char[] ulid) { - return (ulid != null && ulid.length != 0 && isValidString(ulid)); - } - - /** - * Checks if the ULID string is valid. - * - * See {@link UlidValidator#isValid(String)}. - * - * @param ulid a ULID string - * @throws InvalidUlidException if invalid - */ - public static void validate(String ulid) { - if (ulid == null || ulid.length() == 0 || !isValidString(ulid.toCharArray())) { - throw new InvalidUlidException("Invalid ULID: \"" + ulid + "\""); - } - } - - /** - * Checks if the ULID char array is valid. - * - * See {@link UlidValidator#isValid(String)}. - * - * @param ulid a ULID char array - * @throws InvalidUlidException if invalid - */ - public static void validate(char[] ulid) { - if (ulid == null || ulid.length == 0 || !isValidString(ulid)) { - throw new InvalidUlidException("Invalid ULID: \"" + (ulid == null ? null : new String(ulid)) + "\""); - } - } - - /** - * Checks if the string is a valid ULID. - * - * A valid ULID string is a sequence of 26 characters from Crockford's base 32 - * alphabet. - * - *
-	 * Examples of valid ULID strings:
-	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except U)
-	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, including OIL, except U)
-	 * - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except U, with hyphens)
-	 * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, including OIL, except U, with hyphens)
-	 * 
- * - * @param c a char array - * @return boolean true if valid - */ - protected static boolean isValidString(final char[] c) { - - // the two extra bits added by base-32 encoding must be zero - if ((BASE32_VALUES[c[0]] & 0b11000) != 0) { - return false; // overflow - } - - int hyphen = 0; - for (int i = 0; i < c.length; i++) { - if (c[i] == '-') { - hyphen++; - continue; - } - if (c[i] == 'U' || c[i] == 'u') { - return false; - } - // ASCII codes: A-Z, 0-9, a-z - if (!((c[i] >= 0x41 && c[i] <= 0x5a) || (c[i] >= 0x30 && c[i] <= 0x39) || (c[i] >= 0x61 && c[i] <= 0x7a))) { - return false; - } - } - return (c.length - hyphen) == STRING_LENGTH; - } -} diff --git a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java index 3a4dd5f..fbbae66 100644 --- a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java +++ b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java @@ -3,22 +3,13 @@ package com.github.f4b6a3.ulid; import org.junit.runner.RunWith; import org.junit.runners.Suite; +import com.github.f4b6a3.ulid.creator.MonotonicUlidSpecCreatorTest; import com.github.f4b6a3.ulid.creator.UlidSpecCreatorTest; -import com.github.f4b6a3.ulid.ulid.UlidCreatorDefaultTest; -import com.github.f4b6a3.ulid.ulid.UlidCreatorDefaultStringTest; -import com.github.f4b6a3.ulid.ulid.UlidCreatorMonotonicTest; -import com.github.f4b6a3.ulid.ulid.UlidCreatorMonotonicStringTest; -import com.github.f4b6a3.ulid.util.UlidValidatorTest; -import com.github.f4b6a3.ulid.util.internal.UlidTest; @RunWith(Suite.class) @Suite.SuiteClasses({ - UlidCreatorDefaultTest.class, - UlidCreatorDefaultStringTest.class, - UlidCreatorMonotonicTest.class, - UlidCreatorMonotonicStringTest.class, + MonotonicUlidSpecCreatorTest.class, UlidSpecCreatorTest.class, - UlidValidatorTest.class, UlidTest.class, }) diff --git a/src/test/java/com/github/f4b6a3/ulid/util/internal/UlidTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java similarity index 78% rename from src/test/java/com/github/f4b6a3/ulid/util/internal/UlidTest.java rename to src/test/java/com/github/f4b6a3/ulid/UlidTest.java index 85dd723..ed2a9b4 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/internal/UlidTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java @@ -1,6 +1,8 @@ -package com.github.f4b6a3.ulid.util.internal; +package com.github.f4b6a3.ulid; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.util.Random; import java.util.UUID; @@ -11,7 +13,7 @@ import com.github.f4b6a3.ulid.Ulid; public class UlidTest { - private static final int DEFAULT_LOOP_MAX = 100_000; + private static final int DEFAULT_LOOP_MAX = 10_000; protected static final char[] ALPHABET_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray(); protected static final char[] ALPHABET_JAVA = "0123456789abcdefghijklmnopqrstuv".toCharArray(); // Long.parseUnsignedLong() @@ -191,6 +193,40 @@ public class UlidTest { return r1 + r2; } + + @Test + public void isValidString() { + + String ulid = null; // Null + assertFalse("Null ULID should be invalid.", Ulid.isValidString(ulid)); + + ulid = ""; // length: 0 + assertFalse("ULID with empty string should be invalid .", Ulid.isValidString(ulid)); + + ulid = "0123456789ABCDEFGHJKMNPQRS"; // All upper case + assertTrue("ULID in upper case should valid.", Ulid.isValidString(ulid)); + + ulid = "0123456789abcdefghjklmnpqr"; // All lower case + assertTrue("ULID in lower case should be valid.", Ulid.isValidString(ulid)); + + ulid = "0123456789AbCdEfGhJkMnPqRs"; // Mixed case + assertTrue("Ulid in upper and lower case should valid.", Ulid.isValidString(ulid)); + + ulid = "0123456789ABCDEFGHJKLMNPQ"; // length: 25 + assertFalse("ULID length lower than 26 should be invalid.", Ulid.isValidString(ulid)); + + ulid = "0123456789ABCDEFGHJKMNPQZZZ"; // length: 27 + assertFalse("ULID length greater than 26 should be invalid.", Ulid.isValidString(ulid)); + + ulid = "u123456789ABCDEFGHJKMNPQRS"; // Letter u + assertFalse("ULID with 'u' or 'U' should be invalid.", Ulid.isValidString(ulid)); + + ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char + assertFalse("ULID with special chars should be invalid.", Ulid.isValidString(ulid)); + + ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1 + assertFalse("ULID with timestamp greater than (2^48)-1 should be invalid.", Ulid.isValidString(ulid)); + } private static String transliterate(String string, char[] alphabet1, char[] alphabet2) { char[] output = string.toCharArray(); diff --git a/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java index 9516985..9899d33 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UniquenessTest.java @@ -117,7 +117,7 @@ public class UniquenessTest { } public static void execute(boolean verbose, int threadCount, int requestCount) { - UlidSpecCreator creator = UlidCreator.getMonotonicCreator(); + UlidSpecCreator creator = UlidCreator.getMonotonicUlidSpecCreator(); UniquenessTest test = new UniquenessTest(threadCount, requestCount, creator, verbose); test.start(); diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/AbstractUlidSpecCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/AbstractUlidSpecCreatorTest.java new file mode 100644 index 0000000..09b70d8 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/creator/AbstractUlidSpecCreatorTest.java @@ -0,0 +1,53 @@ +package com.github.f4b6a3.ulid.creator; + +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +public abstract class AbstractUlidSpecCreatorTest { + + protected static final int DEFAULT_LOOP_MAX = 10_000; + + protected static final String DUPLICATE_UUID_MSG = "A duplicate ULID was created."; + + protected static final int THREAD_TOTAL = availableProcessors(); + + protected static final Random RANDOM = new Random(); + + protected static final long TIME_MASK = 0x0000ffffffffffffL; + + private static int availableProcessors() { + int processors = Runtime.getRuntime().availableProcessors(); + if (processors < 4) { + processors = 4; + } + return processors; + } + + protected static class TestThread extends Thread { + + public static Set hashSet = new HashSet<>(); + private UlidSpecCreator creator; + private int loopLimit; + + public TestThread(UlidSpecCreator creator, int loopLimit) { + this.creator = creator; + this.loopLimit = loopLimit; + } + + public static void clearHashSet() { + hashSet = new HashSet<>(); + } + + @Override + public void run() { + long timestamp = System.currentTimeMillis(); + for (int i = 0; i < loopLimit; i++) { + synchronized (hashSet) { + hashSet.add(creator.create(timestamp).toUuid()); + } + } + } + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorMonotonicTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreatorTest.java similarity index 63% rename from src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorMonotonicTest.java rename to src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreatorTest.java index 80efe8d..c8e1f2b 100644 --- a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorMonotonicTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/creator/MonotonicUlidSpecCreatorTest.java @@ -1,4 +1,4 @@ -package com.github.f4b6a3.ulid.ulid; +package com.github.f4b6a3.ulid.creator; import org.junit.Test; @@ -6,12 +6,11 @@ import com.github.f4b6a3.ulid.Ulid; import com.github.f4b6a3.ulid.UlidCreator; import static org.junit.Assert.*; + import java.util.Arrays; import java.util.HashSet; -public class UlidCreatorMonotonicTest { - - private static final int DEFAULT_LOOP_MAX = 100_000; +public class MonotonicUlidSpecCreatorTest extends AbstractUlidSpecCreatorTest { @Test public void testGetUlid() { @@ -68,4 +67,34 @@ public class UlidCreatorMonotonicTest { assertEquals("The ULID list is not ordered", list[i], other[i]); } } + + @Test + public void testGetMonotonicUlidInParallel() throws InterruptedException { + + Thread[] threads = new Thread[THREAD_TOTAL]; + TestThread.clearHashSet(); + + // Instantiate and start many threads + for (int i = 0; i < THREAD_TOTAL; i++) { + threads[i] = new TestThread(UlidCreator.getMonotonicUlidSpecCreator(), DEFAULT_LOOP_MAX); + threads[i].start(); + } + + // Wait all the threads to finish + for (Thread thread : threads) { + thread.join(); + } + + // Check if the quantity of unique UUIDs is correct + assertEquals(DUPLICATE_UUID_MSG, TestThread.hashSet.size(), (DEFAULT_LOOP_MAX * THREAD_TOTAL)); + } + + @Test + public void testGetMonotonicUlidTime() { + for (int i = 0; i < 100; i++) { + long time = RANDOM.nextLong() & TIME_MASK; + Ulid ulid = UlidCreator.getMonotonicUlid(time); + assertEquals(time, ulid.getTime()); + } + } } diff --git a/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java index 154df73..4cf38c4 100644 --- a/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/creator/UlidSpecCreatorTest.java @@ -1,10 +1,5 @@ package com.github.f4b6a3.ulid.creator; -import java.util.HashSet; -import java.util.Random; -import java.util.Set; -import java.util.UUID; - import org.junit.Test; import com.github.f4b6a3.ulid.Ulid; @@ -12,24 +7,75 @@ import com.github.f4b6a3.ulid.UlidCreator; import static org.junit.Assert.*; -public class UlidSpecCreatorTest { +import java.util.HashSet; - private static final int DEFAULT_LOOP_MAX = 100_000; +public class UlidSpecCreatorTest extends AbstractUlidSpecCreatorTest { - protected static final String DUPLICATE_UUID_MSG = "A duplicate ULID was created."; + @Test + public void testGetUlid() { + Ulid[] list = new Ulid[DEFAULT_LOOP_MAX]; - protected static final int THREAD_TOTAL = availableProcessors(); + long startTime = System.currentTimeMillis(); - private static final Random RANDOM = new Random(); - - private static final long TIME_MASK = 0x0000ffffffffffffL; - - private static int availableProcessors() { - int processors = Runtime.getRuntime().availableProcessors(); - if (processors < 4) { - processors = 4; + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + list[i] = UlidCreator.getUlid(); } - return processors; + + long endTime = System.currentTimeMillis(); + + checkNullOrInvalid(list); + checkUniqueness(list); + checkCreationTime(list, startTime, endTime); + } + + private void checkNullOrInvalid(Ulid[] list) { + for (Ulid ulid : list) { + assertNotNull("ULID is null", ulid); + } + } + + private void checkUniqueness(Ulid[] list) { + + HashSet set = new HashSet<>(); + + for (Ulid ulid : list) { + assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid)); + } + + assertEquals("There are duplicated ULIDs", set.size(), list.length); + } + + private void checkCreationTime(Ulid[] list, long startTime, long endTime) { + + assertTrue("Start time was after end time", startTime <= endTime); + + for (Ulid ulid : list) { + long creationTime = ulid.getTime(); + assertTrue("Creation time was before start time " + creationTime + " " + startTime, + creationTime >= startTime); + assertTrue("Creation time was after end time", creationTime <= endTime); + } + } + + @Test + public void testGetUlidInParallel() throws InterruptedException { + + Thread[] threads = new Thread[THREAD_TOTAL]; + TestThread.clearHashSet(); + + // Instantiate and start many threads + for (int i = 0; i < THREAD_TOTAL; i++) { + threads[i] = new TestThread(UlidCreator.getUlidSpecCreator(), DEFAULT_LOOP_MAX); + threads[i].start(); + } + + // Wait all the threads to finish + for (Thread thread : threads) { + thread.join(); + } + + // Check if the quantity of unique UUIDs is correct + assertEquals(DUPLICATE_UUID_MSG, TestThread.hashSet.size(), (DEFAULT_LOOP_MAX * THREAD_TOTAL)); } @Test @@ -40,81 +86,4 @@ public class UlidSpecCreatorTest { assertEquals(time, ulid.getTime()); } } - - @Test - public void testGetMonotonicUlidTime() { - for (int i = 0; i < 100; i++) { - long time = RANDOM.nextLong() & TIME_MASK; - Ulid ulid = UlidCreator.getMonotonicUlid(time); - assertEquals(time, ulid.getTime()); - } - } - - @Test - public void testGetDefaultUlidInParallel() throws InterruptedException { - - Thread[] threads = new Thread[THREAD_TOTAL]; - TestThread.clearHashSet(); - - // Instantiate and start many threads - for (int i = 0; i < THREAD_TOTAL; i++) { - threads[i] = new TestThread(UlidCreator.getDefaultCreator(), DEFAULT_LOOP_MAX); - threads[i].start(); - } - - // Wait all the threads to finish - for (Thread thread : threads) { - thread.join(); - } - - // Check if the quantity of unique UUIDs is correct - assertEquals(DUPLICATE_UUID_MSG, TestThread.hashSet.size(), (DEFAULT_LOOP_MAX * THREAD_TOTAL)); - } - - @Test - public void testGetMonotonicUlidInParallel() throws InterruptedException { - - Thread[] threads = new Thread[THREAD_TOTAL]; - TestThread.clearHashSet(); - - // Instantiate and start many threads - for (int i = 0; i < THREAD_TOTAL; i++) { - threads[i] = new TestThread(UlidCreator.getMonotonicCreator(), DEFAULT_LOOP_MAX); - threads[i].start(); - } - - // Wait all the threads to finish - for (Thread thread : threads) { - thread.join(); - } - - // Check if the quantity of unique UUIDs is correct - assertEquals(DUPLICATE_UUID_MSG, TestThread.hashSet.size(), (DEFAULT_LOOP_MAX * THREAD_TOTAL)); - } - - public static class TestThread extends Thread { - - public static Set hashSet = new HashSet<>(); - private UlidSpecCreator creator; - private int loopLimit; - - public TestThread(UlidSpecCreator creator, int loopLimit) { - this.creator = creator; - this.loopLimit = loopLimit; - } - - public static void clearHashSet() { - hashSet = new HashSet<>(); - } - - @Override - public void run() { - long timestamp = System.currentTimeMillis(); - for (int i = 0; i < loopLimit; i++) { - synchronized (hashSet) { - hashSet.add(creator.create(timestamp).toUuid()); - } - } - } - } } diff --git a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorDefaultStringTest.java b/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorDefaultStringTest.java deleted file mode 100644 index ea7d23e..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorDefaultStringTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.f4b6a3.ulid.ulid; - -import org.junit.Test; - -import com.github.f4b6a3.ulid.Ulid; -import com.github.f4b6a3.ulid.UlidCreator; -import com.github.f4b6a3.ulid.util.UlidValidator; - -import static org.junit.Assert.*; -import java.util.HashSet; - -public class UlidCreatorDefaultStringTest { - - private static final int ULID_LENGTH = 26; - private static final int DEFAULT_LOOP_MAX = 100_000; - - @Test - public void testGetUlid() { - String[] list = new String[DEFAULT_LOOP_MAX]; - - long startTime = System.currentTimeMillis(); - - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - list[i] = UlidCreator.getUlid().toString(); - } - - long endTime = System.currentTimeMillis(); - - checkNullOrInvalid(list); - checkUniqueness(list); - checkCreationTime(list, startTime, endTime); - } - - private void checkNullOrInvalid(String[] list) { - for (String ulid : list) { - assertNotNull("ULID is null", ulid); - assertTrue("ULID is empty", !ulid.isEmpty()); - assertEquals("ULID length is wrong", ULID_LENGTH, ulid.length()); - assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); - } - } - - private void checkUniqueness(String[] list) { - - HashSet set = new HashSet<>(); - - for (String ulid : list) { - assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid)); - } - - assertEquals("There are duplicated ULIDs", set.size(), list.length); - } - - private void checkCreationTime(String[] list, long startTime, long endTime) { - - assertTrue("Start time was after end time", startTime <= endTime); - - for (String ulid : list) { - long creationTime = Ulid.of(ulid).getTime(); - assertTrue("Creation time was before start time " + creationTime + " " + startTime, - creationTime >= startTime); - assertTrue("Creation time was after end time", creationTime <= endTime); - } - } -} diff --git a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorDefaultTest.java b/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorDefaultTest.java deleted file mode 100644 index 565d991..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorDefaultTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.f4b6a3.ulid.ulid; - -import org.junit.Test; - -import com.github.f4b6a3.ulid.Ulid; -import com.github.f4b6a3.ulid.UlidCreator; - -import static org.junit.Assert.*; -import java.util.HashSet; - -public class UlidCreatorDefaultTest { - - private static final int DEFAULT_LOOP_MAX = 100_000; - - @Test - public void testGetUlid() { - Ulid[] list = new Ulid[DEFAULT_LOOP_MAX]; - - long startTime = System.currentTimeMillis(); - - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - list[i] = UlidCreator.getUlid(); - } - - long endTime = System.currentTimeMillis(); - - checkNullOrInvalid(list); - checkUniqueness(list); - checkCreationTime(list, startTime, endTime); - } - - private void checkNullOrInvalid(Ulid[] list) { - for (Ulid ulid : list) { - assertNotNull("ULID is null", ulid); - } - } - - private void checkUniqueness(Ulid[] list) { - - HashSet set = new HashSet<>(); - - for (Ulid ulid : list) { - assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid)); - } - - assertEquals("There are duplicated ULIDs", set.size(), list.length); - } - - private void checkCreationTime(Ulid[] list, long startTime, long endTime) { - - assertTrue("Start time was after end time", startTime <= endTime); - - for (Ulid ulid : list) { - long creationTime = ulid.getTime(); - assertTrue("Creation time was before start time " + creationTime + " " + startTime, - creationTime >= startTime); - assertTrue("Creation time was after end time", creationTime <= endTime); - } - } -} diff --git a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorMonotonicStringTest.java b/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorMonotonicStringTest.java deleted file mode 100644 index 57ecdda..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/ulid/UlidCreatorMonotonicStringTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.github.f4b6a3.ulid.ulid; - -import org.junit.Test; - -import com.github.f4b6a3.ulid.Ulid; -import com.github.f4b6a3.ulid.UlidCreator; -import com.github.f4b6a3.ulid.util.UlidValidator; - -import static org.junit.Assert.*; -import java.util.Arrays; -import java.util.HashSet; - -public class UlidCreatorMonotonicStringTest { - - private static final int ULID_LENGTH = 26; - private static final int DEFAULT_LOOP_MAX = 100_000; - - @Test - public void testGetUlid() { - String[] list = new String[DEFAULT_LOOP_MAX]; - - long startTime = System.currentTimeMillis(); - - for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - list[i] = UlidCreator.getMonotonicUlid().toString(); - } - - long endTime = System.currentTimeMillis(); - - checkNullOrInvalid(list); - checkUniqueness(list); - checkOrdering(list); - checkCreationTime(list, startTime, endTime); - } - - private void checkNullOrInvalid(String[] list) { - for (String ulid : list) { - assertNotNull("ULID is null", ulid); - assertTrue("ULID is empty", !ulid.isEmpty()); - assertEquals("ULID length is wrong", ULID_LENGTH, ulid.length()); - assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); - } - } - - private void checkUniqueness(String[] list) { - - HashSet set = new HashSet<>(); - - for (String ulid : list) { - assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid)); - } - - assertEquals("There are duplicated ULIDs", set.size(), list.length); - } - - private void checkCreationTime(String[] list, long startTime, long endTime) { - - assertTrue("Start time was after end time", startTime <= endTime); - - for (String ulid : list) { - long creationTime = Ulid.of(ulid).getTime(); - assertTrue("Creation time was before start time " + creationTime + " " + startTime, - creationTime >= startTime); - assertTrue("Creation time was after end time", creationTime <= endTime); - } - } - - private void checkOrdering(String[] list) { - String[] other = Arrays.copyOf(list, list.length); - Arrays.sort(other); - - for (int i = 0; i < list.length; i++) { - assertEquals("The ULID list is not ordered", list[i], other[i]); - } - } -} diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java deleted file mode 100644 index 05e917c..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidValidatorTest.java +++ /dev/null @@ -1,47 +0,0 @@ -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 testIsValid() { - - 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-ABCDEFGHJKM---NPQRS"; // 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)); - } -}