diff --git a/design/perfin-logo.svg b/design/perfin-logo.svg index 0e953e2..a4d5e94 100644 --- a/design/perfin-logo.svg +++ b/design/perfin-logo.svg @@ -9,9 +9,9 @@ id="svg1" inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" sodipodi:docname="perfin-logo.svg" - inkscape:export-filename="../src/main/resources/images/perfin-logo_64.png" - inkscape:export-xdpi="96" - inkscape:export-ydpi="96" + inkscape:export-filename="perfin-logo_256.png" + inkscape:export-xdpi="384" + inkscape:export-ydpi="384" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -26,9 +26,9 @@ inkscape:pagecheckerboard="1" inkscape:deskcolor="#505050" inkscape:document-units="px" - inkscape:zoom="4" - inkscape:cx="-0.99999999" - inkscape:cy="38.25" + inkscape:zoom="8" + inkscape:cx="25.125" + inkscape:cy="33.375" inkscape:window-width="1920" inkscape:window-height="1025" inkscape:window-x="1080" diff --git a/design/perfin-logo_256.png b/design/perfin-logo_256.png new file mode 100644 index 0000000..3a97420 Binary files /dev/null and b/design/perfin-logo_256.png differ diff --git a/pom.xml b/pom.xml index dc999e1..8f35203 100644 --- a/pom.xml +++ b/pom.xml @@ -49,11 +49,6 @@ h2 2.2.224 - - com.github.f4b6a3 - ulid-creator - 5.2.2 - @@ -106,6 +101,58 @@ + + + + org.moditect + moditect-maven-plugin + 1.0.0.Final + + + add-module-infos + generate-resources + + add-module-info + + + ${project.build.directory}/modules + + + + com.h2database + h2 + 2.2.224 + + + module com.h2database { + requires java.compiler; + requires jdk.net; + requires static lucene.core; + requires static lucene.queryparser; + requires static slf4j.api; + requires static jakarta.servlet; + requires transitive java.desktop; + requires transitive java.instrument; + requires java.logging; + requires transitive java.management; + requires static java.naming; + requires transitive java.scripting; + requires java.sql; + requires transitive java.transaction.xa; + requires transitive java.xml; + requires static javax.servlet.api; + requires static org.locationtech.jts; + requires static org.osgi.service.jdbc; + requires static osgi.core; + provides java.sql.Driver with org.h2.Driver; + } + + + + + + + \ No newline at end of file diff --git a/scripts/package-linux-deb.sh b/scripts/package-linux-deb.sh new file mode 100755 index 0000000..22786df --- /dev/null +++ b/scripts/package-linux-deb.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Creates a native linux installer for Perfin, in the .deb format (for Ubuntu, Debian, etc.) + +mvn clean package + +function join_by { + local d=${1-} f=${2-} + if shift 2; then + printf %s "$f" "${@/#/$d}" + fi +} + +# Gets a ":"-separated string of all the dependency jar-files. +module_jar_files=(target/lib/*) +module_jar_files_path=$(join_by ":" ${module_jar_files[@]}) +module_path="target/classes:$module_jar_files_path" + +# Fix because H2 is not modular: +rm target/lib/h2-*.jar +module_path="$module_path:target/modules/h2-2.2.224.jar" + +jpackage \ + --name "Perfin" \ + --app-version "0.0.1" \ + --description "Desktop application for personal finance. Add your accounts, track transactions, and store receipts, invoices, and more." \ + --icon design/perfin-logo_256.png \ + --vendor "Andrew Lalis" \ + --module com.andrewlalis.perfin/com.andrewlalis.perfin.PerfinApp \ + --module-path $module_path \ + --add-modules com.h2database \ + --linux-deb-maintainer "andrewlalisofficial@gmail.com" \ + --linux-shortcut \ + --linux-menu-group "Office;Finance;Java" \ diff --git a/run-jar.sh b/scripts/run-jar.sh similarity index 68% rename from run-jar.sh rename to scripts/run-jar.sh index ab86ff2..c81299c 100755 --- a/run-jar.sh +++ b/scripts/run-jar.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# A helper script to (optionally) build and run a JAR packaged version of Perfin. +# Provide the "build" argument to rebuild the project before running it. + shouldBuild=0 for i in "$@" ; do if [[ $i == "build" ]] ; then diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAttachmentRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAttachmentRepository.java index 65e3e80..2acdbc3 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAttachmentRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcAttachmentRepository.java @@ -1,10 +1,10 @@ package com.andrewlalis.perfin.data.impl; import com.andrewlalis.perfin.data.AttachmentRepository; +import com.andrewlalis.perfin.data.ulid.UlidCreator; import com.andrewlalis.perfin.data.util.DbUtil; import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.model.Attachment; -import com.github.f4b6a3.ulid.UlidCreator; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/main/java/com/andrewlalis/perfin/data/ulid/Ulid.java b/src/main/java/com/andrewlalis/perfin/data/ulid/Ulid.java new file mode 100644 index 0000000..c6c6678 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/data/ulid/Ulid.java @@ -0,0 +1,814 @@ +/* + * MIT License + * + * Copyright (c) 2020-2023 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.andrewlalis.perfin.data.ulid; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Arrays; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +/** + * A class that represents ULIDs. + *

+ * ULID is a 128-bit value that has two components: + *

+ *

+ * ULID has 128-bit compatibility with {@link UUID}. Like a UUID, a ULID can + * also be stored as a 16-byte array. + *

+ * Instances of this class are immutable. + * + * @see ULID Specification + */ +public final class Ulid implements Serializable, Comparable { + + private static final long serialVersionUID = 2625269413446854731L; + + private final long msb; // most significant bits + private final long lsb; // least significant bits + + /** + * Number of characters of a ULID. + */ + public static final int ULID_CHARS = 26; + /** + * Number of characters of the time component of a ULID. + */ + public static final int TIME_CHARS = 10; + /** + * Number of characters of the random component of a ULID. + */ + public static final int RANDOM_CHARS = 16; + + /** + * Number of bytes of a ULID. + */ + public static final int ULID_BYTES = 16; + /** + * Number of bytes of the time component of a ULID. + */ + public static final int TIME_BYTES = 6; + /** + * Number of bytes of the random component of a ULID. + */ + public static final int RANDOM_BYTES = 10; + /** + * A special ULID that has all 128 bits set to ZERO. + */ + public static final Ulid MIN = new Ulid(0x0000000000000000L, 0x0000000000000000L); + /** + * A special ULID that has all 128 bits set to ONE. + */ + public static final Ulid MAX = new Ulid(0xffffffffffffffffL, 0xffffffffffffffffL); + + static final byte[] ALPHABET_VALUES = new byte[256]; + static final char[] ALPHABET_UPPERCASE = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray(); + static final char[] ALPHABET_LOWERCASE = "0123456789abcdefghjkmnpqrstvwxyz".toCharArray(); + + static { + + // Initialize the alphabet map with -1 + Arrays.fill(ALPHABET_VALUES, (byte) -1); + + // Map the alphabets chars to values + for (int i = 0; i < ALPHABET_UPPERCASE.length; i++) { + ALPHABET_VALUES[ALPHABET_UPPERCASE[i]] = (byte) i; + } + for (int i = 0; i < ALPHABET_LOWERCASE.length; i++) { + ALPHABET_VALUES[ALPHABET_LOWERCASE[i]] = (byte) i; + } + + // Upper case OIL + ALPHABET_VALUES['O'] = 0x00; + ALPHABET_VALUES['I'] = 0x01; + ALPHABET_VALUES['L'] = 0x01; + + // Lower case OIL + ALPHABET_VALUES['o'] = 0x00; + ALPHABET_VALUES['i'] = 0x01; + ALPHABET_VALUES['l'] = 0x01; + } + + // 0xffffffffffffffffL + 1 = 0x0000000000000000L + private static final long INCREMENT_OVERFLOW = 0x0000000000000000L; + + /** + * Creates a new ULID. + *

+ * Useful to make copies of ULIDs. + * + * @param ulid a ULID + */ + public Ulid(Ulid ulid) { + this.msb = ulid.msb; + this.lsb = ulid.lsb; + } + + /** + * Creates a new ULID. + *

+ * If you want to make a copy of a {@link UUID}, use {@link Ulid#from(UUID)} + * instead. + * + * @param mostSignificantBits the first 8 bytes as a long value + * @param leastSignificantBits the last 8 bytes as a long value + */ + public Ulid(long mostSignificantBits, long leastSignificantBits) { + this.msb = mostSignificantBits; + this.lsb = leastSignificantBits; + } + + /** + * Creates a new ULID. + *

+ * The time parameter is the number of milliseconds since 1970-01-01, also known + * as Unix epoch. It must be a positive number not larger than 2^48-1. + *

+ * The random parameter must be an arbitrary array of 10 bytes. + *

+ * Note: ULIDs cannot be composed of dates before 1970-01-01, as their embedded + * timestamp is internally treated as an unsigned integer, i.e., it can only + * represent the set of natural numbers including zero, up to 2^48-1. + * + * @param time the number of milliseconds since 1970-01-01 + * @param random an array of 10 bytes + * @throws IllegalArgumentException if time is negative or larger than 2^48-1 + * @throws IllegalArgumentException if random is null or its length is not 10 + */ + public Ulid(long time, byte[] random) { + + // The time component has 48 bits. + if ((time & 0xffff000000000000L) != 0) { + // ULID specification: + // "Any attempt to decode or encode a ULID larger than this (time > 2^48-1) + // should be rejected by all implementations, to prevent overflow bugs." + throw new IllegalArgumentException("Invalid time value"); // overflow or negative time! + } + // The random component has 80 bits (10 bytes). + if (random == null || random.length != RANDOM_BYTES) { + throw new IllegalArgumentException("Invalid random bytes"); // null or wrong length! + } + + long long0 = 0; + long long1 = 0; + + long0 |= time << 16; + long0 |= (long) (random[0x0] & 0xff) << 8; + long0 |= (long) (random[0x1] & 0xff); + + long1 |= (long) (random[0x2] & 0xff) << 56; + long1 |= (long) (random[0x3] & 0xff) << 48; + long1 |= (long) (random[0x4] & 0xff) << 40; + long1 |= (long) (random[0x5] & 0xff) << 32; + long1 |= (long) (random[0x6] & 0xff) << 24; + long1 |= (long) (random[0x7] & 0xff) << 16; + long1 |= (long) (random[0x8] & 0xff) << 8; + long1 |= (long) (random[0x9] & 0xff); + + this.msb = long0; + this.lsb = long1; + } + + /** + * Returns a fast new ULID. + *

+ * This static method is a quick alternative to {@link UlidCreator#getUlid()}. + *

+ * It employs {@link ThreadLocalRandom} which works very well, although not + * cryptographically strong. It can be useful, for example, for logging. + *

+ * Security-sensitive applications that require a cryptographically secure + * pseudo-random generator should use {@link UlidCreator#getUlid()}. + * + * @return a ULID + * @see {@link ThreadLocalRandom} + * @since 5.1.0 + */ + public static Ulid fast() { + final long time = System.currentTimeMillis(); + ThreadLocalRandom random = ThreadLocalRandom.current(); + return new Ulid((time << 16) | (random.nextLong() & 0xffffL), random.nextLong()); + } + + /** + * Returns the minimum ULID for a given time. + *

+ * The 48 bits of the time component are filled with the given time and the 80 + * bits of the random component are all set to ZERO. + *

+ * For example, the minimum ULID for 2022-02-22 22:22:22.222 is + * `{@code new Ulid(0x017f2387460e0000L, 0x0000000000000000L)}`, where + * `{@code 0x017f2387460e}` is the timestamp in hexadecimal. + *

+ * It can be useful to find all records before or after a specific timestamp in + * a table without a `{@code created_at}` field. + * + * @param time the number of milliseconds since 1970-01-01 + * @return a ULID + * @since 5.2.0 + */ + public static Ulid min(long time) { + return new Ulid((time << 16) | 0x0000L, 0x0000000000000000L); + } + + /** + * Returns the maximum ULID for a given time. + *

+ * The 48 bits of the time component are filled with the given time and the 80 + * bits or the random component are all set to ONE. + *

+ * For example, the maximum ULID for 2022-02-22 22:22:22.222 is + * `{@code new Ulid(0x017f2387460effffL, 0xffffffffffffffffL)}`, where + * `{@code 0x017f2387460e}` is the timestamp in hexadecimal. + *

+ * It can be useful to find all records before or after a specific timestamp in + * a table without a `{@code created_at}` field. + * + * @param time the number of milliseconds since 1970-01-01 + * @return a ULID + * @since 5.2.0 + */ + public static Ulid max(long time) { + return new Ulid((time << 16) | 0xffffL, 0xffffffffffffffffL); + } + + /** + * Converts a UUID into a ULID. + * + * @param uuid a UUID + * @return a ULID + */ + public static Ulid from(UUID uuid) { + return new Ulid(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); + } + + /** + * Converts a byte array into a ULID. + * + * @param bytes an array of 16 bytes + * @return a ULID + * @throws IllegalArgumentException if bytes are null or its length is not 16 + */ + public static Ulid from(byte[] bytes) { + + if (bytes == null || bytes.length != ULID_BYTES) { + throw new IllegalArgumentException("Invalid ULID bytes"); // null or wrong length! + } + + long msb = 0; + long lsb = 0; + + msb |= (bytes[0x0] & 0xffL) << 56; + msb |= (bytes[0x1] & 0xffL) << 48; + msb |= (bytes[0x2] & 0xffL) << 40; + msb |= (bytes[0x3] & 0xffL) << 32; + msb |= (bytes[0x4] & 0xffL) << 24; + msb |= (bytes[0x5] & 0xffL) << 16; + msb |= (bytes[0x6] & 0xffL) << 8; + msb |= (bytes[0x7] & 0xffL); + + lsb |= (bytes[0x8] & 0xffL) << 56; + lsb |= (bytes[0x9] & 0xffL) << 48; + lsb |= (bytes[0xa] & 0xffL) << 40; + lsb |= (bytes[0xb] & 0xffL) << 32; + lsb |= (bytes[0xc] & 0xffL) << 24; + lsb |= (bytes[0xd] & 0xffL) << 16; + lsb |= (bytes[0xe] & 0xffL) << 8; + lsb |= (bytes[0xf] & 0xffL); + + return new Ulid(msb, lsb); + } + + /** + * Converts a canonical string into a ULID. + *

+ * The input string must be 26 characters long and must contain only characters + * from Crockford's base 32 alphabet. + *

+ * The first character of the input string must be between 0 and 7. + * + * @param string a canonical string + * @return a ULID + * @throws IllegalArgumentException if the input string is invalid + * @see Crockford's Base 32 + */ + public static Ulid from(String string) { + + final char[] chars = toCharArray(string); + + long time = 0; + long random0 = 0; + long random1 = 0; + + time |= (long) ALPHABET_VALUES[chars[0x00]] << 45; + time |= (long) ALPHABET_VALUES[chars[0x01]] << 40; + time |= (long) ALPHABET_VALUES[chars[0x02]] << 35; + time |= (long) ALPHABET_VALUES[chars[0x03]] << 30; + time |= (long) ALPHABET_VALUES[chars[0x04]] << 25; + time |= (long) ALPHABET_VALUES[chars[0x05]] << 20; + time |= (long) ALPHABET_VALUES[chars[0x06]] << 15; + time |= (long) ALPHABET_VALUES[chars[0x07]] << 10; + time |= (long) ALPHABET_VALUES[chars[0x08]] << 5; + time |= (long) ALPHABET_VALUES[chars[0x09]]; + + random0 |= (long) ALPHABET_VALUES[chars[0x0a]] << 35; + random0 |= (long) ALPHABET_VALUES[chars[0x0b]] << 30; + random0 |= (long) ALPHABET_VALUES[chars[0x0c]] << 25; + random0 |= (long) ALPHABET_VALUES[chars[0x0d]] << 20; + random0 |= (long) ALPHABET_VALUES[chars[0x0e]] << 15; + random0 |= (long) ALPHABET_VALUES[chars[0x0f]] << 10; + random0 |= (long) ALPHABET_VALUES[chars[0x10]] << 5; + random0 |= (long) ALPHABET_VALUES[chars[0x11]]; + + random1 |= (long) ALPHABET_VALUES[chars[0x12]] << 35; + random1 |= (long) ALPHABET_VALUES[chars[0x13]] << 30; + random1 |= (long) ALPHABET_VALUES[chars[0x14]] << 25; + random1 |= (long) ALPHABET_VALUES[chars[0x15]] << 20; + random1 |= (long) ALPHABET_VALUES[chars[0x16]] << 15; + random1 |= (long) ALPHABET_VALUES[chars[0x17]] << 10; + random1 |= (long) ALPHABET_VALUES[chars[0x18]] << 5; + random1 |= (long) ALPHABET_VALUES[chars[0x19]]; + + final long msb = (time << 16) | (random0 >>> 24); + final long lsb = (random0 << 40) | (random1 & 0xffffffffffL); + + return new Ulid(msb, lsb); + } + + /** + * Convert the ULID into a UUID. + *

+ * A ULID has 128-bit compatibility with a {@link UUID}. + *

+ * If you need an RFC-4122 UUIDv4 do this: {@code Ulid.toRfc4122().toUuid()}. + * + * @return a UUID. + */ + public UUID toUuid() { + return new UUID(this.msb, this.lsb); + } + + /** + * Convert the ULID into a byte array. + * + * @return a byte array. + */ + public byte[] toBytes() { + + final byte[] bytes = new byte[ULID_BYTES]; + + bytes[0x0] = (byte) (msb >>> 56); + bytes[0x1] = (byte) (msb >>> 48); + bytes[0x2] = (byte) (msb >>> 40); + bytes[0x3] = (byte) (msb >>> 32); + bytes[0x4] = (byte) (msb >>> 24); + bytes[0x5] = (byte) (msb >>> 16); + bytes[0x6] = (byte) (msb >>> 8); + bytes[0x7] = (byte) (msb); + + bytes[0x8] = (byte) (lsb >>> 56); + bytes[0x9] = (byte) (lsb >>> 48); + bytes[0xa] = (byte) (lsb >>> 40); + bytes[0xb] = (byte) (lsb >>> 32); + bytes[0xc] = (byte) (lsb >>> 24); + bytes[0xd] = (byte) (lsb >>> 16); + bytes[0xe] = (byte) (lsb >>> 8); + bytes[0xf] = (byte) (lsb); + + return bytes; + } + + /** + * Converts the ULID into a canonical string in upper case. + *

+ * The output string is 26 characters long and contains only characters from + * Crockford's Base 32 alphabet. + *

+ * For lower case string, use the shorthand {@code Ulid#toLowerCase()}, instead + * of {@code Ulid#toString()#toLowerCase()}. + * + * @return a ULID string + * @see Crockford's Base 32 + */ + @Override + public String toString() { + return toString(ALPHABET_UPPERCASE); + } + + /** + * Converts the ULID into a canonical string in lower case. + *

+ * The output string is 26 characters long and contains only characters from + * Crockford's Base 32 alphabet. + *

+ * It is a shorthand at least twice as fast as + * {@code Ulid.toString().toLowerCase()}. + * + * @return a string + * @see Crockford's Base 32 + */ + public String toLowerCase() { + return toString(ALPHABET_LOWERCASE); + } + + /** + * Converts the ULID into another ULID that is compatible with UUIDv4. + *

+ * The bytes of the returned ULID are compliant with the RFC-4122 version 4. + *

+ * If you need a RFC-4122 UUIDv4 do this: {@code Ulid.toRfc4122().toUuid()}. + *

+ * Note: If you use this method, you can not get the original ULID, since + * it changes 6 bits of it to generate a UUIDv4. + * + * @return a ULID + * @see RFC-4122 + */ + public Ulid toRfc4122() { + + // set the 4 most significant bits of the 7th byte to 0, 1, 0 and 0 + final long msb4 = (this.msb & 0xffffffffffff0fffL) | 0x0000000000004000L; // RFC-4122 version 4 + // set the 2 most significant bits of the 9th byte to 1 and 0 + final long lsb4 = (this.lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // RFC-4122 variant 2 + + return new Ulid(msb4, lsb4); + } + + /** + * Returns the instant of creation. + *

+ * The instant of creation is extracted from the time component. + * + * @return the {@link Instant} of creation + */ + public Instant getInstant() { + return Instant.ofEpochMilli(this.getTime()); + } + + /** + * Returns the instant of creation. + *

+ * The instant of creation is extracted from the time component. + * + * @param string a canonical string + * @return the {@link Instant} of creation + * @throws IllegalArgumentException if the input string is invalid + */ + public static Instant getInstant(String string) { + return Instant.ofEpochMilli(getTime(string)); + } + + /** + * Returns the time component as a number. + *

+ * The time component is a number between 0 and 2^48-1. It is equivalent to the + * count of milliseconds since 1970-01-01 (Unix epoch). + * + * @return a number of milliseconds + */ + public long getTime() { + return this.msb >>> 16; + } + + /** + * Returns the time component as a number. + *

+ * The time component is a number between 0 and 2^48-1. It is equivalent to the + * count of milliseconds since 1970-01-01 (Unix epoch). + * + * @param string a canonical string + * @return a number of milliseconds + * @throws IllegalArgumentException if the input string is invalid + */ + public static long getTime(String string) { + + final char[] chars = toCharArray(string); + + long time = 0; + + time |= (long) ALPHABET_VALUES[chars[0x00]] << 45; + time |= (long) ALPHABET_VALUES[chars[0x01]] << 40; + time |= (long) ALPHABET_VALUES[chars[0x02]] << 35; + time |= (long) ALPHABET_VALUES[chars[0x03]] << 30; + time |= (long) ALPHABET_VALUES[chars[0x04]] << 25; + time |= (long) ALPHABET_VALUES[chars[0x05]] << 20; + time |= (long) ALPHABET_VALUES[chars[0x06]] << 15; + time |= (long) ALPHABET_VALUES[chars[0x07]] << 10; + time |= (long) ALPHABET_VALUES[chars[0x08]] << 5; + time |= (long) ALPHABET_VALUES[chars[0x09]]; + + return time; + } + + /** + * Returns the random component as a byte array. + *

+ * The random component is an array of 10 bytes (80 bits). + * + * @return a byte array + */ + public byte[] getRandom() { + + final byte[] bytes = new byte[RANDOM_BYTES]; + + bytes[0x0] = (byte) (msb >>> 8); + bytes[0x1] = (byte) (msb); + + bytes[0x2] = (byte) (lsb >>> 56); + bytes[0x3] = (byte) (lsb >>> 48); + bytes[0x4] = (byte) (lsb >>> 40); + bytes[0x5] = (byte) (lsb >>> 32); + bytes[0x6] = (byte) (lsb >>> 24); + bytes[0x7] = (byte) (lsb >>> 16); + bytes[0x8] = (byte) (lsb >>> 8); + bytes[0x9] = (byte) (lsb); + + return bytes; + } + + /** + * Returns the random component as a byte array. + *

+ * The random component is an array of 10 bytes (80 bits). + * + * @param string a canonical string + * @return a byte array + * @throws IllegalArgumentException if the input string is invalid + */ + public static byte[] getRandom(String string) { + + final char[] chars = toCharArray(string); + + long random0 = 0; + long random1 = 0; + + random0 |= (long) ALPHABET_VALUES[chars[0x0a]] << 35; + random0 |= (long) ALPHABET_VALUES[chars[0x0b]] << 30; + random0 |= (long) ALPHABET_VALUES[chars[0x0c]] << 25; + random0 |= (long) ALPHABET_VALUES[chars[0x0d]] << 20; + random0 |= (long) ALPHABET_VALUES[chars[0x0e]] << 15; + random0 |= (long) ALPHABET_VALUES[chars[0x0f]] << 10; + random0 |= (long) ALPHABET_VALUES[chars[0x10]] << 5; + random0 |= (long) ALPHABET_VALUES[chars[0x11]]; + + random1 |= (long) ALPHABET_VALUES[chars[0x12]] << 35; + random1 |= (long) ALPHABET_VALUES[chars[0x13]] << 30; + random1 |= (long) ALPHABET_VALUES[chars[0x14]] << 25; + random1 |= (long) ALPHABET_VALUES[chars[0x15]] << 20; + random1 |= (long) ALPHABET_VALUES[chars[0x16]] << 15; + random1 |= (long) ALPHABET_VALUES[chars[0x17]] << 10; + random1 |= (long) ALPHABET_VALUES[chars[0x18]] << 5; + random1 |= (long) ALPHABET_VALUES[chars[0x19]]; + + final byte[] bytes = new byte[RANDOM_BYTES]; + + bytes[0x0] = (byte) (random0 >>> 32); + bytes[0x1] = (byte) (random0 >>> 24); + bytes[0x2] = (byte) (random0 >>> 16); + bytes[0x3] = (byte) (random0 >>> 8); + bytes[0x4] = (byte) (random0); + + bytes[0x5] = (byte) (random1 >>> 32); + bytes[0x6] = (byte) (random1 >>> 24); + bytes[0x7] = (byte) (random1 >>> 16); + bytes[0x8] = (byte) (random1 >>> 8); + bytes[0x9] = (byte) (random1); + + return bytes; + } + + /** + * Returns the most significant bits as a number. + * + * @return a number. + */ + public long getMostSignificantBits() { + return this.msb; + } + + /** + * Returns the least significant bits as a number. + * + * @return a number. + */ + public long getLeastSignificantBits() { + return this.lsb; + } + + /** + * Returns a new ULID by incrementing the random component of the current ULID. + *

+ * Since the random component contains 80 bits: + *

+ *

+ * Due to (1) and (2), it does not throw the error message recommended by the + * specification. When an overflow occurs in the random 80 bits, the time + * component is simply incremented to maintain monotonicity. + * + * @return a ULID + */ + public Ulid increment() { + + long newMsb = this.msb; + long newLsb = this.lsb + 1; // increment the LEAST significant bits + + if (newLsb == INCREMENT_OVERFLOW) { + newMsb += 1; // increment the MOST significant bits + } + + return new Ulid(newMsb, newLsb); + } + + /** + * Checks if the input string is valid. + *

+ * The input string must be 26 characters long and must contain only characters + * from Crockford's base 32 alphabet. + *

+ * The first character of the input string must be between 0 and 7. + * + * @param string a canonical string + * @return true if the input string is valid + * @see Crockford's Base 32 + */ + public static boolean isValid(String string) { + return string != null && isValidCharArray(string.toCharArray()); + } + + /** + * Returns a hash code value for the ULID. + */ + @Override + public int hashCode() { + final long bits = msb ^ lsb; + return (int) (bits ^ (bits >>> 32)); + } + + /** + * Checks if some other ULID is equal to this one. + */ + @Override + public boolean equals(Object other) { + if (other == null) + return false; + if (other.getClass() != Ulid.class) + return false; + Ulid that = (Ulid) other; + if (lsb != that.lsb) + return false; + else return msb == that.msb; + } + + /** + * Compares two ULIDs as unsigned 128-bit integers. + *

+ * The first of two ULIDs is greater than the second if the most significant + * byte in which they differ is greater for the first ULID. + * + * @param that a ULID to be compared with + * @return -1, 0 or 1 as {@code this} is less than, equal to, or greater than + * {@code that} + */ + @Override + public int compareTo(Ulid that) { + + // used to compare as UNSIGNED longs + final long min = 0x8000000000000000L; + + final long a = this.msb + min; + final long b = that.msb + min; + + if (a > b) + return 1; + else if (a < b) + return -1; + + final long c = this.lsb + min; + final long d = that.lsb + min; + + if (c > d) + return 1; + else if (c < d) + return -1; + + return 0; + } + + String toString(char[] alphabet) { + + final char[] chars = new char[ULID_CHARS]; + + long time = this.msb >>> 16; + long random0 = ((this.msb & 0xffffL) << 24) | (this.lsb >>> 40); + long random1 = (this.lsb & 0xffffffffffL); + + chars[0x00] = alphabet[(int) (time >>> 45 & 0b11111)]; + chars[0x01] = alphabet[(int) (time >>> 40 & 0b11111)]; + chars[0x02] = alphabet[(int) (time >>> 35 & 0b11111)]; + chars[0x03] = alphabet[(int) (time >>> 30 & 0b11111)]; + chars[0x04] = alphabet[(int) (time >>> 25 & 0b11111)]; + chars[0x05] = alphabet[(int) (time >>> 20 & 0b11111)]; + chars[0x06] = alphabet[(int) (time >>> 15 & 0b11111)]; + chars[0x07] = alphabet[(int) (time >>> 10 & 0b11111)]; + chars[0x08] = alphabet[(int) (time >>> 5 & 0b11111)]; + chars[0x09] = alphabet[(int) (time & 0b11111)]; + + chars[0x0a] = alphabet[(int) (random0 >>> 35 & 0b11111)]; + chars[0x0b] = alphabet[(int) (random0 >>> 30 & 0b11111)]; + chars[0x0c] = alphabet[(int) (random0 >>> 25 & 0b11111)]; + chars[0x0d] = alphabet[(int) (random0 >>> 20 & 0b11111)]; + chars[0x0e] = alphabet[(int) (random0 >>> 15 & 0b11111)]; + chars[0x0f] = alphabet[(int) (random0 >>> 10 & 0b11111)]; + chars[0x10] = alphabet[(int) (random0 >>> 5 & 0b11111)]; + chars[0x11] = alphabet[(int) (random0 & 0b11111)]; + + chars[0x12] = alphabet[(int) (random1 >>> 35 & 0b11111)]; + chars[0x13] = alphabet[(int) (random1 >>> 30 & 0b11111)]; + chars[0x14] = alphabet[(int) (random1 >>> 25 & 0b11111)]; + chars[0x15] = alphabet[(int) (random1 >>> 20 & 0b11111)]; + chars[0x16] = alphabet[(int) (random1 >>> 15 & 0b11111)]; + chars[0x17] = alphabet[(int) (random1 >>> 10 & 0b11111)]; + chars[0x18] = alphabet[(int) (random1 >>> 5 & 0b11111)]; + chars[0x19] = alphabet[(int) (random1 & 0b11111)]; + + return new String(chars); + } + + static char[] toCharArray(String string) { + char[] chars = string == null ? null : string.toCharArray(); + if (!isValidCharArray(chars)) { + throw new IllegalArgumentException(String.format("Invalid ULID: \"%s\"", string)); + } + return chars; + } + + /* + * Checks if the string is a valid ULID. + * + * A valid ULID string is a sequence of 26 characters from Crockford's Base 32 + * alphabet. + * + * The first character of the input string must be between 0 and 7. + */ + static boolean isValidCharArray(final char[] chars) { + + if (chars == null || chars.length != ULID_CHARS) { + return false; // null or wrong size! + } + + // The time component has 48 bits. + // The base32 encoded time component has 50 bits. + // The time component cannot be greater than than 2^48-1. + // So the 2 first bits of the base32 decoded time component must be ZERO. + // As a consequence, the 1st char of the input string must be between 0 and 7. + if ((ALPHABET_VALUES[chars[0]] & 0b11000) != 0) { + // ULID specification: + // "Any attempt to decode or encode a ULID larger than this (time > 2^48-1) + // should be rejected by all implementations, to prevent overflow bugs." + return false; // time overflow! + } + + for (char aChar : chars) { + if (ALPHABET_VALUES[aChar] == -1) { + return false; // invalid character! + } + } + + return true; // It seems to be OK. + } +} diff --git a/src/main/java/com/andrewlalis/perfin/data/ulid/UlidCreator.java b/src/main/java/com/andrewlalis/perfin/data/ulid/UlidCreator.java new file mode 100644 index 0000000..4c5760a --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/data/ulid/UlidCreator.java @@ -0,0 +1,167 @@ +/* + * MIT License + * + * Copyright (c) 2020-2023 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.andrewlalis.perfin.data.ulid; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * A class that generates ULIDs. + *

+ * Both types of ULID can be easily created by this generator, i.e. monotonic + * and non-monotonic. + *

+ * In addition, a "non-standard" hash-based ULID can also be generated, in which + * the random component is replaced with the first 10 bytes of an SHA-256 hash. + */ +public final class UlidCreator { + + private UlidCreator() { + } + + /** + * Returns a ULID. + *

+ * The random component is reset for each new ULID generated. + * + * @return a ULID + */ + public static Ulid getUlid() { + return UlidFactoryHolder.INSTANCE.create(); + } + + /** + * Returns a ULID. + *

+ * The random component is reset for each new ULID generated. + * + * @param time the current time in milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @return a ULID + */ + public static Ulid getUlid(final long time) { + return UlidFactoryHolder.INSTANCE.create(time); + } + + /** + * Returns a Monotonic ULID. + *

+ * The random component is incremented for each new ULID generated in the same + * millisecond. + * + * @return a ULID + */ + public static Ulid getMonotonicUlid() { + return MonotonicFactoryHolder.INSTANCE.create(); + } + + /** + * Returns a Monotonic ULID. + *

+ * The random component is incremented for each new ULID generated in the same + * millisecond. + * + * @param time the current time in milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @return a ULID + */ + public static Ulid getMonotonicUlid(final long time) { + return MonotonicFactoryHolder.INSTANCE.create(time); + } + + /** + * Returns a Hash ULID. + *

+ * The random component is replaced with the first 10 bytes of an SHA-256 hash. + *

+ * It always returns the same ULID for a specific pair of {@code time} and + * {@code string}. + *

+ * Usage example: + * + *

{@code
+     * long time = file.getCreatedAt();
+     * String name = file.getFileName();
+     * Ulid ulid = UlidCreator.getHashUlid(time, name);
+     * }
+ * + * @param time the time in milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @param string a string to be hashed using SHA-256 algorithm. + * @return a ULID + * @since 5.2.0 + */ + public static Ulid getHashUlid(final long time, String string) { + byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + return getHashUlid(time, bytes); + } + + /** + * Returns a Hash ULID. + *

+ * The random component is replaced with the first 10 bytes of an SHA-256 hash. + *

+ * It always returns the same ULID for a specific pair of {@code time} and + * {@code bytes}. + *

+ * Usage example: + * + *

{@code
+     * long time = file.getCreatedAt();
+     * byte[] bytes = file.getFileBinary();
+     * Ulid ulid = UlidCreator.getHashUlid(time, bytes);
+     * }
+ * + * @param time the time in milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @param bytes a byte array to be hashed using SHA-256 algorithm. + * @return a ULID + * @since 5.2.0 + */ + public static Ulid getHashUlid(final long time, byte[] bytes) { + // Calculate the hash and take the first 10 bytes + byte[] hash = hasher("SHA-256").digest(bytes); + byte[] rand = Arrays.copyOf(hash, 10); + return new Ulid(time, rand); + } + + private static MessageDigest hasher(final String algorithm) { + try { + return MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("%s not supported", algorithm)); + } + } + + private static class UlidFactoryHolder { + static final UlidFactory INSTANCE = UlidFactory.newInstance(); + } + + private static class MonotonicFactoryHolder { + static final UlidFactory INSTANCE = UlidFactory.newMonotonicInstance(); + } +} diff --git a/src/main/java/com/andrewlalis/perfin/data/ulid/UlidFactory.java b/src/main/java/com/andrewlalis/perfin/data/ulid/UlidFactory.java new file mode 100644 index 0000000..21f2862 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/data/ulid/UlidFactory.java @@ -0,0 +1,508 @@ +/* + * MIT License + * + * Copyright (c) 2020-2023 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.andrewlalis.perfin.data.ulid; + +import java.security.SecureRandom; +import java.time.Clock; +import java.util.Objects; +import java.util.Random; +import java.util.function.IntFunction; +import java.util.function.LongFunction; +import java.util.function.LongSupplier; + +/** + * A class that actually generates ULIDs. + *

+ * This class is used by {@link UlidCreator}. + *

+ * You can use this class if you need to use a specific random generator + * strategy. However, most people just need {@link UlidCreator}. + *

+ * Instances of this class can behave in one of two ways: monotonic or + * non-monotonic (default). + *

+ * If the factory is monotonic, the random component is incremented by 1 if more + * than one ULID is generated within the same millisecond. + *

+ * The maximum ULIDs that can be generated per millisecond is 2^80. + */ +public final class UlidFactory { + + private final LongSupplier timeFunction; + private final LongFunction ulidFunction; + + // ****************************** + // Constructors + // ****************************** + + /** + * Default constructor. + */ + public UlidFactory() { + this(new UlidFunction()); + } + + private UlidFactory(LongFunction ulidFunction) { + this(ulidFunction, System::currentTimeMillis); + } + + private UlidFactory(LongFunction ulidFunction, LongSupplier timeFunction) { + + Objects.requireNonNull(ulidFunction, "ULID function must not be null"); + Objects.requireNonNull(timeFunction, "Time function must not be null"); + + this.ulidFunction = ulidFunction; + this.timeFunction = timeFunction; + + if (this.ulidFunction instanceof MonotonicFunction) { + // initialize the internal state of the monotonic function + ((MonotonicFunction) this.ulidFunction).initialize(this.timeFunction); + } + } + + /** + * Returns a new factory. + *

+ * It is equivalent to the default constructor {@code new UlidFactory()}. + * + * @return {@link UlidFactory} + */ + public static UlidFactory newInstance() { + return new UlidFactory(new UlidFunction()); + } + + /** + * Returns a new factory. + * + * @param random a {@link Random} generator + * @return {@link UlidFactory} + */ + public static UlidFactory newInstance(Random random) { + return new UlidFactory(new UlidFunction(random)); + } + + /** + * Returns a new factory. + *

+ * The given random function must return a long value. + * + * @param randomFunction a random function that returns a long value + * @return {@link UlidFactory} + */ + public static UlidFactory newInstance(LongSupplier randomFunction) { + return new UlidFactory(new UlidFunction(randomFunction)); + } + + /** + * Returns a new factory. + *

+ * The given random function must return a byte array. + * + * @param randomFunction a random function that returns a byte array + * @return {@link UlidFactory} + */ + public static UlidFactory newInstance(IntFunction randomFunction) { + return new UlidFactory(new UlidFunction(randomFunction)); + } + + /** + * Returns a new monotonic factory. + * + * @return {@link UlidFactory} + */ + public static UlidFactory newMonotonicInstance() { + return new UlidFactory(new MonotonicFunction()); + } + + /** + * Returns a new monotonic factory. + * + * @param random a {@link Random} generator + * @return {@link UlidFactory} + */ + public static UlidFactory newMonotonicInstance(Random random) { + return new UlidFactory(new MonotonicFunction(random)); + } + + /** + * Returns a new monotonic factory. + *

+ * The given random function must return a long value. + * + * @param randomFunction a random function that returns a long value + * @return {@link UlidFactory} + */ + public static UlidFactory newMonotonicInstance(LongSupplier randomFunction) { + return new UlidFactory(new MonotonicFunction(randomFunction)); + } + + /** + * Returns a new monotonic factory. + *

+ * The given random function must return a byte array. + * + * @param randomFunction a random function that returns a byte array + * @return {@link UlidFactory} + */ + public static UlidFactory newMonotonicInstance(IntFunction randomFunction) { + return new UlidFactory(new MonotonicFunction(randomFunction)); + } + + /** + * Returns a new monotonic factory. + * + * @param random a {@link Random} generator + * @param clock a clock instance that provides the current time in + * milliseconds, measured from the UNIX epoch of 1970-01-01T00:00Z + * (UTC) + * @return {@link UlidFactory} + */ + static UlidFactory newMonotonicInstance(Random random, Clock clock) { + Objects.requireNonNull(clock, "Clock instant must not be null"); + return new UlidFactory(new MonotonicFunction(random), clock::millis); + } + + /** + * Returns a new monotonic factory. + *

+ * The given random function must return a long value. + * + * @param randomFunction a random function that returns a long value + * @param clock a clock instance that provides the current time in + * milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @return {@link UlidFactory} + */ + static UlidFactory newMonotonicInstance(LongSupplier randomFunction, Clock clock) { + Objects.requireNonNull(clock, "Clock instant must not be null"); + return new UlidFactory(new MonotonicFunction(randomFunction), clock::millis); + } + + /** + * Returns a new monotonic factory. + *

+ * The given random function must return a byte array. + * + * @param randomFunction a random function that returns a byte array + * @param clock a clock instance that provides the current time in + * milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @return {@link UlidFactory} + */ + static UlidFactory newMonotonicInstance(IntFunction randomFunction, Clock clock) { + Objects.requireNonNull(clock, "Clock instant must not be null"); + return new UlidFactory(new MonotonicFunction(randomFunction), clock::millis); + } + + /** + * Returns a new monotonic factory. + * + * @param random a {@link Random} generator + * @param timeFunction a function that returns the current time in milliseconds, + * measured from the UNIX epoch of 1970-01-01T00:00Z (UTC) + * @return {@link UlidFactory} + */ + public static UlidFactory newMonotonicInstance(Random random, LongSupplier timeFunction) { + return new UlidFactory(new MonotonicFunction(random), timeFunction); + } + + /** + * Returns a new monotonic factory. + *

+ * The given random function must return a long value. + * + * @param randomFunction a random function that returns a long value + * @param timeFunction a function that returns the current time in + * milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @return {@link UlidFactory} + */ + public static UlidFactory newMonotonicInstance(LongSupplier randomFunction, LongSupplier timeFunction) { + return new UlidFactory(new MonotonicFunction(randomFunction), timeFunction); + } + + /** + * Returns a new monotonic factory. + *

+ * The given random function must return a byte array. + * + * @param randomFunction a random function that returns a byte array + * @param timeFunction a function that returns the current time in + * milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @return {@link UlidFactory} + */ + public static UlidFactory newMonotonicInstance(IntFunction randomFunction, LongSupplier timeFunction) { + return new UlidFactory(new MonotonicFunction(randomFunction), timeFunction); + } + + // ****************************** + // Public methods + // ****************************** + + /** + * Returns a new ULID. + * + * @return a ULID + */ + public synchronized Ulid create() { + return this.ulidFunction.apply(timeFunction.getAsLong()); + } + + /** + * Returns a new ULID. + * + * @param time the current time in milliseconds, measured from the UNIX epoch of + * 1970-01-01T00:00Z (UTC) + * @return a ULID + */ + public synchronized Ulid create(final long time) { + return this.ulidFunction.apply(time); + } + + // ****************************** + // Package-private inner classes + // ****************************** + + /** + * Function that creates ULIDs. + */ + static final class UlidFunction implements LongFunction { + + private final IRandom random; + + private UlidFunction(IRandom random) { + this.random = random; + } + + public UlidFunction() { + this(IRandom.newInstance()); + } + + public UlidFunction(Random random) { + this(IRandom.newInstance(random)); + } + + public UlidFunction(LongSupplier randomFunction) { + this(IRandom.newInstance(randomFunction)); + } + + public UlidFunction(IntFunction randomFunction) { + this(IRandom.newInstance(randomFunction)); + } + + @Override + public Ulid apply(final long time) { + if (this.random instanceof ByteRandom) { + return new Ulid(time, this.random.nextBytes(Ulid.RANDOM_BYTES)); + } else { + final long msb = (time << 16) | (this.random.nextLong() & 0xffffL); + final long lsb = this.random.nextLong(); + return new Ulid(msb, lsb); + } + } + } + + /** + * Function that creates Monotonic ULIDs. + */ + static final class MonotonicFunction implements LongFunction { + + private Ulid lastUlid; + + private final IRandom random; + + // Used to preserve monotonicity when the system clock is + // adjusted by NTP after a small clock drift or when the + // system clock jumps back by 1 second due to leap second. + static final int CLOCK_DRIFT_TOLERANCE = 10_000; + + private MonotonicFunction(IRandom random) { + this.random = random; + } + + public MonotonicFunction() { + this(IRandom.newInstance()); + } + + public MonotonicFunction(Random random) { + this(IRandom.newInstance(random)); + } + + public MonotonicFunction(LongSupplier randomFunction) { + this(IRandom.newInstance(randomFunction)); + } + + public MonotonicFunction(IntFunction randomFunction) { + this(IRandom.newInstance(randomFunction)); + } + + void initialize(LongSupplier timeFunction) { + // initialize the factory with the instant 1970-01-01 00:00:00.000 UTC + this.lastUlid = new Ulid(0L, this.random.nextBytes(Ulid.RANDOM_BYTES)); + } + + @Override + public synchronized Ulid apply(final long time) { + + final long lastTime = lastUlid.getTime(); + + // Check if the current time is the same as the previous time or has moved + // backwards after a small system clock adjustment or after a leap second. + // Drift tolerance = (previous_time - 10s) < current_time <= previous_time + if ((time > lastTime - CLOCK_DRIFT_TOLERANCE) && (time <= lastTime)) { + this.lastUlid = this.lastUlid.increment(); + } else { + if (this.random instanceof ByteRandom) { + this.lastUlid = new Ulid(time, this.random.nextBytes(Ulid.RANDOM_BYTES)); + } else { + final long msb = (time << 16) | (this.random.nextLong() & 0xffffL); + final long lsb = this.random.nextLong(); + this.lastUlid = new Ulid(msb, lsb); + } + } + + return new Ulid(this.lastUlid); + } + } + + static interface IRandom { + + public long nextLong(); + + public byte[] nextBytes(int length); + + static IRandom newInstance() { + return new ByteRandom(); + } + + static IRandom newInstance(Random random) { + if (random == null) { + return new ByteRandom(); + } else { + if (random instanceof SecureRandom) { + return new ByteRandom(random); + } else { + return new LongRandom(random); + } + } + } + + static IRandom newInstance(LongSupplier randomFunction) { + return new LongRandom(randomFunction); + } + + static IRandom newInstance(IntFunction randomFunction) { + return new ByteRandom(randomFunction); + } + } + + static class LongRandom implements IRandom { + + private final LongSupplier randomFunction; + + public LongRandom() { + this(newRandomFunction(null)); + } + + public LongRandom(Random random) { + this(newRandomFunction(random)); + } + + public LongRandom(LongSupplier randomFunction) { + this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null); + } + + @Override + public long nextLong() { + return randomFunction.getAsLong(); + } + + @Override + public byte[] nextBytes(int length) { + + int shift = 0; + long random = 0; + final byte[] bytes = new byte[length]; + + for (int i = 0; i < length; i++) { + if (shift < Byte.SIZE) { + shift = Long.SIZE; + random = randomFunction.getAsLong(); + } + shift -= Byte.SIZE; // 56, 48, 40... + bytes[i] = (byte) (random >>> shift); + } + + return bytes; + } + + static LongSupplier newRandomFunction(Random random) { + final Random entropy = random != null ? random : new SecureRandom(); + return entropy::nextLong; + } + } + + static class ByteRandom implements IRandom { + + private final IntFunction randomFunction; + + public ByteRandom() { + this(newRandomFunction(null)); + } + + public ByteRandom(Random random) { + this(newRandomFunction(random)); + } + + public ByteRandom(IntFunction randomFunction) { + this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null); + } + + @Override + public long nextLong() { + long number = 0; + byte[] bytes = this.randomFunction.apply(Long.BYTES); + for (int i = 0; i < Long.BYTES; i++) { + number = (number << 8) | (bytes[i] & 0xff); + } + return number; + } + + @Override + public byte[] nextBytes(int length) { + return this.randomFunction.apply(length); + } + + static IntFunction newRandomFunction(Random random) { + final Random entropy = random != null ? random : new SecureRandom(); + return (final int length) -> { + final byte[] bytes = new byte[length]; + entropy.nextBytes(bytes); + return bytes; + }; + } + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 3936074..f70e4dc 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -7,8 +7,6 @@ module com.andrewlalis.perfin { requires com.fasterxml.jackson.databind; - requires com.github.f4b6a3.ulid; - requires java.sql; exports com.andrewlalis.perfin to javafx.graphics;