diff --git a/.gitignore b/.gitignore index a1c2a23..f56052a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ +# Eclipse +.classpath +.project +.settings/ + +# Maven +log/ +target/ + # Compiled class file *.class @@ -21,3 +30,6 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# Other +release.properties diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e401522 --- /dev/null +++ b/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + + com.github.f4b6a3 + ulid-creator + 0.0.1-SNAPSHOT + jar + + ulid-creator + http://github.com/f4b6a3 + A Java library for generating and handling ULIDs. + + + + + MIT License + https://opensource.org/licenses/MIT + + + + + + Fabio Lima + + + + + UTF-8 + 1.7 + ${jdk.version} + ${jdk.version} + + + + + junit + junit + 4.12 + test + + + + diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java new file mode 100644 index 0000000..628e55d --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -0,0 +1,74 @@ +/* + * 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; + +import java.util.UUID; + +import com.github.f4b6a3.ulid.factory.LexicalOrderGuidCreator; +import com.github.f4b6a3.ulid.util.UlidUtil; + +/** + * A factory for Universally Unique Lexicographically Sortable Identifiers. + * + * @see The ULID spec; https://github.com/ulid/spec + * + */ +public class UlidCreator { + + + private UlidCreator() { + } + + // TODO: not working yet + public static String getUlid() { + UUID guid = getLexicalOrderGuid(); + return UlidUtil.fromUuidToUlid(guid); + } + + /** + * Returns a Lexical Order GUID based on the ULID specification. + * + * If you need a ULID string instead of a GUID, use + * {@link UlidCreator#getUlid()}. + * + * @return a Lexical Order GUID + */ + public static UUID getLexicalOrderGuid() { + return LexicalOrderCreatorLazyHolder.INSTANCE.create(); + } + + /** + * Returns a {@link LexicalOrderGuidCreator}. + * + * @return {@link LexicalOrderGuidCreator} + */ + public static LexicalOrderGuidCreator getLexicalOrderCreator() { + return new LexicalOrderGuidCreator(); + } + + private static class LexicalOrderCreatorLazyHolder { + static final LexicalOrderGuidCreator INSTANCE = getLexicalOrderCreator().withoutOverflowException(); + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java b/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java new file mode 100644 index 0000000..8740403 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/exception/UlidCreatorException.java @@ -0,0 +1,34 @@ +/* + * MIT License + * + * Copyright (c) 2020 Fabio Lima + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.f4b6a3.ulid.exception; + +public class UlidCreatorException extends RuntimeException { + + private static final long serialVersionUID = 6755381080404981234L; + + public UlidCreatorException(String message) { + super(message); + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreator.java b/src/main/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreator.java new file mode 100644 index 0000000..7dcafdf --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreator.java @@ -0,0 +1,254 @@ +/* + * 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.factory; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.UUID; + +import com.github.f4b6a3.ulid.timestamp.TimestampStrategy; +import com.github.f4b6a3.ulid.random.Xorshift128PlusRandom; +import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategy; + +/** + * Factory that creates lexicographically sortable GUIDs, based on the ULID + * specification - Universally Unique Lexicographically Sortable Identifier. + * + * ULID specification: https://github.com/ulid/spec + */ +public class LexicalOrderGuidCreator { + + protected static final long MAX_LOW = 0xffffffffffffffffL; // unsigned + protected static final long MAX_HIGH = 0x000000000000ffffL; + + protected long previousTimestamp; + protected boolean enableOverflowException = true; + + protected Random random; + + protected long low; + protected long high; + + protected static final String OVERFLOW_MESSAGE = "The system caused an overflow in the generator by requesting too many GUIDs."; + + protected TimestampStrategy timestampStrategy; + + public LexicalOrderGuidCreator() { + this.reset(); + this.timestampStrategy = new DefaultTimestampStrategy(); + } + + /** + * + * Return a Lexical Order GUID. + * + * It has two parts: + * + * 1. A part of 48 bits that represent the amount of milliseconds since Unix + * Epoch, 1 January 1970. + * + * 2. A part of 80 bits that has a random value generated a secure random + * generator. + * + * If more than one GUID is generated within the same millisecond, the + * random part is incremented by one. + * + * The random part is reset to a new value every time the millisecond part + * changes. + * + * ### Specification of Universally Unique Lexicographically Sortable ID + * + * #### Components + * + * ##### Timestamp + * + * It is a 48 bit integer. UNIX-time in milliseconds. Won't run out of space + * 'til the year 10889 AD. + * + * ##### Randomness + * + * It is a 80 bits integer. Cryptographically secure source of randomness, + * if possible. + * + * #### Sorting + * + * The left-most character must be sorted first, and the right-most + * character sorted last (lexical order). The default ASCII character set + * must be used. Within the same millisecond, sort order is not guaranteed. + * + * #### Monotonicity + * + * When generating a ULID within the same millisecond, we can provide some + * guarantees regarding sort order. Namely, if the same millisecond is + * detected, the random component is incremented by 1 bit in the least + * significant bit position (with carrying). + * + * If, in the extremely unlikely event that, you manage to generate more + * than 280 ULIDs within the same millisecond, or cause the random component + * to overflow with less, the generation will fail. + * + * @return {@link UUID} a UUID value + * + * @throws UlidCreatorException + * an overflow exception if too many requests within the same + * millisecond causes an overflow when incrementing the random + * bits of the GUID. + */ + public synchronized UUID create() { + + final long timestamp = this.getTimestamp(); + + final long msb = (timestamp << 16) | high; + final long lsb = low; + + return new UUID(msb, lsb); + } + + /** + * Return the current timestamp and resets or increments the random part. + * + * @return timestamp + */ + protected synchronized long getTimestamp() { + + final long timestamp = this.timestampStrategy.getTimestamp(); + + if (timestamp == this.previousTimestamp) { + this.increment(); + } else { + this.reset(); + } + + this.previousTimestamp = timestamp; + return timestamp; + } + + /** + * Reset the random part of the GUID. + */ + protected synchronized void reset() { + if (random == null) { + this.low = SecureRandomLazyHolder.INSTANCE.nextLong(); + this.high = SecureRandomLazyHolder.INSTANCE.nextLong() & MAX_HIGH; + } else { + this.low = random.nextLong(); + this.high = random.nextLong() & MAX_HIGH; + } + } + + /** + * Increment the random part of the GUID. + * + * @throws UlidCreatorException + * if an overflow happens. + */ + protected synchronized void increment() { + if ((this.low++ == MAX_LOW) && (this.high++ == MAX_HIGH)) { + this.high = 0L; + // Too many requests + if (enableOverflowException) { + throw new LexicalOrderGuidException(OVERFLOW_MESSAGE); + } + } + } + + /** + * Used for changing the timestamp strategy. + * + * @param timestampStrategy + * a timestamp strategy + * @return {@link LexicalOrderGuidCreator} + */ + @SuppressWarnings("unchecked") + public synchronized T withTimestampStrategy( + TimestampStrategy timestampStrategy) { + this.timestampStrategy = timestampStrategy; + return (T) this; + } + + /** + * Replace the default random generator, in a fluent way, to another that + * extends {@link Random}. + * + * The default random generator is {@link java.security.SecureRandom}. + * + * For other faster pseudo-random generators, see {@link XorshiftRandom} and + * its variations. + * + * See {@link Random}. + * + * @param random + * a random generator + * @return {@link LexicalOrderGuidCreator} + */ + @SuppressWarnings("unchecked") + public synchronized T withRandomGenerator(Random random) { + this.random = random; + return (T) this; + } + + /** + * Replaces the default random generator with a faster one. + * + * The host fingerprint is used to generate a seed for the random number + * generator. + * + * See {@link Xorshift128PlusRandom} and + * {@link FingerprintUtil#getFingerprint()} + * + * @return {@link LexicalOrderGuidCreator} + */ + @SuppressWarnings("unchecked") + public synchronized T withFastRandomGenerator() { + this.random = new Xorshift128PlusRandom(); + return (T) this; + } + + /** + * Used to disable the overflow exception. + * + * An exception thrown when too many requests within the same millisecond + * causes an overflow while incrementing the random bits of the GUID. + * + * @return {@link LexicalOrderGuidCreator} + */ + @SuppressWarnings("unchecked") + public synchronized T withoutOverflowException() { + this.enableOverflowException = false; + return (T) this; + } + + public static class LexicalOrderGuidException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public LexicalOrderGuidException(String message) { + super(message); + } + } + + private static class SecureRandomLazyHolder { + static final Random INSTANCE = new SecureRandom(); + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/random/Xorshift128PlusRandom.java b/src/main/java/com/github/f4b6a3/ulid/random/Xorshift128PlusRandom.java new file mode 100644 index 0000000..a9195ce --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/random/Xorshift128PlusRandom.java @@ -0,0 +1,80 @@ +/* + * 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.random; + +import java.util.Random; + +/** + * A subclass of {@link java.util.Random} that implements the Xorshift 128 Plus + * random number generator. + * + * https://en.wikipedia.org/wiki/Xorshift + * + */ +public class Xorshift128PlusRandom extends Random { + + private static final long serialVersionUID = -7271232011767476928L; + + long[] seed = new long[2]; + private static int count; + + public Xorshift128PlusRandom() { + this((int) System.nanoTime()); + } + + /** + * Constructor that receives an integer as 'salt'. This value is combined + * with the current milliseconds and the object hash code to generate two + * seeds. + * + * @param salt + * a number used to generate two seeds. + */ + public Xorshift128PlusRandom(int salt) { + long time = System.currentTimeMillis() + count++; + long hash = (long) this.hashCode(); + this.seed[0] = (((long) salt) << 32) | (time & 0x00000000ffffffffL); + this.seed[1] = (((long) salt) << 32) | (hash & 0x00000000ffffffffL); + } + + public Xorshift128PlusRandom(long[] seed) { + this.seed = seed; + } + + @Override + protected int next(int bits) { + return (int) (nextLong() >>> (64 - bits)); + } + + @Override + public long nextLong() { + long x = seed[0]; + final long y = seed[1]; + seed[0] = y; + x ^= x << 23; // a + seed[1] = x ^ y ^ (x >>> 17) ^ (y >>> 26); // b, c + return seed[1] + y; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategy.java b/src/main/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategy.java new file mode 100644 index 0000000..d802ec0 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategy.java @@ -0,0 +1,36 @@ +/* + * 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.timestamp; + +public class DefaultTimestampStrategy implements TimestampStrategy { + + /** + * Returns the count of milliseconds since 01-01-1970. + */ + @Override + public long getTimestamp() { + return System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/timestamp/FixedTimestampStretegy.java b/src/main/java/com/github/f4b6a3/ulid/timestamp/FixedTimestampStretegy.java new file mode 100644 index 0000000..0d568f6 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/timestamp/FixedTimestampStretegy.java @@ -0,0 +1,39 @@ +/* + * 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.timestamp; + +public class FixedTimestampStretegy implements TimestampStrategy { + + protected long timestamp = 0; + + public FixedTimestampStretegy(long timestamp) { + this.timestamp = timestamp; + } + + @Override + public long getTimestamp() { + return this.timestamp; + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/timestamp/TimestampStrategy.java b/src/main/java/com/github/f4b6a3/ulid/timestamp/TimestampStrategy.java new file mode 100644 index 0000000..57f42f9 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/timestamp/TimestampStrategy.java @@ -0,0 +1,29 @@ +/* + * 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.timestamp; + +public interface TimestampStrategy { + long getTimestamp(); +} diff --git a/src/main/java/com/github/f4b6a3/ulid/util/Base32Util.java b/src/main/java/com/github/f4b6a3/ulid/util/Base32Util.java new file mode 100644 index 0000000..8b075ae --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/Base32Util.java @@ -0,0 +1,507 @@ +/* + * 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.math.BigInteger; +import java.nio.charset.StandardCharsets; + +/** + * + * This class contain static methods for encoding to and from Base 32. + * + * Supported alphabets: base32, base32hex, zbase32 and crockford base32. + * + * RFC-4648: https://tools.ietf.org/html/rfc4648 + * + * Wikipedia: https://en.wikipedia.org/wiki/Base32 + * + */ +public class Base32Util { + + public static final String ALPHABET_BASE_32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + public static final String ALPHABET_BASE_32_HEX = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + public static final String ALPHABET_BASE_32_Z = "ybndrfg8ejkmcpqxot1uwisza345h769"; + public static final String ALPHABET_BASE_32_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + + private static final char[] _ALPHABET_BASE_32 = ALPHABET_BASE_32.toCharArray(); + private static final char[] _ALPHABET_BASE_32_HEX = ALPHABET_BASE_32_HEX.toCharArray(); + private static final char[] _ALPHABET_BASE_32_Z = ALPHABET_BASE_32_Z.toCharArray(); + private static final char[] _ALPHABET_BASE_32_CROCKFORD = ALPHABET_BASE_32_CROCKFORD.toCharArray(); + + private static final int[] PADDING_ENCODE = { 0, 6, 4, 3, 1 }; + private static final int[] PADDING_DECODE = { 0, 4, 4, 3, 3, 2, 2, 1 }; + + private static final int BASE = 32; + + private Base32Util() { + } + + // ---------------------- + // Base 32 + // ---------------------- + + public static String toBase32(long number) { + return encodeLong(number, _ALPHABET_BASE_32); + } + + public static String toBase32(BigInteger number) { + return encodeBigInteger(number, _ALPHABET_BASE_32); + } + + public static String toBase32(byte[] bytes) { + return encode(bytes, _ALPHABET_BASE_32, '='); + } + + public static String toBase32(String string) { + return toBase32(toBytes(string)); + } + + public static byte[] fromBase32(String string) { + return decode(normalize(string), _ALPHABET_BASE_32, '='); + } + + public static String fromBase32AsString(String string) { + return toString(fromBase32(string)); + } + + public static long fromBase32AsLong(String string) { + return decodeLong(normalize(string), _ALPHABET_BASE_32); + } + + public static BigInteger fromBase32AsBigInteger(String string) { + return decodeBigInteger(normalize(string), _ALPHABET_BASE_32); + } + + // ---------------------- + // Base 32 Hex + // ---------------------- + + public static String toBase32Hex(long number) { + return encodeLong(number, _ALPHABET_BASE_32_HEX); + } + + public static String toBase32Hex(BigInteger number) { + return encodeBigInteger(number, _ALPHABET_BASE_32_HEX); + } + + public static String toBase32Hex(byte[] bytes) { + return encode(bytes, _ALPHABET_BASE_32_HEX, '='); + } + + public static String toBase32Hex(String string) { + return toBase32Hex(toBytes(string)); + } + + public static byte[] fromBase32Hex(String string) { + return decode(normalize(string), _ALPHABET_BASE_32_HEX, '='); + } + + public static String fromBase32HexAsString(String string) { + return toString(fromBase32Hex(string)); + } + + public static long fromBase32HexAsLong(String string) { + return decodeLong(normalize(string), _ALPHABET_BASE_32_HEX); + } + + public static BigInteger fromBase32HexAsBigInteger(String string) { + return decodeBigInteger(normalize(string), _ALPHABET_BASE_32_HEX); + } + + // ---------------------- + // Base 32 Crockford + // ---------------------- + + public static String toBase32Crockford(long number) { + return encodeLong(number, _ALPHABET_BASE_32_CROCKFORD); + } + + public static String toBase32Crockford(BigInteger number) { + return encodeBigInteger(number, _ALPHABET_BASE_32_CROCKFORD); + } + + public static String toBase32Crockford(byte[] bytes) { + return encode(bytes, _ALPHABET_BASE_32_CROCKFORD, null); + } + + public static String toBase32Crockford(String string) { + return toBase32Crockford(toBytes(string)); + } + + public static byte[] fromBase32Crockford(String string) { + return decode(normalizeCrockford(string), _ALPHABET_BASE_32_CROCKFORD, null); + } + + public static String fromBase32CrockfordAsString(String string) { + return toString(fromBase32Crockford(string)); + } + + public static long fromBase32CrockfordAsLong(String string) { + return decodeLong(normalizeCrockford(string), _ALPHABET_BASE_32_CROCKFORD); + } + + public static BigInteger fromBase32CrockfordAsBigInteger(String string) { + return decodeBigInteger(normalizeCrockford(string), _ALPHABET_BASE_32_CROCKFORD); + } + + // ---------------------- + // Z Base 32 Z + // ---------------------- + + public static String toBase32Z(long number) { + return encodeLong(number, _ALPHABET_BASE_32_Z); + } + + public static String toBase32Z(BigInteger number) { + return encodeBigInteger(number, _ALPHABET_BASE_32_Z); + } + + public static String toBase32Z(byte[] bytes) { + return encode(bytes, _ALPHABET_BASE_32_Z, null); + } + + public static String toBase32Z(String string) { + return toBase32Z(toBytes(string)); + } + + public static byte[] fromBase32Z(String string) { + return decode(normalizeZ(string), _ALPHABET_BASE_32_Z, null); + } + + public static String fromBase32ZAsString(String string) { + return toString(fromBase32Z(string)); + } + + public static long fromBase32ZAsLong(String string) { + return decodeLong(normalizeZ(string), _ALPHABET_BASE_32_Z); + } + + public static BigInteger fromBase32ZAsBigInteger(String string) { + return decodeBigInteger(normalizeZ(string), _ALPHABET_BASE_32_Z); + } + + // ---------------------- + // Utils + // ---------------------- + + /** + * Convert a string to an array of bytes using UTF-8. + * + * @param string + * @return + */ + public static byte[] toBytes(String string) { + return string.getBytes(StandardCharsets.UTF_8); + } + + /** + * Convert an array of bytes to a string using UTF-8. + * + * @param bytes + * @return + */ + public static String toString(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + + // ---------------------- + // Encoder and Decoder + // ---------------------- + + /** + * Encode a long number to base 32 string. + * + * @param number + * a long number + * @param alphabet + * an alphabet + * @return a base32 encoded string + */ + public static String encodeLong(long number, char[] alphabet) { + + long n = number; + StringBuilder builder = new StringBuilder(); + + while (n > 0) { + builder.append(alphabet[(int) (n % BASE)]); + n = n / BASE; + } + + return builder.reverse().toString(); + } + + /** + * Encode a BigInteger to base 32 string. + * + * @param number + * a BigInteger + * @param alphabet + * an alphabet + * @return a base32 encoded string + */ + public static String encodeBigInteger(BigInteger number, char[] alphabet) { + + BigInteger n = number; + BigInteger b = BigInteger.valueOf(BASE); + + StringBuilder builder = new StringBuilder(); + + while (n.compareTo(BigInteger.ZERO) > 0) { + builder.append(alphabet[n.remainder(b).intValue()]); + n = n.divide(b); + } + + return builder.reverse().toString(); + } + + /** + * Decode a base 32 string to a long number. + * + * @param string + * a base 32 encoded string + * @param alphabet + * an alphabet + * @return a long number + */ + public static long decodeLong(String string, char[] alphabet) { + + long n = 0; + + for (char c : string.toCharArray()) { + int d = map(c, alphabet); + n = BASE * n + d; + } + + return n; + } + + /** + * Decode a base 32 string to a BigInteger. + * + * @param string + * a base 32 encoded string + * @param alphabet + * an alphabet + * @return a BigInteger + */ + public static BigInteger decodeBigInteger(String string, char[] alphabet) { + + BigInteger n = BigInteger.ZERO; + BigInteger b = BigInteger.valueOf(BASE); + + for (char c : string.toCharArray()) { + int d = map(c, alphabet); + n = b.multiply(n).add(BigInteger.valueOf(d)); + } + + return n; + } + + /** + * Encode an array of bytes into a base 32 string. + * + * @param bytes + * an array of bytes + * @param alphabet + * an alphabet + * @param padding + * a padding char, if necessary + * @return a base 32 encoded string + */ + public static String encode(byte[] bytes, char[] alphabet, Character padding) { + + if (bytes == null || bytes.length == 0) { + return ""; + } + + int div = bytes.length / 5; + int mod = bytes.length % 5; + + int blocks = (div + (mod == 0 ? 0 : 1)); + + byte[] input = new byte[5 * blocks]; + char[] output = new char[8 * blocks]; + + System.arraycopy(bytes, 0, input, 0, bytes.length); + + for (int i = 0; i < blocks; i++) { + byte[] blk = new byte[5]; + System.arraycopy(input, i * 5, blk, 0, 5); + + int[] out = new int[8]; + + out[0] = ((blk[0] & 0b11111000) >>> 3); + out[1] = ((blk[0] & 0b00000111) << 2) | ((blk[1] & 0b11000000) >>> 6); + out[2] = ((blk[1] & 0b00111110) >>> 1); + out[3] = ((blk[1] & 0b00000001) << 4) | ((blk[2] & 0b11110000) >>> 4); + out[4] = ((blk[2] & 0b00001111) << 1) | ((blk[3] & 0b10000000) >>> 7); + out[5] = ((blk[3] & 0b01111100) >>> 2); + out[6] = ((blk[3] & 0b00000011) << 3) | ((blk[4] & 0b11100000) >>> 5); + out[7] = (blk[4] & 0b00011111); + + char[] chars = new char[8]; + for (int j = 0; j < 8; j++) { + chars[j] = alphabet[out[j]]; + } + + System.arraycopy(chars, 0, output, i * 8, 8); + } + + int outputSize = output.length - PADDING_ENCODE[mod]; + if (padding != null) { + // Add padding: '=' + for (int i = outputSize; i < output.length; i++) { + output[i] = padding; + } + } else { + // Remove padding + char[] temp = new char[outputSize]; + System.arraycopy(output, 0, temp, 0, outputSize); + output = temp; + } + + return new String(output); + } + + /** + * Decode a base 32 string into an byte array. + * + * @param string + * a base 32 encoded string + * @param alphabet + * an alphabet + * @param padding + * a padding char, if necessary + * @return a byte array + */ + public static byte[] decode(String string, char[] alphabet, Character padding) { + + if (string == null || string.length() == 0) { + return new byte[0]; + } + + char[] chars = null; + char[] alph = alphabet; + + if (padding != null) { + chars = string.replaceAll(padding.toString(), "").toCharArray(); + } else { + chars = string.toCharArray(); + } + + validate(chars, alph); + + int div = chars.length / 8; + int mod = chars.length % 8; + int size = (div + (mod == 0 ? 0 : 1)); + + char[] input = new char[8 * size]; + byte[] output = new byte[5 * size]; + + System.arraycopy(chars, 0, input, 0, chars.length); + + for (int i = 0; i < size; i++) { + char[] blk = new char[8]; + System.arraycopy(input, i * 8, blk, 0, 8); + + byte[] out = new byte[5]; + + out[0] = (byte) ((map(blk[0], alph) << 3) | (map(blk[1], alph) >>> 2)); + out[1] = (byte) ((map(blk[1], alph) << 6) | (map(blk[2], alph) << 1) | (map(blk[3], alph) >>> 4)); + out[2] = (byte) ((map(blk[3], alph) << 4) | (map(blk[4], alph) >>> 1)); + out[3] = (byte) ((map(blk[4], alph) << 7) | (map(blk[5], alph) << 2) | (map(blk[6], alph) >>> 3)); + out[4] = (byte) ((map(blk[6], alph) << 5) | (map(blk[7], alph))); + + System.arraycopy(out, 0, output, i * 5, 5); + + } + + // Remove padding + int outputSize = output.length - PADDING_DECODE[mod]; + byte[] temp = new byte[outputSize]; + System.arraycopy(output, 0, temp, 0, outputSize); + output = temp; + + return output; + } + + protected static void validate(char[] chars, char[] alphabet) { + for (int i = 0; i < chars.length; i++) { + boolean found = false; + for (int j = 0; j < alphabet.length; j++) { + if (chars[i] == alphabet[j]) { + found = true; + break; + } + } + if (!found) { + throw new Base32UtilException("Invalid character"); + } + } + } + + private static int map(char c, char[] alphabet) { + for (int i = 0; i < alphabet.length; i++) { + if (alphabet[i] == c) { + return (byte) i; + } + } + return (byte) '0'; + } + + protected static String normalize(String string) { + if (string == null) { + return ""; + } + return string.toUpperCase(); + } + + protected static String normalizeZ(String string) { + if (string == null) { + return ""; + } + return string.toLowerCase(); + } + + protected static String normalizeCrockford(String string) { + if (string == null) { + return ""; + } + String normalized = string.toUpperCase(); + normalized = normalized.replaceAll("-", ""); + normalized = normalized.replaceAll("I", "1"); + normalized = normalized.replaceAll("L", "1"); + normalized = normalized.replaceAll("O", "0"); + return normalized; + } + + public static class Base32UtilException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public Base32UtilException(String message) { + super(message); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/f4b6a3/ulid/util/ByteUtil.java b/src/main/java/com/github/f4b6a3/ulid/util/ByteUtil.java new file mode 100644 index 0000000..25b03ab --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/ByteUtil.java @@ -0,0 +1,258 @@ +/* + * 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; + +/** + * Class that contains many static methods for byte handling. + */ +public class ByteUtil { + + private ByteUtil() { + } + + /** + * Get a number from a given hexadecimal string. + * + * @param hexadecimal a string + * @return a long + */ + public static long toNumber(final String hexadecimal) { + return toNumber(toBytes(hexadecimal)); + } + + /** + * Get a number from a given array of bytes. + * + * @param bytes a byte array + * @return a long + */ + public static long toNumber(final byte[] bytes) { + return toNumber(bytes, 0, bytes.length); + } + + public static long toNumber(final byte[] bytes, final int start, final int end) { + long result = 0; + for (int i = start; i < end; i++) { + result = (result << 8) | (bytes[i] & 0xff); + } + return result; + } + + /** + * Get an array of bytes from a given number. + * + * @param number a long value + * @return a byte array + */ + public static byte[] toBytes(final long number) { + return new byte[] { + (byte) (number >>> 56), + (byte) (number >>> 48), + (byte) (number >>> 40), + (byte) (number >>> 32), + (byte) (number >>> 24), + (byte) (number >>> 16), + (byte) (number >>> 8), + (byte) (number) + }; + } + + /** + * Get an array of bytes from a given hexadecimal string. + * + * @param hexadecimal a string + * @return a byte array + */ + public static byte[] toBytes(final String hexadecimal) { + final int length = hexadecimal.length(); + byte[] bytes = new byte[length / 2]; + for (int i = 0; i < length; i += 2) { + bytes[i / 2] = (byte) ((fromHexChar(hexadecimal.charAt(i)) << 4) | fromHexChar(hexadecimal.charAt(i + 1))); + } + return bytes; + } + + /** + * Get a hexadecimal string from given array of bytes. + * + * @param bytes byte array + * @return a string + */ + public static String toHexadecimal(final byte[] bytes) { + final int length = bytes.length; + final char[] hexadecimal = new char[length * 2]; + for (int i = 0; i < length; i++) { + final int v = bytes[i] & 0xFF; + hexadecimal[i * 2] = toHexChar(v >>> 4); + hexadecimal[(i * 2) + 1] = toHexChar(v & 0x0F); + } + return new String(hexadecimal); + } + + /** + * Get a hexadecimal string from given number. + * + * @param number an integer + * @return a string + */ + public static String toHexadecimal(final long number) { + return toHexadecimal(toBytes(number)); + } + + /** + * Get a number value from a hexadecimal char. + * + * @param chr a character + * @return an integer + */ + public static int fromHexChar(final char chr) { + + if (chr >= 0x61 && chr <= 0x66) { + // ASCII codes from 'a' to 'f' + return (int) chr - 0x57; + } else if (chr >= 0x41 && chr <= 0x46) { + // ASCII codes from 'A' to 'F' + return (int) chr - 0x37; + } else if (chr >= 0x30 && chr <= 0x39) { + // ASCII codes from 0 to 9 + return (int) chr - 0x30; + } + + return 0; + } + + /** + * Get a hexadecimal from a number value. + * + * @param number an integer + * @return a char + */ + public static char toHexChar(final int number) { + if (number >= 0x0a && number <= 0x0f) { + // ASCII codes from 'a' to 'f' + return (char) (0x57 + number); + } else if (number >= 0x00 && number <= 0x09) { + // ASCII codes from 0 to 9 + return (char) (0x30 + number); + } + return 0; + } + + /** + * Get a new array with a specific length and filled with a byte value. + * + * @param length array size + * @param value byte value + * @return a byte array + */ + public static byte[] array(final int length, final byte value) { + final byte[] result = new byte[length]; + for (int i = 0; i < length; i++) { + result[i] = value; + } + return result; + } + + /** + * Copy an entire array. + * + * @param bytes byte array + * @return a byte array + */ + public static byte[] copy(final byte[] bytes) { + return copy(bytes, 0, bytes.length); + } + + /** + * Copy part of an array. + * + * @param bytes byte array + * @param start start position + * @param end end position + * @return a byte array + */ + public static byte[] copy(final byte[] bytes, final int start, final int end) { + final int length = end - start; + final byte[] result = new byte[length]; + System.arraycopy(bytes, start, result, 0, length); + return result; + } + + /** + * Concatenates two byte arrays. + * + * @param bytes1 byte array 1 + * @param bytes2 byte array 2 + * @return a byte array + */ + public static byte[] concat(final byte[] bytes1, final byte[] bytes2) { + final byte[] result = new byte[bytes1.length + bytes2.length]; + System.arraycopy(bytes1, 0, result, 0, bytes1.length); + System.arraycopy(bytes2, 0, result, bytes1.length, bytes2.length); + return result; + } + + /** + * Replace part of an array of bytes with another subarray of bytes and + * starting from a given index. + * + * @param bytes byte array + * @param replacement replacement byte array + * @param index start position + * @return a byte array + */ + public static byte[] replace(final byte[] bytes, final byte[] replacement, final int index) { + + byte[] result = new byte[bytes.length]; + + for (int i = 0; i < index; i++) { + result[i] = bytes[i]; + } + + for (int i = 0; i < replacement.length; i++) { + result[index + i] = replacement[i]; + } + return result; + } + + /** + * Check if two arrays of bytes are equal. + * + * @param bytes1 byte array 1 + * @param bytes2 byte array 2 + * @return a boolean + */ + public static boolean equalArrays(final byte[] bytes1, final byte[] bytes2) { + if (bytes1.length != bytes2.length) { + return false; + } + for (int i = 0; i < bytes1.length; i++) { + if (bytes1[i] != bytes2[i]) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java new file mode 100644 index 0000000..ebf3ee0 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java @@ -0,0 +1,234 @@ +/* + * 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.time.Instant; +import java.util.UUID; + +public class UlidUtil { + + // Date: 10889-08-02T05:31:50.655Z + protected static final long TIMESTAMP_MAX = (long) Math.pow(2, 48) - 1; + + protected static final String ULID_PATTERN_STRICT = "^[0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{26}$"; + protected static final String ULID_PATTERN_LOOSE = "^[0-9a-tv-zA-TV-Z]{26}$"; + + private UlidUtil() { + } + + /** + * Convert a UUID to ULID string + * + * @param uuid + * a UUID + * @return a ULID + */ + public static String fromUuidToUlid(UUID uuid) { + // TODO: not working yet + // byte[] bytes = UuidUtil.fromUuidToBytes(uuid); + // return fromBytesToUlid(bytes); + return null; + } + + /** + * 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(String ulid) { + // TODO: not working yet + // byte[] bytes = fromUlidToBytes(ulid); + // return UuidUtil.fromBytesToUuid(bytes); + return null; + } + + /** + * 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); + long timeNumber = ByteUtil.toNumber(timeBytes); + String timestampComponent = leftPad(Base32Util.toBase32Crockford(timeNumber)); + + byte[] randBytes = new byte[10]; + System.arraycopy(bytes, 6, randBytes, 0, 10); + 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(String ulid) { + UlidUtil.validate(ulid); + byte[] bytes = new byte[16]; + + String timestampComponent = ulid.substring(0, 10); + long timeNumber = Base32Util.fromBase32CrockfordAsLong(timestampComponent); + byte[] timeBytes = ByteUtil.toBytes(timeNumber); + System.arraycopy(timeBytes, 2, bytes, 0, 6); + + 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 + * @return boolean true if valid + */ + 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 + * @return boolean true if valid + */ + protected static void validate(String ulid, boolean strict) { + if (!isValid(ulid, strict)) { + throw new UlidUtilException(String.format("Invalid ULID: %s.", ulid)); + } + } + + /** + * Checks if the string is a valid ULID. + * + * The validation mode is not strict. + * + * @see {@link UlidUtil#validate(String, boolean)}. + */ + public static boolean isValid(String ulid) { + return isValid(ulid, false); + } + + /** + * Checks if the string is a valid ULID. + * + *
+	 * Strict validation: checks if the string is in the ULID specification format:
+	 * 
+	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU)
+	 * 
+	 * Loose validation: checks if the string is in one of these formats:
+	 *
+	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, except uU)
+	 * 
+ * + * @param ulid + * a ULID + * @param strict + * true for strict validation, false for loose validation + * @return boolean true if valid + */ + public static boolean isValid(String ulid, boolean strict) { + + if (ulid == null || ulid.isEmpty()) { + return false; + } + + boolean matches = false; + + if (strict) { + matches = ulid.matches(ULID_PATTERN_STRICT); + } else { + String u = ulid.replaceAll("-", ""); + matches = u.matches(ULID_PATTERN_LOOSE); + } + + if (!matches) { + return false; + } + + long timestamp = extractUnixMilliseconds(ulid); + return timestamp >= 0 && timestamp <= TIMESTAMP_MAX; + } + + public static long extractTimestamp(String ulid) { + UlidUtil.validate(ulid); + return extractUnixMilliseconds(ulid); + } + + public static Instant extractInstant(String ulid) { + long milliseconds = extractTimestamp(ulid); + return Instant.ofEpochMilli(milliseconds); + } + + public static String extractTimestampComponent(String ulid) { + UlidUtil.validate(ulid); + return ulid.substring(0, 10); + } + + public static String extractRandomnessComponent(String ulid) { + UlidUtil.validate(ulid); + return ulid.substring(10, 26); + } + + protected static long extractUnixMilliseconds(String ulid) { + String milliseconds = ulid.substring(0, 10); + return Base32Util.fromBase32CrockfordAsLong(milliseconds); + } + + private static String leftPad(String unpadded) { + return "0000000000".substring(unpadded.length()) + unpadded; + } + + public static class UlidUtilException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public UlidUtilException(String message) { + super(message); + } + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java new file mode 100644 index 0000000..d4ec39b --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java @@ -0,0 +1,76 @@ +package com.github.f4b6a3.ulid; + +import org.junit.Test; + +import com.github.f4b6a3.ulid.UlidCreator; +import com.github.f4b6a3.ulid.util.UlidUtil; + +import static org.junit.Assert.*; +import java.util.Arrays; +import java.util.HashSet; + +public class UlidCreatorTest { + + private static final int ULID_LENGTH = 26; + private static final int DEFAULT_LOOP_MAX = 10_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(); + } + + long endTime = System.currentTimeMillis(); + + checkNullOrInvalid(list); + checkUniqueness(list); + checkOrdering(list); + checkCreationTime(list, startTime, endTime); + } + + private void checkNullOrInvalid(String[] list) { + for (String ulid : list) { + 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)); + } + } + + private void checkUniqueness(String[] list) { + + HashSet set = new HashSet<>(); + + for (String ulid : list) { + assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid)); + } + + assertTrue("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 = UlidUtil.extractTimestamp(ulid); + 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++) { + assertTrue("The ULID list is not ordered", list[i].equals(other[i])); + } + } + +} diff --git a/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorMock.java b/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorMock.java new file mode 100644 index 0000000..9f96741 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorMock.java @@ -0,0 +1,10 @@ +package com.github.f4b6a3.ulid.factory; + +class LexicalOrderGuidCreatorMock extends LexicalOrderGuidCreator { + public LexicalOrderGuidCreatorMock(long low, long high, long previousTimestamp) { + super(); + this.low = low; + this.high = high; + this.previousTimestamp = previousTimestamp; + } +} \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorTest.java new file mode 100644 index 0000000..9b9f8af --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorTest.java @@ -0,0 +1,136 @@ +package com.github.f4b6a3.ulid.factory; + +import java.util.Random; +import java.util.UUID; + +import org.junit.Test; + +import com.github.f4b6a3.ulid.exception.UlidCreatorException; +import com.github.f4b6a3.ulid.random.Xorshift128PlusRandom; +import com.github.f4b6a3.ulid.timestamp.FixedTimestampStretegy; + +import static org.junit.Assert.*; + +public class LexicalOrderGuidCreatorTest { + + private static final long DEFAULT_LOOP = 1000; + + private static final long TIMESTAMP = System.currentTimeMillis(); + private static final long MAX_LOW = LexicalOrderGuidCreator.MAX_LOW; + private static final long MAX_HIGH = LexicalOrderGuidCreator.MAX_HIGH; + + private static final Random RANDOM = new Xorshift128PlusRandom(); + + @Test + public void testRandomMostSignificantBits() { + + long low = RANDOM.nextInt(); + long high = RANDOM.nextInt(Short.MAX_VALUE); + + LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = creator.create(); + long firstMsb = (short) uuid.getMostSignificantBits(); + long lastMsb = 0; + for (int i = 0; i <= DEFAULT_LOOP; i++) { + uuid = creator.create(); + lastMsb = (short) uuid.getMostSignificantBits(); + } + + assertEquals(String.format("The last MSB should be iqual to the first %s.", firstMsb), firstMsb, lastMsb); + + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); + uuid = creator.create(); + lastMsb = (short) uuid.getMostSignificantBits(); + assertNotEquals("The last MSB should be be random after timestamp changed.", firstMsb, lastMsb); + } + + @Test + public void testRandomLeastSignificantBits() { + + LexicalOrderGuidCreator creator = new LexicalOrderGuidCreator(); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = creator.create(); + long firstLsb = uuid.getLeastSignificantBits(); + long lastLsb = 0; + for (int i = 0; i < DEFAULT_LOOP; i++) { + uuid = creator.create(); + lastLsb = uuid.getLeastSignificantBits(); + } + + long expected = firstLsb + DEFAULT_LOOP; + assertEquals(String.format("The last LSB should be iqual to %s.", expected), expected, lastLsb); + + long notExpected = firstLsb + DEFAULT_LOOP + 1; + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); + uuid = creator.create(); + lastLsb = uuid.getLeastSignificantBits(); + assertNotEquals("The last LSB should be random after timestamp changed.", notExpected, lastLsb); + } + + @Test + public void testIncrementOfRandomLeastSignificantBits() { + + long low = RANDOM.nextInt(); + long high = RANDOM.nextInt(Short.MAX_VALUE); + + LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = new UUID(0, 0); + for (int i = 0; i < DEFAULT_LOOP; i++) { + uuid = creator.create(); + } + + long expected = low + DEFAULT_LOOP; + long randomLsb = uuid.getLeastSignificantBits(); + assertEquals(String.format("The LSB should be iqual to %s.", expected), expected, randomLsb); + } + + @Test + public void testIncrementOfRandomMostSignificantBits() { + + long low = MAX_LOW; + long high = RANDOM.nextInt(Short.MAX_VALUE); + + LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = new UUID(0, 0); + for (int i = 0; i < DEFAULT_LOOP; i++) { + uuid = creator.create(); + } + + long expected = high + 1; + long randomMsb = uuid.getMostSignificantBits() & MAX_HIGH; + assertEquals(String.format("The MSB should be iqual to %s.", expected), expected, randomMsb); + } + + @Test(expected = UlidCreatorException.class) + public void testShouldThrowOverflowException() { + + long low = MAX_LOW - DEFAULT_LOOP; + long high = MAX_HIGH; + + LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + UUID uuid = new UUID(0, 0); + for (int i = 0; i < DEFAULT_LOOP; i++) { + uuid = creator.create(); + } + + long expected = MAX_LOW; + long randomLsb = uuid.getLeastSignificantBits(); + assertEquals(String.format("The LSB should be iqual to %s.", expected), expected, randomLsb); + + expected = MAX_HIGH; + long randomMsb = uuid.getMostSignificantBits() & MAX_HIGH; + assertEquals(String.format("The MSB should be iqual to %s.", expected), expected, randomMsb); + + creator.create(); + fail("It should throw an overflow exception."); + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/factory/TestSuite.java b/src/test/java/com/github/f4b6a3/ulid/factory/TestSuite.java new file mode 100644 index 0000000..5925970 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/factory/TestSuite.java @@ -0,0 +1,25 @@ +package com.github.f4b6a3.ulid.factory; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +import com.github.f4b6a3.ulid.UlidCreatorTest; +import com.github.f4b6a3.ulid.random.NaiveRandomTest; +import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategyTest; +import com.github.f4b6a3.ulid.util.Base32UtilTest; +import com.github.f4b6a3.ulid.util.ByteUtilTest; +import com.github.f4b6a3.ulid.util.UlidUtilTest; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + DefaultTimestampStrategyTest.class, + ByteUtilTest.class, + NaiveRandomTest.class, + LexicalOrderGuidCreatorTest.class, + Base32UtilTest.class, + UlidUtilTest.class, + UlidCreatorTest.class, +}) + +public class TestSuite { +} \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/ulid/random/NaiveRandomTest.java b/src/test/java/com/github/f4b6a3/ulid/random/NaiveRandomTest.java new file mode 100644 index 0000000..a6ccc88 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/random/NaiveRandomTest.java @@ -0,0 +1,28 @@ +package com.github.f4b6a3.ulid.random; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class NaiveRandomTest { + + private static final int DEFAULT_LOOP_LIMIT = 10_000; + private static final String EXPECTED_BIT_COUNT_RANDOM_LONG = "The average bit count expected for random long values is 32"; + + @Test + public void testXorshift128PlusNextLongNaiveAverageBitCount() { + + double accumulator = 0; + + Xorshift128PlusRandom random = new Xorshift128PlusRandom(); + + for (int i = 0; i < DEFAULT_LOOP_LIMIT; i++) { + long value = random.nextLong(); + accumulator += Long.bitCount(value); + } + + double average = Math.round(accumulator / DEFAULT_LOOP_LIMIT); + + assertTrue(EXPECTED_BIT_COUNT_RANDOM_LONG, average == 32); + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategyTest.java b/src/test/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategyTest.java new file mode 100644 index 0000000..b3d4902 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/timestamp/DefaultTimestampStrategyTest.java @@ -0,0 +1,5 @@ +package com.github.f4b6a3.ulid.timestamp; + +public class DefaultTimestampStrategyTest { + // TODO +} diff --git a/src/test/java/com/github/f4b6a3/ulid/util/Base32UtilTest.java b/src/test/java/com/github/f4b6a3/ulid/util/Base32UtilTest.java new file mode 100644 index 0000000..2ff3d31 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/util/Base32UtilTest.java @@ -0,0 +1,410 @@ +package com.github.f4b6a3.ulid.util; + +import static org.junit.Assert.*; + +import java.math.BigInteger; + +import org.junit.Test; + +import com.github.f4b6a3.ulid.util.Base32Util.Base32UtilException; + +public class Base32UtilTest { + + private static final String LONG_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras finibus fringilla sem, ac dictum dui mollis vitae. Suspendisse id euismod nunc. Curabitur mollis felis nec scelerisque eleifend. Curabitur eleifend, eros sed pellentesque egestas, dolor risus sodales sapien, ut finibus urna nisi id massa. Maecenas facilisis accumsan vestibulum. Fusce et sapien sed justo lacinia efficitur. Aenean libero mi, auctor nec rutrum at, tincidunt eu nisi. Curabitur urna quam, lobortis vel sem ullamcorper, dignissim varius metus. Proin congue nunc ut nisl hendrerit, vel euismod nunc auctor. Nunc metus arcu, lacinia ac dolor quis, dignissim tempor ante. Nunc condimentum tortor nec urna mattis venenatis. Vestibulum elit magna, sollicitudin eu iaculis at, faucibus eu lacus. Curabitur ultrices, elit non dictum vulputate, ligula elit egestas elit, vitae tincidunt lacus massa eu justo. Nam a velit ullamcorper, tincidunt tortor in, sodales velit. Suspendisse non mi quis lacus lacinia ultrices at at massa. Nullam tempus turpis duis."; + + private static final String LONG_TEXT_BASE_32 = "JRXXEZLNEBUXA43VNUQGI33MN5ZCA43JOQQGC3LFOQWCAY3PNZZWKY3UMV2HK4RAMFSGS4DJONRWS3THEBSWY2LUFYQEG4TBOMQGM2LONFRHK4ZAMZZGS3THNFWGYYJAONSW2LBAMFRSAZDJMN2HK3JAMR2WSIDNN5WGY2LTEB3GS5DBMUXCAU3VONYGK3TENFZXGZJANFSCAZLVNFZW233EEBXHK3TDFYQEG5LSMFRGS5DVOIQG233MNRUXGIDGMVWGS4ZANZSWGIDTMNSWYZLSNFZXC5LFEBSWYZLJMZSW4ZBOEBBXK4TBMJUXI5LSEBSWYZLJMZSW4ZBMEBSXE33TEBZWKZBAOBSWY3DFNZ2GK43ROVSSAZLHMVZXIYLTFQQGI33MN5ZCA4TJON2XGIDTN5SGC3DFOMQHGYLQNFSW4LBAOV2CAZTJNZUWE5LTEB2XE3TBEBXGS43JEBUWIIDNMFZXGYJOEBGWCZLDMVXGC4ZAMZQWG2LMNFZWS4ZAMFRWG5LNONQW4IDWMVZXI2LCOVWHK3JOEBDHK43DMUQGK5BAONQXA2LFNYQHGZLEEBVHK43UN4QGYYLDNFXGSYJAMVTGM2LDNF2HK4ROEBAWK3TFMFXCA3DJMJSXE3ZANVUSYIDBOVRXI33SEBXGKYZAOJ2XI4TVNUQGC5BMEB2GS3TDNFSHK3TUEBSXKIDONFZWSLRAIN2XEYLCNF2HK4RAOVZG4YJAOF2WC3JMEBWG6YTPOJ2GS4ZAOZSWYIDTMVWSA5LMNRQW2Y3POJYGK4RMEBSGSZ3ONFZXG2LNEB3GC4TJOVZSA3LFOR2XGLRAKBZG62LOEBRW63THOVSSA3TVNZRSA5LUEBXGS43MEBUGK3TEOJSXE2LUFQQHMZLMEBSXK2LTNVXWIIDOOVXGGIDBOVRXI33SFYQE45LOMMQG2ZLUOVZSAYLSMN2SYIDMMFRWS3TJMEQGCYZAMRXWY33SEBYXK2LTFQQGI2LHNZUXG43JNUQHIZLNOBXXEIDBNZ2GKLRAJZ2W4YZAMNXW4ZDJNVSW45DVNUQHI33SORXXEIDOMVRSA5LSNZQSA3LBOR2GS4ZAOZSW4ZLOMF2GS4ZOEBLGK43UNFRHK3DVNUQGK3DJOQQG2YLHNZQSYIDTN5WGY2LDNF2HKZDJNYQGK5JANFQWG5LMNFZSAYLUFQQGMYLVMNUWE5LTEBSXKIDMMFRXK4ZOEBBXK4TBMJUXI5LSEB2WY5DSNFRWK4ZMEBSWY2LUEBXG63RAMRUWG5DVNUQHM5LMOB2XIYLUMUWCA3DJM52WYYJAMVWGS5BAMVTWK43UMFZSAZLMNF2CYIDWNF2GCZJAORUW4Y3JMR2W45BANRQWG5LTEBWWC43TMEQGK5JANJ2XG5DPFYQE4YLNEBQSA5TFNRUXIIDVNRWGC3LDN5ZHAZLSFQQHI2LOMNUWI5LOOQQHI33SORXXEIDJNYWCA43PMRQWYZLTEB3GK3DJOQXCAU3VONYGK3TENFZXGZJANZXW4IDNNEQHC5LJOMQGYYLDOVZSA3DBMNUW42LBEB2WY5DSNFRWK4ZAMF2CAYLUEBWWC43TMEXCATTVNRWGC3JAORSW24DVOMQHI5LSOBUXGIDEOVUXGLQ="; + private static final String LONG_TEXT_BASE_32_HEX = "9HNN4PBD41KN0SRLDKG68RRCDTP20SR9EGG62RB5EGM20ORFDPPMAORKCLQ7ASH0C5I6IS39EDHMIRJ741IMOQBK5OG46SJ1ECG6CQBED5H7ASP0CPP6IRJ7D5M6OO90EDIMQB10C5HI0P39CDQ7AR90CHQMI83DDTM6OQBJ41R6IT31CKN20KRLEDO6ARJ4D5PN6P90D5I20PBLD5PMQRR441N7ARJ35OG46TBIC5H6IT3LE8G6QRRCDHKN6836CLM6ISP0DPIM683JCDIMOPBID5PN2TB541IMOPB9CPIMSP1E411NASJ1C9KN8TBI41IMOPB9CPIMSP1C41IN4RRJ41PMAP10E1IMOR35DPQ6ASRHELII0PB7CLPN8OBJ5GG68RRCDTP20SJ9EDQN683JDTI62R35ECG76OBGD5IMSB10ELQ20PJ9DPKM4TBJ41QN4RJ141N6ISR941KM883DC5PN6O9E416M2PB3CLN62SP0CPGM6QBCD5PMISP0C5HM6TBDEDGMS83MCLPN8QB2ELM7AR9E4137ASR3CKG6AT10EDGN0QB5DOG76PB441L7ASRKDSG6OOB3D5N6IO90CLJ6CQB3D5Q7ASHE410MARJ5C5N20R39C9IN4RP0DLKIO831ELHN8RRI41N6AOP0E9QN8SJLDKG62T1C41Q6IRJ3D5I7ARJK41INA83ED5PMIBH08DQN4OB2D5Q7ASH0ELP6SO90E5QM2R9C41M6UOJFE9Q6ISP0EPIMO83JCLMI0TBCDHGMQORFE9O6ASHC41I6IPRED5PN6QBD41R62SJ9ELPI0RB5EHQN6BH0A1P6UQBE41HMURJ7ELII0RJLDPHI0TBK41N6ISRC41K6ARJ4E9IN4QBK5GG7CPBC41INAQBJDLNM883EELN66831ELHN8RRI5OG4STBECCG6QPBKELPI0OBICDQIO83CC5HMIRJ9C4G62OP0CHNMORRI41ONAQBJ5GG68QB7DPKN6SR9DKG78PBDE1NN4831DPQ6ABH09PQMSOP0CDNMSP39DLIMST3LDKG78RRIEHNN483ECLHI0TBIDPGI0RB1EHQ6ISP0EPIMSPBEC5Q6ISPE41B6ASRKD5H7AR3LDKG6AR39EGG6QOB7DPGIO83JDTM6OQB3D5Q7AP39DOG6AT90D5GM6TBCD5PI0OBK5GG6COBLCDKM4TBJ41INA83CC5HNASPE411NASJ1C9KN8TBI41QMOT3ID5HMASPC41IMOQBK41N6URH0CHKM6T3LDKG7CTBCE1QN8OBKCKM20R39CTQMOO90CLM6IT10CLJMASRKC5PI0PBCD5Q2O83MD5Q62P90EHKMSOR9CHQMST10DHGM6TBJ41MM2SRJC4G6AT90D9QN6T3F5OG4SOBD41GI0TJ5DHKN883LDHM62RB3DTP70PBI5GG78QBECDKM8TBEEGG78RRIEHNN4839DOM20SRFCHGMOPBJ41R6AR39EGN20KRLEDO6ARJ4D5PN6P90DPNMS83DD4G72TB9ECG6OOB3ELPI0R31CDKMSQB141QMOT3ID5HMASP0C5Q20OBK41MM2SRJC4N20JJLDHM62R90EHIMQS3LECG78TBIE1KN6834ELKN6BG="; + private static final String LONG_TEXT_BASE_32_Z = "jtzzr3mprbwzyh5ipwoge55cp73nyh5jqoogn5mfqosnya5xp33ska5wci48khtycf1g1hdjqpts15u8rb1sa4mwfaorghubqcogc4mqpft8kh3yc33g15u8pfsgaajyqp1s4mbycft1y3djcp48k5jyct4s1edpp7sga4murb5g17dbcwznyw5iqpagk5urpf3zg3jypf1ny3mipf3s455rrbz8k5udfaorg7m1cftg17diqeog455cptwzgedgcisg1h3yp31sgeducp1sa3m1pf3zn7mfrb1sa3mjc31sh3bqrbbzkhubcjwze7m1rb1sa3mjc31sh3bcrb1zr55urb3sk3byqb1sa5dfp34gkh5tqi11y3m8ci3zeamufooge55cp73nyhujqp4zgedup71gn5dfqco8gamopf1shmbyqi4ny3ujp3wsr7murb4zr5ubrbzg1h5jrbwseedpcf3zgajqrbgsn3mdcizgnh3yc3osg4mcpf3s1h3ycftsg7mpqposhedsci3ze4mnqis8k5jqrbd8kh5dcwogk7byqpozy4mfpao8g3mrrbi8kh5wphogaamdpfzg1ajyciugc4mdpf48khtqrbysk5ufcfzny5djcj1zr53ypiw1aedbqitze551rbzgka3yqj4zehuipwogn7bcrb4g15udpf18k5uwrb1zkedqpf3s1mtyep4zramnpf48khtyqi3ghajyqf4sn5jcrbsg6auxqj4g1h3yq31saeducis1y7mcptos4a5xqjagkhtcrb1g135qpf3zg4mprb5gnhujqi31y5mfqt4zgmtykb3g64mqrbts65u8qi11y5uip3t1y7mwrbzg1h5crbwgk5urqj1zr4mwfoo8c3mcrb1zk4mupizseedqqizggedbqitze551faorh7mqccog43mwqi31yam1cp41aedccfts15ujcrogna3yctzsa551rbazk4mufooge4m8p3wzgh5jpwo8e3mpqbzzredbp34gkmtyj34sha3ycpzsh3djpi1sh7dipwo8e551qtzzredqcit1y7m1p3o1y5mbqt4g1h3yq31sh3mqcf4g1h3qrbmgkh5wpft8k5dipwogk5djqoog4am8p3o1aedup7sga4mdpf48k3djpaogk7jypfosg7mcpf31yamwfoogcamicpwsr7murb1zkedccftzkh3qrbbzkhubcjwze7m1rb4sa7d1pftskh3crb1sa4mwrbzg65tyctwsg7dipwo8c7mcqb4zeamwcwsny5djc74saajycisg17byciuskh5wcf31y3mcpf4naedspf4gn3jyqtwsha5jct4sh7byptosg7murbssnh5ucrogk7jypj4zg7dxfaorhamprbo1y7ufptwzeediptsgn5mdp738y3m1foo8e4mqcpwse7mqqoo8e551qtzzredjpasnyh5xctosa3murb5gk5djqoznyw5iqpagk5urpf3zg3jyp3zshedppro8n7mjqcogaamdqi31y5dbcpwsh4mbrb4sa7d1pftskh3ycf4nyamwrbssnh5ucrznyuuiptsgn5jyqt1s4hdiqco8e7m1qbwzgedrqiwzgmo"; + private static final String LONG_TEXT_BASE_32_CROCKFORD = "9HQQ4SBD41MQ0WVNDMG68VVCDXS20WV9EGG62VB5EGP20RVFDSSPARVMCNT7AWH0C5J6JW39EDHPJVK741JPRTBM5RG46WK1ECG6CTBED5H7AWS0CSS6JVK7D5P6RR90EDJPTB10C5HJ0S39CDT7AV90CHTPJ83DDXP6RTBK41V6JX31CMQ20MVNEDR6AVK4D5SQ6S90D5J20SBND5SPTVV441Q7AVK35RG46XBJC5H6JX3NE8G6TVVCDHMQ6836CNP6JWS0DSJP683KCDJPRSBJD5SQ2XB541JPRSB9CSJPWS1E411QAWK1C9MQ8XBJ41JPRSB9CSJPWS1C41JQ4VVK41SPAS10E1JPRV35DST6AWVHENJJ0SB7CNSQ8RBK5GG68VVCDXS20WK9EDTQ683KDXJ62V35ECG76RBGD5JPWB10ENT20SK9DSMP4XBK41TQ4VK141Q6JWV941MP883DC5SQ6R9E416P2SB3CNQ62WS0CSGP6TBCD5SPJWS0C5HP6XBDEDGPW83PCNSQ8TB2ENP7AV9E4137AWV3CMG6AX10EDGQ0TB5DRG76SB441N7AWVMDWG6RRB3D5Q6JR90CNK6CTB3D5T7AWHE410PAVK5C5Q20V39C9JQ4VS0DNMJR831ENHQ8VVJ41Q6ARS0E9TQ8WKNDMG62X1C41T6JVK3D5J7AVKM41JQA83ED5SPJBH08DTQ4RB2D5T7AWH0ENS6WR90E5TP2V9C41P6YRKFE9T6JWS0ESJPR83KCNPJ0XBCDHGPTRVFE9R6AWHC41J6JSVED5SQ6TBD41V62WK9ENSJ0VB5EHTQ6BH0A1S6YTBE41HPYVK7ENJJ0VKNDSHJ0XBM41Q6JWVC41M6AVK4E9JQ4TBM5GG7CSBC41JQATBKDNQP883EENQ66831ENHQ8VVJ5RG4WXBECCG6TSBMENSJ0RBJCDTJR83CC5HPJVK9C4G62RS0CHQPRVVJ41RQATBK5GG68TB7DSMQ6WV9DMG78SBDE1QQ4831DST6ABH09STPWRS0CDQPWS39DNJPWX3NDMG78VVJEHQQ483ECNHJ0XBJDSGJ0VB1EHT6JWS0ESJPWSBEC5T6JWSE41B6AWVMD5H7AV3NDMG6AV39EGG6TRB7DSGJR83KDXP6RTB3D5T7AS39DRG6AX90D5GP6XBCD5SJ0RBM5GG6CRBNCDMP4XBK41JQA83CC5HQAWSE411QAWK1C9MQ8XBJ41TPRX3JD5HPAWSC41JPRTBM41Q6YVH0CHMP6X3NDMG7CXBCE1TQ8RBMCMP20V39CXTPRR90CNP6JX10CNKPAWVMC5SJ0SBCD5T2R83PD5T62S90EHMPWRV9CHTPWX10DHGP6XBK41PP2WVKC4G6AX90D9TQ6X3F5RG4WRBD41GJ0XK5DHMQ883NDHP62VB3DXS70SBJ5GG78TBECDMP8XBEEGG78VVJEHQQ4839DRP20WVFCHGPRSBK41V6AV39EGQ20MVNEDR6AVK4D5SQ6S90DSQPW83DD4G72XB9ECG6RRB3ENSJ0V31CDMPWTB141TPRX3JD5HPAWS0C5T20RBM41PP2WVKC4Q20KKNDHP62V90EHJPTW3NECG78XBJE1MQ6834ENMQ6BG"; + + private static final String SHORT_TEXT = "Lorem ipsum dolor sit volutpat."; + private static final String SHORT_TEXT_BASE_32 = "JRXXEZLNEBUXA43VNUQGI33MN5ZCA43JOQQHM33MOV2HAYLUFY======"; + private static final String SHORT_TEXT_BASE_32_HEX = "9HNN4PBD41KN0SRLDKG68RRCDTP20SR9EGG7CRRCELQ70OBK5O======"; + private static final String SHORT_TEXT_BASE_32_Z = "jtzzr3mprbwzyh5ipwoge55cp73nyh5jqoo8c55cqi48yamwfa"; + private static final String SHORT_TEXT_BASE_32_CROCKFORD = "9HQQ4SBD41MQ0WVNDMG68VVCDXS20WV9EGG7CVVCENT70RBM5R"; + + private static final String[] WORDS = { "", "f", "fo", "foo", "foob", "fooba", "foobar", "foobarc", "foobarcp", + "foobarcpt", "foobarcpto" }; + + private static final String[] WORDS_BASE_32 = { "", "MY======", "MZXQ====", "MZXW6===", "MZXW6YQ=", "MZXW6YTB", + "MZXW6YTBOI======", "MZXW6YTBOJRQ====", "MZXW6YTBOJRXA===", "MZXW6YTBOJRXA5A=", "MZXW6YTBOJRXA5DP" }; + + private static final String[] WORDS_BASE_32_HEX = { "", "CO======", "CPNG====", "CPNMU===", "CPNMUOG=", "CPNMUOJ1", + "CPNMUOJ1E8======", "CPNMUOJ1E9HG====", "CPNMUOJ1E9HN0===", "CPNMUOJ1E9HN0T0=", "CPNMUOJ1E9HN0T3F" }; + + private static final String[] WORDS_BASE_32_Z = { "", "ca", "c3zo", "c3zs6", "c3zs6ao", "c3zs6aub", "c3zs6aubqe", + "c3zs6aubqjto", "c3zs6aubqjtzy", "c3zs6aubqjtzy7y", "c3zs6aubqjtzy7dx" }; + + private static final String[] WORDS_BASE_32_CROCKFORD = { "", "CR", "CSQG", "CSQPY", "CSQPYRG", "CSQPYRK1", + "CSQPYRK1E8", "CSQPYRK1E9HG", "CSQPYRK1E9HQ0", "CSQPYRK1E9HQ0X0", "CSQPYRK1E9HQ0X3F" }; + + private static final int[] NUMBERS = { 102685630, 725393777, 573697669, 614668535, 790665079, 728958755, 966150230, + 410015018, 605266173, 946077566, 214051168, 775737014, 723003700, 391609366, 147844737, 514081413, + 488279622, 550860813, 611087782, 223492126, 706308515, 158990768, 549042286, 26926303, 775714134, 602886016, + 27282100, 675097356, 641101167, 515280699, 454184468, 371424784, 633917378, 887459583, 792903202, 168552040, + 824806922, 696445335, 653338746, 357696553, 353677217, 972662902, 400738139, 537701151, 202077579, + 110209145, 356152341, 168702810, 684185451, 419840003, 480132486, 308833881, 997154252, 918202260, + 103304091, 328467776, 648729690, 733655121, 645189051, 342500864, 560919543, 509761384, 626871960, + 429248550, 319025067, 507317265, 348303729, 256009160, 660250872, 85224414, 414490625, 355994979, 318005886, + 326093128, 492813589, 569014099, 503350412, 168303553, 801566586, 800368918, 742601973, 395588591, + 257341245, 722366808, 501878988, 200718306, 184948029, 149469829, 992401543, 240364551, 976817281, + 161998068, 515579566, 275182272, 376045488, 899163436, 941443452, 974372015, 934795357, 958806784 }; + + private static final String[] NUMBERS_BASE_32 = { "DB5W56", "VTZILR", "RDD3UF", "SKGGHX", "XSBF3X", "VXGBZD", + "4ZMSCW", "MHAVJK", "SBHIH5", "4GH736", "GMEKLA", "XDZTVW", "VRQKJU", "LVO7AW", "EM73UB", "PKIQUF", + "ORVDSG", "QNK6AN", "SGY5NG", "GVEOA6", "VBS2ND", "EXUANQ", "QLTODO", "ZVXG7", "XDY5KW", "R66T4A", "2ASVU", + "UD2KYM", "TDM3LP", "PLNDZ3", "NRETQU", "LCG7QQ", "S4RT6C", "2OLDX7", "XUFPRC", "FAXZTI", "YSTDQK", + "UYF2MX", "TPCKD2", "KVEBBJ", "KRJL5B", "47TKDW", "L6FR23", "QAZKY7", "GAW5ML", "DJDKDZ", "KTU5AV", + "FA4M22", "UMPV3L", "MQMQAD", "OJ4PMG", "JGQ3SZ", "5W6XOM", "3LVJ4U", "DCQS43", "JZIBKA", "TKVVC2", + "V3VMCR", "THJTN3", "KGUJQA", "QW547X", "PGEV3I", "SV2TUY", "MZLUBG", "JQH35L", "PD2DAR", "KMFMLR", + "HUEY6I", "TVVIHY", "CRI266", "MLJIAB", "KTQDLD", "JPIYT6", "JW7SKI", "OV7PIV", "Q6U52T", "PABBEM", + "FAQG6B", "X4N332", "X3JKIW", "WEGNHV", "LZIM7P", "HVNNZ5", "VQ44KY", "O6UEGM", "F7NN7C", "FQMFJ5", + "EOROUF", "5SNWEH", "HFHLAH", "5DSDEB", "E2PZHU", "PLWHVO", "IGN4WA", "LGT75Q", "2ZQJJM", "4B2SL4", + "5BHPFP", "33PWC5", "4SMOYA" }; + + private static final String[] NUMBERS_BASE_32_HEX = { "31TMTU", "LJP8BH", "H33RK5", "IA667N", "NI15RN", "LN61P3", + "SPCI2M", "C70L9A", "I1787T", "S67VRU", "6C4AB0", "N3PJLM", "LHGA9K", "BLEV0M", "4CVRK1", "FA8GK5", + "EHL3I6", "GDAU0D", "I6OTD6", "6L4E0U", "L1IQD3", "4NK0DG", "GBJE3E", "PLN6V", "N3OTAM", "HUUJS0", "Q0ILK", + "K3QAOC", "J3CRBF", "FBD3PR", "DH4JGK", "B26VGG", "ISHJU2", "QEB3NV", "NK5FH2", "50NPJ8", "OIJ3GA", + "KO5QCN", "JF2A3Q", "AL4119", "AH9BT1", "SVJA3M", "BU5HQR", "G0PAOV", "60MTCB", "393A3P", "AJKT0L", + "50SCQQ", "KCFLRB", "CGCG03", "E9SFC6", "96GRIP", "TMUNEC", "RBL9SK", "32GISR", "9P81A0", "JALL2Q", + "LRLC2H", "J79JDR", "A6K9G0", "GMTSVN", "F64LR8", "ILQJKO", "CPBK16", "9G7RTB", "F3Q30H", "AC5CBH", + "7K4OU8", "JLL87O", "2H8QUU", "CB9801", "AJG3B3", "9F8OJU", "9MVIA8", "ELVF8L", "GUKTQJ", "F0114C", + "50G6U1", "NSDRRQ", "NR9A8M", "M46D7L", "BP8CVF", "7LDDPT", "LGSSAO", "EUK46C", "5VDDV2", "5GC59T", + "4EHEK5", "TIDM47", "757B07", "T3I341", "4QFP7K", "FBM7LE", "86DSM0", "B6JVTG", "QPG99C", "S1QIBS", + "T17F5F", "RRFM2T", "SICEO0" }; + + private static final String[] NUMBERS_BASE_32_Z = { "db7s76", "iu3emt", "tdd5wf", "1kgg8z", "z1bf5z", "izgb3d", + "h3c1ns", "c8yijk", "1b8e87", "hg8956", "gcrkmy", "zd3uis", "itokjw", "miq9ys", "rc95wb", "xkeowf", + "qtid1g", "opk6yp", "1ga7pg", "girqy6", "ib14pd", "rzwypo", "omuqdq", "3izg9", "zda7ks", "t66uhy", "4y1iw", + "wd4kac", "udc5mx", "xmpd35", "ptruow", "mng9oo", "1htu6n", "4qmdz9", "zwfxtn", "fyz3ue", "a1udok", + "waf4cz", "uxnkd4", "kirbbj", "ktjm7b", "h9ukds", "m6ft45", "oy3ka9", "gys7cm", "djdkd3", "kuw7yi", + "fyhc44", "wcxi5m", "cocoyd", "qjhxcg", "jgo513", "7s6zqc", "5mijhw", "dno1h5", "j3ebky", "ukiin4", + "i5icnt", "u8jup5", "kgwjoy", "os7h9z", "xgri5e", "1i4uwa", "c3mwbg", "jo857m", "xd4dyt", "kcfcmt", + "8wra6e", "uiie8a", "nte466", "cmjeyb", "kuodmd", "jxeau6", "js91ke", "qi9xei", "o6w74u", "xybbrc", + "fyog6b", "zhp554", "z5jkes", "srgp8i", "m3ec9x", "8ipp37", "iohhka", "q6wrgc", "f9pp9n", "focfj7", + "rqtqwf", "71psr8", "8f8my8", "7d1drb", "r4x38w", "xms8iq", "egphsy", "mgu97o", "43ojjc", "hb41mh", + "7b8xfx", "55xsn7", "h1cqay" }; + + private static final String[] NUMBERS_BASE_32_CROCKFORD = { "31XPXY", "NKS8BH", "H33VM5", "JA667Q", "QJ15VQ", + "NQ61S3", "WSCJ2P", "C70N9A", "J1787X", "W67ZVY", "6C4AB0", "Q3SKNP", "NHGA9M", "BNEZ0P", "4CZVM1", + "FA8GM5", "EHN3J6", "GDAY0D", "J6RXD6", "6N4E0Y", "N1JTD3", "4QM0DG", "GBKE3E", "SNQ6Z", "Q3RXAP", "HYYKW0", + "T0JNM", "M3TARC", "K3CVBF", "FBD3SV", "DH4KGM", "B26ZGG", "JWHKY2", "TEB3QZ", "QM5FH2", "50QSK8", "RJK3GA", + "MR5TCQ", "KF2A3T", "AN4119", "AH9BX1", "WZKA3P", "BY5HTV", "G0SARZ", "60PXCB", "393A3S", "AKMX0N", + "50WCTT", "MCFNVB", "CGCG03", "E9WFC6", "96GVJS", "XPYQEC", "VBN9WM", "32GJWV", "9S81A0", "KANN2T", + "NVNC2H", "K79KDV", "A6M9G0", "GPXWZQ", "F64NV8", "JNTKMR", "CSBM16", "9G7VXB", "F3T30H", "AC5CBH", + "7M4RY8", "KNN87R", "2H8TYY", "CB9801", "AKG3B3", "9F8RKY", "9PZJA8", "ENZF8N", "GYMXTK", "F0114C", + "50G6Y1", "QWDVVT", "QV9A8P", "P46D7N", "BS8CZF", "7NDDSX", "NGWWAR", "EYM46C", "5ZDDZ2", "5GC59X", + "4EHEM5", "XJDP47", "757B07", "X3J341", "4TFS7M", "FBP7NE", "86DWP0", "B6KZXG", "TSG99C", "W1TJBW", + "X17F5F", "VVFP2X", "WJCER0" }; + + private static final String INVALID_CHARS_BASE_32 = "!@#$%*()-_+|,.;:[]0189"; + private static final String INVALID_CHARS_BASE_32_HEX = "!@#$%*()-_+|,.;:[]XYZ"; + private static final String INVALID_CHARS_BASE_32_Z = "!@#$%*()-_+|,.;:[]iv2"; + private static final String INVALID_CHARS_BASE_32_CROCKFORD = "!@#$%*()-_+|,.;:[]ILOU"; + + @Test + public void testToBase32() { + String result = Base32Util.toBase32(LONG_TEXT); + assertEquals(LONG_TEXT_BASE_32.length(), result.length()); + assertTrue(LONG_TEXT_BASE_32.equals(result)); + + result = Base32Util.toBase32(SHORT_TEXT); + assertEquals(SHORT_TEXT_BASE_32.length(), result.length()); + assertTrue(SHORT_TEXT_BASE_32.equals(result)); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.toBase32(WORDS[i]); + assertEquals(WORDS_BASE_32[i].length(), result.length()); + assertTrue(WORDS_BASE_32[i].equals(result)); + } + } + + @Test + public void testFromBase32() { + String result = Base32Util.fromBase32AsString(LONG_TEXT_BASE_32); + assertEquals(LONG_TEXT.length(), result.length()); + assertTrue(LONG_TEXT.equals(result)); + + result = Base32Util.fromBase32AsString(SHORT_TEXT_BASE_32); + assertEquals(SHORT_TEXT.length(), result.length()); + assertTrue(SHORT_TEXT.equals(result)); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.fromBase32AsString(WORDS_BASE_32[i]); + assertEquals(WORDS[i].length(), result.length()); + assertTrue(WORDS[i].equals(result)); + } + + int count = 0; + for (int i = 0; i < INVALID_CHARS_BASE_32.length(); i++) { + try { + Base32Util.fromBase32AsString(SHORT_TEXT_BASE_32 + INVALID_CHARS_BASE_32.charAt(i)); + } catch (Base32UtilException e) { + count++; + } + } + assertEquals(INVALID_CHARS_BASE_32.length(), count); + + } + + @Test + public void testToBase32Hex() { + String result = Base32Util.toBase32Hex(LONG_TEXT); + assertTrue(LONG_TEXT_BASE_32_HEX.equals(result)); + assertEquals(LONG_TEXT_BASE_32_HEX.length(), result.length()); + + result = Base32Util.toBase32Hex(SHORT_TEXT); + assertTrue(SHORT_TEXT_BASE_32_HEX.equals(result)); + assertEquals(SHORT_TEXT_BASE_32_HEX.length(), result.length()); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.toBase32Hex(WORDS[i]); + assertEquals(WORDS_BASE_32_HEX[i].length(), result.length()); + assertTrue(WORDS_BASE_32_HEX[i].equals(result)); + } + } + + @Test + public void testFromBase32Hex() { + String result = Base32Util.fromBase32HexAsString(LONG_TEXT_BASE_32_HEX); + assertTrue(LONG_TEXT.equals(result)); + assertEquals(LONG_TEXT.length(), result.length()); + + result = Base32Util.fromBase32HexAsString(SHORT_TEXT_BASE_32_HEX); + assertEquals(SHORT_TEXT.length(), result.length()); + assertTrue(SHORT_TEXT.equals(result)); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.fromBase32HexAsString(WORDS_BASE_32_HEX[i]); + assertEquals(WORDS[i].length(), result.length()); + assertTrue(WORDS[i].equals(result)); + } + + int count = 0; + for (int i = 0; i < INVALID_CHARS_BASE_32_HEX.length(); i++) { + try { + Base32Util.fromBase32AsString(SHORT_TEXT_BASE_32_HEX + INVALID_CHARS_BASE_32_HEX.charAt(i)); + } catch (Base32UtilException e) { + count++; + } + } + assertEquals(INVALID_CHARS_BASE_32_HEX.length(), count); + } + + @Test + public void testToBase32Z() { + String result = Base32Util.toBase32Z(LONG_TEXT); + assertTrue(LONG_TEXT_BASE_32_Z.equals(result)); + assertEquals(LONG_TEXT_BASE_32_Z.length(), result.length()); + + result = Base32Util.toBase32Z(SHORT_TEXT); + assertEquals(SHORT_TEXT_BASE_32_Z.length(), result.length()); + assertTrue(SHORT_TEXT_BASE_32_Z.equals(result)); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.toBase32Z(WORDS[i]); + assertEquals(WORDS_BASE_32_Z[i].length(), result.length()); + assertTrue(WORDS_BASE_32_Z[i].equals(result)); + } + } + + @Test + public void testFromBase32Z() { + String result = Base32Util.fromBase32ZAsString(LONG_TEXT_BASE_32_Z); + assertTrue(LONG_TEXT.equals(result)); + assertEquals(LONG_TEXT.length(), result.length()); + + result = Base32Util.fromBase32ZAsString(SHORT_TEXT_BASE_32_Z); + assertTrue(SHORT_TEXT.equals(result)); + assertEquals(SHORT_TEXT.length(), result.length()); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.fromBase32ZAsString(WORDS_BASE_32_Z[i]); + assertEquals(WORDS[i].length(), result.length()); + assertTrue(WORDS[i].equals(result)); + } + + int count = 0; + for (int i = 0; i < INVALID_CHARS_BASE_32_Z.length(); i++) { + try { + Base32Util.fromBase32AsString(SHORT_TEXT_BASE_32_Z + INVALID_CHARS_BASE_32_Z.charAt(i)); + } catch (Base32UtilException e) { + count++; + } + } + assertEquals(INVALID_CHARS_BASE_32_Z.length(), count); + } + + @Test + public void testToBase32Crockford() { + String result = Base32Util.toBase32Crockford(LONG_TEXT); + assertEquals(LONG_TEXT_BASE_32_CROCKFORD.length(), result.length()); + assertTrue(LONG_TEXT_BASE_32_CROCKFORD.equals(result)); + + result = Base32Util.toBase32Crockford(SHORT_TEXT); + assertEquals(SHORT_TEXT_BASE_32_CROCKFORD.length(), result.length()); + assertTrue(SHORT_TEXT_BASE_32_CROCKFORD.equals(result)); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.toBase32Crockford(WORDS[i]); + assertEquals(WORDS_BASE_32_CROCKFORD[i].length(), result.length()); + assertTrue(WORDS_BASE_32_CROCKFORD[i].equals(result)); + } + } + + @Test + public void testFromBase32Crockford() { + String result = Base32Util.fromBase32CrockfordAsString(LONG_TEXT_BASE_32_CROCKFORD); + assertEquals(LONG_TEXT.length(), result.length()); + assertTrue(LONG_TEXT.equals(result)); + + result = Base32Util.fromBase32CrockfordAsString(SHORT_TEXT_BASE_32_CROCKFORD); + assertEquals(SHORT_TEXT.length(), result.length()); + assertTrue(SHORT_TEXT.equals(result)); + + for (int i = 0; i < WORDS.length; i++) { + result = Base32Util.fromBase32CrockfordAsString(WORDS_BASE_32_CROCKFORD[i]); + assertEquals(WORDS[i].length(), result.length()); + assertTrue(WORDS[i].equals(result)); + } + + int count = 0; + for (int i = 0; i < INVALID_CHARS_BASE_32_CROCKFORD.length(); i++) { + try { + Base32Util.fromBase32AsString(SHORT_TEXT_BASE_32_CROCKFORD + INVALID_CHARS_BASE_32_CROCKFORD.charAt(i)); + } catch (Base32UtilException e) { + count++; + } + } + assertEquals(INVALID_CHARS_BASE_32_CROCKFORD.length(), count); + } + + @Test + public void testToBase32AsNumber() { + String result = null; + + // Encode from long to base 32 + for (int i = 0; i < NUMBERS.length; i++) { + result = Base32Util.toBase32(NUMBERS[i]); + assertEquals(NUMBERS_BASE_32[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32[i], result); + } + + // Encode from BigInteger to base 32 + for (int i = 0; i < NUMBERS.length; i++) { + result = Base32Util.toBase32(BigInteger.valueOf((NUMBERS[i]))); + assertEquals(NUMBERS_BASE_32[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32[i], result); + } + + // Decode from base 32 to long + long number = 0; + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32AsLong((NUMBERS_BASE_32[i])); + assertEquals(NUMBERS[i], number); + } + + // Decode from base 32 to BigInteger + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32AsBigInteger((NUMBERS_BASE_32[i])).longValue(); + assertEquals(NUMBERS[i], number); + } + } + + @Test + public void testToBase32HexAsNumber() { + String result = null; + + // Encode from long to base 32 + for (int i = 0; i < NUMBERS_BASE_32_HEX.length; i++) { + result = Base32Util.toBase32Hex(NUMBERS[i]); + assertEquals(NUMBERS_BASE_32_HEX[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32_HEX[i], result); + } + + // Encode from BigInteger to base 32 + for (int i = 0; i < NUMBERS_BASE_32_HEX.length; i++) { + result = Base32Util.toBase32Hex(BigInteger.valueOf(NUMBERS[i])); + assertEquals(NUMBERS_BASE_32_HEX[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32_HEX[i], result); + } + + // Decode from base 32 to long + long number = 0; + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32HexAsLong((NUMBERS_BASE_32_HEX[i])); + assertEquals(NUMBERS[i], number); + } + + // Decode from base 32 to BigInteger + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32HexAsBigInteger((NUMBERS_BASE_32_HEX[i])).longValue(); + assertEquals(NUMBERS[i], number); + } + } + + @Test + public void testToBase32ZAsNumber() { + String result = null; + + // Encode from long to base 32 + for (int i = 0; i < NUMBERS.length; i++) { + result = Base32Util.toBase32Z(NUMBERS[i]); + assertEquals(NUMBERS_BASE_32_Z[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32_Z[i], result); + } + + // Encode from BigInteger to base 32 + for (int i = 0; i < NUMBERS.length; i++) { + result = Base32Util.toBase32Z(BigInteger.valueOf(NUMBERS[i])); + assertEquals(NUMBERS_BASE_32_Z[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32_Z[i], result); + } + + // Decode from base 32 to long + long number = 0; + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32ZAsLong((NUMBERS_BASE_32_Z[i])); + assertEquals(NUMBERS[i], number); + } + + // Decode from base 32 to BigInteger + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32ZAsBigInteger((NUMBERS_BASE_32_Z[i])).longValue(); + assertEquals(NUMBERS[i], number); + } + } + + @Test + public void testToBase32CrockfordAsNumber() { + String result = null; + + // Encode from long to base 32 + for (int i = 0; i < NUMBERS.length; i++) { + result = Base32Util.toBase32Crockford(NUMBERS[i]); + assertEquals(NUMBERS_BASE_32_CROCKFORD[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32_CROCKFORD[i], result); + } + + // Encode from BigInteger to base 32 + for (int i = 0; i < NUMBERS.length; i++) { + result = Base32Util.toBase32Crockford(BigInteger.valueOf(NUMBERS[i])); + assertEquals(NUMBERS_BASE_32_CROCKFORD[i].length(), result.length()); + assertEquals(NUMBERS_BASE_32_CROCKFORD[i], result); + } + + // Decode from base 32 to long + long number = 0; + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32CrockfordAsLong((NUMBERS_BASE_32_CROCKFORD[i])); + assertEquals(NUMBERS[i], number); + } + + // Decode from base 32 to BigInteger + for (int i = 0; i < NUMBERS.length; i++) { + number = Base32Util.fromBase32CrockfordAsBigInteger((NUMBERS_BASE_32_CROCKFORD[i])).longValue(); + assertEquals(NUMBERS[i], number); + } + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/util/ByteUtilTest.java b/src/test/java/com/github/f4b6a3/ulid/util/ByteUtilTest.java new file mode 100644 index 0000000..9e869f6 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/util/ByteUtilTest.java @@ -0,0 +1,145 @@ +package com.github.f4b6a3.ulid.util; + +import org.junit.Test; + +import static com.github.f4b6a3.ulid.util.ByteUtil.*; + +import static org.junit.Assert.*; + +public class ByteUtilTest { + + + private long[] numbers = { 0x0000000000000000L, 0x0000000000000001L, 0x0000000000000012L, 0x0000000000000123L, + 0x0000000000001234L, 0x0000000000012345L, 0x0000000000123456L, 0x0000000001234567L, 0x0000000012345678L, + 0x0000000123456789L, 0x000000123456789aL, 0x00000123456789abL, 0x0000123456789abcL, 0x000123456789abcdL, + 0x00123456789abcdeL, 0x0123456789abcdefL }; + + private String[] hexadecimals = { "0000000000000000", "0000000000000001", "0000000000000012", "0000000000000123", + "0000000000001234", "0000000000012345", "0000000000123456", "0000000001234567", "0000000012345678", + "0000000123456789", "000000123456789a", "00000123456789ab", "0000123456789abc", "000123456789abcd", + "00123456789abcde", "0123456789abcdef" }; + + private byte[][] bytes = { + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x12 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x23 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x23, (byte) 0x45 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34, (byte) 0x56 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67, (byte) 0x89 }, + { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78, (byte) 0x9a }, + { (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67, (byte) 0x89, (byte) 0xab }, + { (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78, (byte) 0x9a, (byte) 0xbc }, + { (byte) 0x00, (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67, (byte) 0x89, (byte) 0xab, (byte) 0xcd }, + { (byte) 0x00, (byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78, (byte) 0x9a, (byte) 0xbc, (byte) 0xde }, + { (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67, (byte) 0x89, (byte) 0xab, (byte) 0xcd, (byte) 0xef } + }; + + @Test + public void testConcat() { + + String string1 = "CONCA"; + String string2 = "TENATE"; + + byte[] bytes1 = (string1 + string2).getBytes(); + byte[] bytes2 = concat(string1.getBytes(), string2.getBytes()); + + assertEquals(bytes1.length, bytes2.length); + + for (int i = 0; i < bytes1.length; i++) { + if (bytes1[i] != bytes2[i]) { + fail(); + } + } + } + + @Test + public void testReplace() { + + byte[] bytes1 = bytes[0]; + byte[] bytes2 = {(byte) 0x01, (byte) 0x23 }; + + byte[] bytes3 = replace(bytes1, bytes2, 6); + + assertEquals(bytes[0].length, bytes3.length); + + for (int i = 0; i < bytes[0].length; i++) { + if (bytes3[i] != bytes[3][i]) { + fail(); + } + } + } + + @Test + public void testToBytes() { + + byte[] bytes1 = toBytes(numbers[15]); + + assertEquals(bytes[15].length, bytes1.length); + + for (int i = 0; i < bytes[15].length; i++) { + if (bytes1[i] != bytes[15][i]) { + fail(); + } + } + } + + @Test + public void testCopy() { + + byte[] bytes1 = copy(bytes[15]); + + assertEquals(bytes[15].length, bytes1.length); + + for (int i = 0; i < bytes[15].length; i++) { + if (bytes1[i] != bytes[15][i]) { + fail(); + } + } + } + + @Test + public void testArray() { + + byte[] bytes1 = array(bytes[0].length, bytes[0][0]); + + assertEquals(bytes[0].length, bytes1.length); + + for (int i = 0; i < bytes[0].length; i++) { + if (bytes1[i] != bytes[0][i]) { + fail(); + } + } + } + + @Test + public void testToNumberFromBytes() { + for (int i = 0; i < numbers.length; i++) { + assertEquals(numbers[i], toNumber(bytes[i])); + } + } + + @Test + public void testToBytesFromHexadecimals() { + for (int i = 0; i < bytes.length; i++) { + assertTrue(equalArrays(bytes[i], toBytes(hexadecimals[i]))); + } + } + + @Test + public void testToHexadecimalFromBytes() { + for (int i = 0; i < hexadecimals.length; i++) { + assertEquals(hexadecimals[i], toHexadecimal(bytes[i])); + } + } + + @Test + public void testToNumberFromHexadecimal() { + for (int i = 0; i < hexadecimals.length; i++) { + assertEquals(numbers[i], toNumber(hexadecimals[i])); + } + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java new file mode 100644 index 0000000..3e7e49a --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java @@ -0,0 +1,206 @@ +package com.github.f4b6a3.ulid.util; + +import static org.junit.Assert.*; +import java.time.Instant; +import java.util.UUID; + +import org.junit.Test; + +import com.github.f4b6a3.ulid.util.UlidUtil.UlidUtilException; +import com.github.f4b6a3.ulid.util.ByteUtil; + +public class UlidUtilTest { + + private static final String EXAMPLE_TIMESTAMP = "0123456789"; + private static final String EXAMPLE_RANDOMNESS = "ABCDEFGHJKMNPQRS"; + private static final String EXAMPLE_ULID = "0123456789ABCDEFGHJKMNPQRS"; + + private static final long TIMESTAMP_MAX = 281474976710655l; // 2^48 - 1 + + private static final int ULID_LENGTH = 26; + private static final int DEFAULT_LOOP_MAX = 10_000; + + private static final String[] EXAMPLE_DATES = { "1970-01-01T00:00:00.000Z", "1985-10-26T01:16:00.123Z", + "2001-09-09T01:46:40.456Z", "2020-01-15T14:30:33.789Z", "2038-01-19T03:14:07.321Z" }; + + @Test(expected = UlidUtilException.class) + public void testExtractTimestamp() { + + String ulid = "0000000000" + EXAMPLE_RANDOMNESS; + long milliseconds = UlidUtil.extractTimestamp(ulid); + assertEquals(0, milliseconds); + + ulid = "7ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; + milliseconds = UlidUtil.extractTimestamp(ulid); + assertEquals(TIMESTAMP_MAX, milliseconds); + + ulid = "8ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; + UlidUtil.extractTimestamp(ulid); + fail("Should throw exception: invalid ULID"); + } + + @Test + public void testExtractTimestampList() { + + String randomnessComponent = EXAMPLE_RANDOMNESS; + + for (String i : EXAMPLE_DATES) { + + long milliseconds = Instant.parse(i).toEpochMilli(); + + String timestampComponent = leftPad(Base32Util.toBase32Crockford(milliseconds)); + String ulid = timestampComponent + randomnessComponent; + long result = UlidUtil.extractTimestamp(ulid); + + assertEquals(milliseconds, result); + } + } + + @Test + public void testExtractInstant() { + + String randomnessComponent = EXAMPLE_RANDOMNESS; + + for (String i : EXAMPLE_DATES) { + + Instant instant = Instant.parse(i); + long milliseconds = Instant.parse(i).toEpochMilli(); + + byte[] bytes = new byte[6]; + System.arraycopy(ByteUtil.toBytes(milliseconds), 2, bytes, 0, 6); + + String timestampComponent = leftPad(Base32Util.toBase32Crockford(milliseconds)); + String ulid = timestampComponent + randomnessComponent; + Instant result = UlidUtil.extractInstant(ulid); + + assertEquals(instant, result); + } + } + + @Test + public void testExtractTimestampComponent() { + String ulid = EXAMPLE_ULID; + String expected = EXAMPLE_TIMESTAMP; + String result = UlidUtil.extractTimestampComponent(ulid); + assertEquals(expected, result); + } + + @Test + public void testExtractRandomnessComponent() { + String ulid = EXAMPLE_ULID; + String expected = EXAMPLE_RANDOMNESS; + String result = UlidUtil.extractRandomnessComponent(ulid); + 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)); + } + + // TODO + // @Test + // public void testToAndFromUlid() { + // + // for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + // + // // Use random values + // UUID uuid = UuidCreator.getFastRandom(); + // String ulid = UlidUtil.fromUuidToUlid(uuid); + // + // 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 result = UlidUtil.fromUlidToUuid(ulid); + // assertEquals("Result ULID is different from original ULID", uuid, + // result); + // + // } + // } + + private String leftPad(String unpadded) { + return "0000000000".substring(unpadded.length()) + unpadded; + } +}