From fe5ee67b891c13199fd7a0c67289d60b44213e73 Mon Sep 17 00:00:00 2001 From: azam Date: Sun, 8 Jan 2017 02:05:50 +0900 Subject: [PATCH] Be, and it is --- .settings.xml | 11 + .travis.yml | 34 +++ license | 21 ++ pom.xml | 139 ++++++++++ readme.md | 73 +++++ src/main/java/io/azam/ulidj/ULID.java | 315 ++++++++++++++++++++++ src/test/java/io/azam/ulidj/ULIDTest.java | 217 +++++++++++++++ 7 files changed, 810 insertions(+) create mode 100644 .settings.xml create mode 100644 .travis.yml create mode 100644 license create mode 100644 pom.xml create mode 100644 readme.md create mode 100644 src/main/java/io/azam/ulidj/ULID.java create mode 100644 src/test/java/io/azam/ulidj/ULIDTest.java diff --git a/.settings.xml b/.settings.xml new file mode 100644 index 0000000..7d3ffd8 --- /dev/null +++ b/.settings.xml @@ -0,0 +1,11 @@ + + + + ossrh + ${env.SONATYPE_USERNAME} + ${env.SONATYPE_PASSWORD} + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a43982e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +language: java +jdk: + - oraclejdk8 + - oraclejdk7 +install: + - mvn clean install --settings .settings.xml -DskipTests=true -Dmaven.javadoc.skip=true -Dgpg.skip -B -V +before_install: + - if [ ! -z "$GPG_SECRET_KEYS" ]; then echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --import; fi + - if [ ! -z "$GPG_OWNERTRUST" ]; then echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --import-ownertrust; fi +before_deploy: + - mvn clean package -P release --settings .settings.xml -DperformRelease=true -DskipTests=true -B -U +deploy: + - + provider: script + script: mvn deploy -P release --settings .settings.xml -DperformRelease=true -DskipTests=true -B -U + skip_cleanup: true + on: + repo: azam/ulidj + jdk: oraclejdk7 + - + provider: releases + api_key: "$GITHUB_OAUTH_TOKEN" + file_glob: true + file: + - "target/ulidj-*.jar" + - "target/*.pom" + - "target/*.asc" + - "target/*.sha1" + skip_cleanup: true + overwrite: true + on: + repo: azam/ulidj + jdk: oraclejdk7 + tags: true \ No newline at end of file diff --git a/license b/license new file mode 100644 index 0000000..71babb2 --- /dev/null +++ b/license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Azamshul Azizy + +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. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d1fb0d1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,139 @@ + + 4.0.0 + io.azam.ulidj + ulidj + 1.0.0 + ulidj + ULID (Universally Unique Lexicographically Sortable Identifier) generator and parser for Java. + https://github.com/azam/ulidj + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + https://github.com/azam/ulidj + azam + + + + 1 + azam + azamshul@gmail.com + azam.io + http://azam.io + + Geek + + +9 + + + + GitHub + https://github.com/azam/ulidj/issues + + + azam + http://azam.io + + + UTF-8 + + + + junit + junit + 4.12 + test + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + release + + false + + + ${env.GPG_EXECUTABLE} + ${env.GPG_PASSPHRASE} + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.0 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.6 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d6f7471 --- /dev/null +++ b/readme.md @@ -0,0 +1,73 @@ +# ulidj + +[![MIT licensed](https://img.shields.io/badge/license-mit-blue.svg)](https://raw.githubusercontent.com/azam/ulidj/master/license) +[![Travis CI](https://api.travis-ci.org/azam/ulidj.svg?branch=master)](https://travis-ci.org/azam/ulidj) + +ULID (Universally Unique Lexicographically Sortable Identifier) generator and parser for Java. + +Refer [alizain/ulid](https://github.com/alizain/ulid) for a more detailed ULID specification. + +## License + +``` +MIT License + +Copyright (c) 2016 Azamshul Azizy + +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. +``` + +## Maven + +Add the following tag to `dependencies` tag in your `pom.xml` file. + +```xml + + io.azam + ulidj + 1.0.0 + +``` + +## Usage + +ULID generation examples: + +```java +String ulid1 = ULID.random(); +String ulid2 = ULID.random(ThreadLocalRandom.current()); +String ulid3 = ULID.random(SecureRandom.newInstance("SHA1PRNG")); +byte[] entropy = new byte[] { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9 }; +String ulid4 = ULID.generate(System.currentTimeMillis(), entropy); +``` + +ULID parsing examples: + +```java +String ulid = "003JZ9J6G80123456789abcdef"; +assert ULID.isValid(ulid); +long ts = ULID.getTimestamp(ulid); +assert ts == 123456789000L; +byte[] entropy = ULID.getEntropy(ulid); +``` + +## Prior Art + +- [Lewiscowles1986/jULID](https://github.com/Lewiscowles1986/jULID) +- [alizain/ulid](https://github.com/alizain/ulid) \ No newline at end of file diff --git a/src/main/java/io/azam/ulidj/ULID.java b/src/main/java/io/azam/ulidj/ULID.java new file mode 100644 index 0000000..103db25 --- /dev/null +++ b/src/main/java/io/azam/ulidj/ULID.java @@ -0,0 +1,315 @@ +/** + * MIT License + * + * Copyright (c) 2016 Azamshul Azizy + * + * 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 io.azam.ulidj; + +import java.util.Random; + +/** + * ULID string generator and parser class, using Crockford Base32 encoding. Only + * upper case letters are used for generation. Parsing allows upper and lower + * case letters, and i and l will be treated as 1 and o will be treated as 0. + *
+ *
+ * ULID generation examples:
+ * + *
+ * String ulid1 = ULID.random();
+ * String ulid2 = ULID.random(ThreadLocalRandom.current());
+ * String ulid3 = ULID.random(SecureRandom.newInstance("SHA1PRNG"));
+ * byte[] entropy = new byte[] { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9 };
+ * String ulid4 = ULID.generate(System.currentTimeMillis(), entropy);
+ * 
+ * + * ULID parsing examples:
+ * + *
+ * String ulid = "003JZ9J6G80123456789abcdef";
+ * assert ULID.isValid(ulid);
+ * long ts = ULID.getTimestamp(ulid);
+ * assert ts == 123456789000L;
+ * byte[] entropy = ULID.getEntropy(ulid);
+ * 
+ * + * @author azam + * @since 0.0.1 + * + * @see Base32 Encoding + * @see ULID + */ +public class ULID { + /** + * ULID string length. + */ + public static final int ULID_LENGTH = 26; + + /** + * Minimum allowed timestamp value. + */ + public static final long MIN_TIME = 0x0L; + + /** + * Maximum allowed timestamp value. + */ + public static final long MAX_TIME = 0x0000ffffffffffffL; + + /** + * Base32 characters mapping + */ + private static final char[] C = new char[] { // + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // + 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, // + 0x47, 0x48, 0x4a, 0x4b, 0x4d, 0x4e, 0x50, 0x51, // + 0x52, 0x53, 0x54, 0x56, 0x57, 0x58, 0x59, 0x5a }; + + /** + * {@code char} to {@code byte} O(1) mapping with alternative chars mapping + */ + private static final byte[] V = new byte[] { // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, // + (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, // + (byte) 0x08, (byte) 0x09, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0x0a, (byte) 0x0b, (byte) 0x0c, // + (byte) 0x0d, (byte) 0x0e, (byte) 0x0f, (byte) 0x10, // + (byte) 0x11, (byte) 0xff, (byte) 0x12, (byte) 0x13, // + (byte) 0xff, (byte) 0x14, (byte) 0x15, (byte) 0xff, // + (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, // + (byte) 0x1a, (byte) 0xff, (byte) 0x1b, (byte) 0x1c, // + (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0x0a, (byte) 0x0b, (byte) 0x0c, // + (byte) 0x0d, (byte) 0x0e, (byte) 0x0f, (byte) 0x10, // + (byte) 0x11, (byte) 0xff, (byte) 0x12, (byte) 0x13, // + (byte) 0xff, (byte) 0x14, (byte) 0x15, (byte) 0xff, // + (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, // + (byte) 0x1a, (byte) 0xff, (byte) 0x1b, (byte) 0x1c, // + (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff // + }; + + /** + * Generate random ULID string using {@link java.util.Random} instance. + * + * @return ULID string + */ + public static String random() { + byte[] entropy = new byte[10]; + Random random = new Random(); + random.nextBytes(entropy); + return generate(System.currentTimeMillis(), entropy); + } + + /** + * Generate random ULID string using provided {@link java.util.Random} + * instance. + * + * @param random + * {@link java.util.Random} instance + * @return ULID string + */ + public static String random(Random random) { + byte[] entropy = new byte[10]; + random.nextBytes(entropy); + return generate(System.currentTimeMillis(), entropy); + } + + /** + * Generate ULID from Unix epoch timestamp in millisecond and entropy bytes. + * Throws {@link java.lang.IllegalArgumentException} if timestamp is less + * than {@value #MIN_TIME}, is more than {@value #MAX_TIME}, or entropy + * bytes is null or less than 10 bytes. + * + * @param time + * Unix epoch timestamp in millisecond + * @param entropy + * Entropy bytes + * @return ULID string + */ + public static String generate(long time, byte[] entropy) { + if (time < MIN_TIME || time > MAX_TIME || entropy == null || entropy.length < 10) { + throw new IllegalArgumentException("Time is too long, or entropy is less than 10 bytes or null"); + } + + char[] chars = new char[26]; + + // time + chars[0] = C[((byte) (time >>> 45)) & 0x1f]; + chars[1] = C[((byte) (time >>> 40)) & 0x1f]; + chars[2] = C[((byte) (time >>> 35)) & 0x1f]; + chars[3] = C[((byte) (time >>> 30)) & 0x1f]; + chars[4] = C[((byte) (time >>> 25)) & 0x1f]; + chars[5] = C[((byte) (time >>> 20)) & 0x1f]; + chars[6] = C[((byte) (time >>> 15)) & 0x1f]; + chars[7] = C[((byte) (time >>> 10)) & 0x1f]; + chars[8] = C[((byte) (time >>> 5)) & 0x1f]; + chars[9] = C[((byte) (time)) & 0x1f]; + + // entropy + chars[10] = C[(byte) ((entropy[0] & 0xff) >>> 3)]; + chars[11] = C[(byte) (((entropy[0] << 2) | ((entropy[1] & 0xff) >>> 6)) & 0x1f)]; + chars[12] = C[(byte) (((entropy[1] & 0xff) >>> 1) & 0x1f)]; + chars[13] = C[(byte) (((entropy[1] << 4) | ((entropy[2] & 0xff) >>> 4)) & 0x1f)]; + chars[14] = C[(byte) (((entropy[2] << 5) | ((entropy[3] & 0xff) >>> 7)) & 0x1f)]; + chars[15] = C[(byte) (((entropy[3] & 0xff) >>> 2) & 0x1f)]; + chars[16] = C[(byte) (((entropy[3] << 3) | ((entropy[4] & 0xff) >>> 5)) & 0x1f)]; + chars[17] = C[(byte) (entropy[4] & 0x1f)]; + chars[18] = C[(byte) ((entropy[5] & 0xff) >>> 3)]; + chars[19] = C[(byte) (((entropy[5] << 2) | ((entropy[6] & 0xff) >>> 6)) & 0x1f)]; + chars[20] = C[(byte) (((entropy[6] & 0xff) >>> 1) & 0x1f)]; + chars[21] = C[(byte) (((entropy[6] << 4) | ((entropy[7] & 0xff) >>> 4)) & 0x1f)]; + chars[22] = C[(byte) (((entropy[7] << 5) | ((entropy[8] & 0xff) >>> 7)) & 0x1f)]; + chars[23] = C[(byte) (((entropy[8] & 0xff) >>> 2) & 0x1f)]; + chars[24] = C[(byte) (((entropy[8] << 3) | ((entropy[9] & 0xff) >>> 5)) & 0x1f)]; + chars[25] = C[(byte) (entropy[9] & 0x1f)]; + + return new String(chars); + } + + /** + * Checks ULID string validity. + * + * @param ulid + * ULID string + * @return true if ULID string is valid + */ + public static boolean isValid(CharSequence ulid) { + if (ulid == null || ulid.length() != ULID_LENGTH) { + return false; + } + for (int i = 0; i < ULID_LENGTH; i++) { + /** We only care for chars between 0x00 and 0xff. */ + char c = ulid.charAt(i); + if (c < 0 || c > V.length || V[c] == (byte) 0xff) { + return false; + } + } + return true; + } + + /** + * Extract and return the timestamp part from ULID. Expects a valid ULID + * string. Call {@link io.azam.ulidj.ULID#isValid(CharSequence)} and check + * validity before calling this method if you do not trust the origin of the + * ULID string. + * + * @param ulid + * ULID string + * @return Unix epoch timestamp in millisecond + */ + public static long getTimestamp(CharSequence ulid) { + return (long) V[ulid.charAt(0)] << 45 // + | (long) V[ulid.charAt(1)] << 40 // + | (long) V[ulid.charAt(2)] << 35 // + | (long) V[ulid.charAt(3)] << 30 // + | (long) V[ulid.charAt(4)] << 25 // + | (long) V[ulid.charAt(5)] << 20 // + | (long) V[ulid.charAt(6)] << 15 // + | (long) V[ulid.charAt(7)] << 10 // + | (long) V[ulid.charAt(8)] << 5 // + | (long) V[ulid.charAt(9)]; + } + + /** + * Extract and return the entropy part from ULID. Expects a valid ULID + * string. Call {@link io.azam.ulidj.ULID#isValid(CharSequence)} and check + * validity before calling this method if you do not trust the origin of the + * ULID string. + * + * @param ulid + * ULID string + * @return Entropy bytes + */ + public static byte[] getEntropy(CharSequence ulid) { + byte[] bytes = new byte[10]; + bytes[0] = (byte) ((V[ulid.charAt(10)] << 3) // + | (V[ulid.charAt(11)] & 0xff) >>> 2); + bytes[1] = (byte) ((V[ulid.charAt(11)] << 6) // + | V[ulid.charAt(12)] << 1 // + | (V[ulid.charAt(13)] & 0xff) >>> 4); + bytes[2] = (byte) ((V[ulid.charAt(13)] << 4) // + | (V[ulid.charAt(14)] & 0xff) >>> 1); + bytes[3] = (byte) ((V[ulid.charAt(14)] << 7) // + | V[ulid.charAt(15)] << 2 // + | (V[ulid.charAt(16)] & 0xff) >>> 3); + bytes[4] = (byte) ((V[ulid.charAt(16)] << 5) // + | V[ulid.charAt(17)]); + bytes[5] = (byte) ((V[ulid.charAt(18)] << 3) // + | (V[ulid.charAt(19)] & 0xff) >>> 2); + bytes[6] = (byte) ((V[ulid.charAt(19)] << 6) // + | V[ulid.charAt(20)] << 1 // + | (V[ulid.charAt(21)] & 0xff) >>> 4); + bytes[7] = (byte) ((V[ulid.charAt(21)] << 4) // + | (V[ulid.charAt(22)] & 0xff) >>> 1); + bytes[8] = (byte) ((V[ulid.charAt(22)] << 7) // + | V[ulid.charAt(23)] << 2 // + | (V[ulid.charAt(24)] & 0xff) >>> 3); + bytes[9] = (byte) ((V[ulid.charAt(24)] << 5) // + | V[ulid.charAt(25)]); + return bytes; + } +} diff --git a/src/test/java/io/azam/ulidj/ULIDTest.java b/src/test/java/io/azam/ulidj/ULIDTest.java new file mode 100644 index 0000000..9e645ad --- /dev/null +++ b/src/test/java/io/azam/ulidj/ULIDTest.java @@ -0,0 +1,217 @@ +/** + * MIT License + * + * Copyright (c) 2016 Azamshul Azizy + * + * 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 io.azam.ulidj; + +import java.util.Random; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test class for {@link io.azam.ulidj.ULID} + * + * @author azam + * @since 0.0.1 + */ +public class ULIDTest { + private static final byte[] ZERO_ENTROPY = new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }; + + private static class TestParam { + public final long timestamp; + public final byte[] entropy; + public final String value; + public final boolean isIllegalArgument; + public final String reproducer; + + public TestParam(long timestamp, byte[] entropy, String value, boolean isIllegalArgument) { + this.timestamp = timestamp; + this.entropy = entropy; + this.value = value; + this.isIllegalArgument = isIllegalArgument; + StringBuilder sb = new StringBuilder(); + sb.append("ULID.generate(").append(Long.toString(timestamp)).append("L,"); + if (entropy != null) { + sb.append("new byte[]{"); + for (int i = 0; i < entropy.length; i++) { + sb.append("0x").append(Integer.toHexString((entropy[i] & 0xFF) + 0x100).substring(1)); + if (i + 1 < entropy.length) { + sb.append(","); + } + } + sb.append("}"); + } else { + sb.append("null"); + } + sb.append(")"); + this.reproducer = sb.toString(); + } + } + + private static final TestParam[] TEST_PARAMETERS = new TestParam[] { // + new TestParam(ULID.MIN_TIME, ZERO_ENTROPY, "00000000000000000000000000", false), // + new TestParam(ULID.MAX_TIME, ZERO_ENTROPY, "7ZZZZZZZZZ0000000000000000", false), // + new TestParam(0x00000001L, ZERO_ENTROPY, "00000000010000000000000000", false), // + new TestParam(0x0000000fL, ZERO_ENTROPY, "000000000F0000000000000000", false), // + new TestParam(0x00000010L, ZERO_ENTROPY, "000000000G0000000000000000", false), // + new TestParam(0x00000011L, ZERO_ENTROPY, "000000000H0000000000000000", false), // + new TestParam(0x0000001fL, ZERO_ENTROPY, "000000000Z0000000000000000", false), // + new TestParam(0x00000020L, ZERO_ENTROPY, "00000000100000000000000000", false), // + new TestParam(0x00000021L, ZERO_ENTROPY, "00000000110000000000000000", false), // + new TestParam(0x0000002fL, ZERO_ENTROPY, "000000001F0000000000000000", false), // + new TestParam(0x00000030L, ZERO_ENTROPY, "000000001G0000000000000000", false), // + new TestParam(0x00000031L, ZERO_ENTROPY, "000000001H0000000000000000", false), // + new TestParam(0x0000003fL, ZERO_ENTROPY, "000000001Z0000000000000000", false), // + new TestParam(0x00000040L, ZERO_ENTROPY, "00000000200000000000000000", false), // + new TestParam(0x000000f0L, ZERO_ENTROPY, "000000007G0000000000000000", false), // + new TestParam(0x000000ffL, ZERO_ENTROPY, "000000007Z0000000000000000", false), // + new TestParam(0x00000100L, ZERO_ENTROPY, "00000000800000000000000000", false), // + new TestParam(0x00000101L, ZERO_ENTROPY, "00000000810000000000000000", false), // + new TestParam(0x000001ffL, ZERO_ENTROPY, "00000000FZ0000000000000000", false), // + new TestParam(0x00000200L, ZERO_ENTROPY, "00000000G00000000000000000", false), // + new TestParam(0x00000201L, ZERO_ENTROPY, "00000000G10000000000000000", false), // + new TestParam(0x000002ffL, ZERO_ENTROPY, "00000000QZ0000000000000000", false), // + new TestParam(0x00000300L, ZERO_ENTROPY, "00000000R00000000000000000", false), // + new TestParam(0x00000301L, ZERO_ENTROPY, "00000000R10000000000000000", false), // + new TestParam(0x000003ffL, ZERO_ENTROPY, "00000000ZZ0000000000000000", false), // + new TestParam(0x00000400L, ZERO_ENTROPY, "00000001000000000000000000", false), // + new TestParam(0x00000401L, ZERO_ENTROPY, "00000001010000000000000000", false), // + new TestParam(0x000007ffL, ZERO_ENTROPY, "00000001ZZ0000000000000000", false), // + new TestParam(0x00000800L, ZERO_ENTROPY, "00000002000000000000000000", false), // + new TestParam(0x00007fffL, ZERO_ENTROPY, "0000000ZZZ0000000000000000", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01 }, + "00000000000000000000000001", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0f }, + "0000000000000000000000000F", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10 }, + "0000000000000000000000000G", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f }, + "0000000000000000000000000Z", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20 }, + "00000000000000000000000010", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x21 }, + "00000000000000000000000011", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2f }, + "0000000000000000000000001F", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x30 }, + "0000000000000000000000001G", false), // + new TestParam(ULID.MIN_TIME, new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f }, + "0000000000000000000000001Z", false), // + }; + + @Test + public void testRandom() { + String value = ULID.random(); + Assert.assertNotNull("Generated ULID must not be null", value); + Assert.assertEquals("Generated ULID length must be 26", 26, value.length()); + Assert.assertTrue("Generated ULID characters must only include [0123456789ABCDEFGHJKMNPQRSTVWXYZ]", + value.matches("[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")); + } + + @Test + public void testGenerateRandom() { + byte[] entropy = new byte[10]; + Random random = new Random(); + random.nextBytes(entropy); + String value = ULID.generate(System.currentTimeMillis(), entropy); + Assert.assertNotNull("Generated ULID must not be null", value); + Assert.assertEquals("Generated ULID length must be 26, but returned " + value.length() + " instead", 26, + value.length()); + Assert.assertTrue( + "Generated ULID characters must only include [0123456789ABCDEFGHJKMNPQRSTVWXYZ], but returned " + value + + " instead", + value.matches("[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")); + } + + @Test + public void testGenerateFixedValues() { + for (TestParam params : TEST_PARAMETERS) { + boolean hasIllegalArgumentException = false; + try { + String value = ULID.generate(params.timestamp, params.entropy); + Assert.assertEquals("Generated ULID must be equal to \"" + params.value + "\" for " + params.reproducer + + " , but returned \"" + value + "\" instead", params.value, value); + Assert.assertNotNull("Generated ULID must not be null", value); + Assert.assertEquals("Generated ULID length must be 26, but returned " + value.length() + " instead", 26, + value.length()); + Assert.assertTrue( + "Generated ULID characters must only include [0123456789ABCDEFGHJKMNPQRSTVWXYZ], but returned " + + value + " instead", + value.matches("[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")); + } catch (IllegalArgumentException e) { + hasIllegalArgumentException = true; + } + if (params.isIllegalArgument) { + Assert.assertTrue("IllegalArgumentException is expected for " + params.reproducer, + hasIllegalArgumentException); + } else { + Assert.assertFalse("IllegalArgumentException is not expected for " + params.reproducer, + hasIllegalArgumentException); + } + } + } + + @Test + public void testIsValidNegative() { + String[] invalidUlids = new String[] { // + null, // + "", // + "0", // + "000000000000000000000000000", // + "-0000000000000000000000000", // + "0000000000000000000000000U", // + "0000000000000000000000000/u3042", // + "0000000000000000000000000#", // + }; + for (String ulid : invalidUlids) { + Assert.assertFalse("ULID \"" + ulid + "\" should be invalid", ULID.isValid(ulid)); + } + } + + @Test + public void testIsValidFixedValues() { + for (TestParam params : TEST_PARAMETERS) { + if (!params.isIllegalArgument) { + Assert.assertTrue("ULID string is valid", ULID.isValid(params.value)); + } + } + } + + @Test + public void testGetTimestampFixedValues() { + for (TestParam params : TEST_PARAMETERS) { + if (!params.isIllegalArgument) { + Assert.assertEquals("ULID timestamp is different", params.timestamp, ULID.getTimestamp(params.value)); + } + } + } + + @Test + public void testGetEntropyFixedValues() { + for (TestParam params : TEST_PARAMETERS) { + if (!params.isIllegalArgument) { + Assert.assertArrayEquals("ULID entropy is different", params.entropy, ULID.getEntropy(params.value)); + } + } + } +}