/* * 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.io.Serializable; import java.time.Instant; import java.util.UUID; /** * This class represents a ULID. */ public final class Ulid implements Serializable, Comparable { private final long msb; private final long lsb; protected static final long TIME_MAX = 281474976710655L; // 2^48 - 1 public static final int ULID_LENGTH = 26; public static final int TIME_LENGTH = 10; public static final int RANDOM_LENGTH = 16; public static final int ULID_BYTES_LENGTH = 16; public static final int TIME_BYTES_LENGTH = 6; public static final int RANDOM_BYTES_LENGTH = 10; // 0xffffffffffffffffL + 1 = 0x0000000000000000L private static final long INCREMENT_OVERFLOW = 0x0000000000000000L; protected static final char[] ALPHABET_UPPERCASE = // { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', // 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' }; protected static final char[] ALPHABET_LOWERCASE = // { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', // 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' }; protected static final long[] ALPHABET_VALUES = new long[128]; static { for (int i = 0; i < ALPHABET_VALUES.length; i++) { ALPHABET_VALUES[i] = -1; } // Numbers ALPHABET_VALUES['0'] = 0x00; ALPHABET_VALUES['1'] = 0x01; ALPHABET_VALUES['2'] = 0x02; ALPHABET_VALUES['3'] = 0x03; ALPHABET_VALUES['4'] = 0x04; ALPHABET_VALUES['5'] = 0x05; ALPHABET_VALUES['6'] = 0x06; ALPHABET_VALUES['7'] = 0x07; ALPHABET_VALUES['8'] = 0x08; ALPHABET_VALUES['9'] = 0x09; // Lower case ALPHABET_VALUES['a'] = 0x0a; ALPHABET_VALUES['b'] = 0x0b; ALPHABET_VALUES['c'] = 0x0c; ALPHABET_VALUES['d'] = 0x0d; ALPHABET_VALUES['e'] = 0x0e; ALPHABET_VALUES['f'] = 0x0f; ALPHABET_VALUES['g'] = 0x10; ALPHABET_VALUES['h'] = 0x11; ALPHABET_VALUES['j'] = 0x12; ALPHABET_VALUES['k'] = 0x13; ALPHABET_VALUES['m'] = 0x14; ALPHABET_VALUES['n'] = 0x15; ALPHABET_VALUES['p'] = 0x16; ALPHABET_VALUES['q'] = 0x17; ALPHABET_VALUES['r'] = 0x18; ALPHABET_VALUES['s'] = 0x19; ALPHABET_VALUES['t'] = 0x1a; ALPHABET_VALUES['v'] = 0x1b; ALPHABET_VALUES['w'] = 0x1c; ALPHABET_VALUES['x'] = 0x1d; ALPHABET_VALUES['y'] = 0x1e; ALPHABET_VALUES['z'] = 0x1f; // Lower case OIL ALPHABET_VALUES['o'] = 0x00; ALPHABET_VALUES['i'] = 0x01; ALPHABET_VALUES['l'] = 0x01; // Upper case ALPHABET_VALUES['A'] = 0x0a; ALPHABET_VALUES['B'] = 0x0b; ALPHABET_VALUES['C'] = 0x0c; ALPHABET_VALUES['D'] = 0x0d; ALPHABET_VALUES['E'] = 0x0e; ALPHABET_VALUES['F'] = 0x0f; ALPHABET_VALUES['G'] = 0x10; ALPHABET_VALUES['H'] = 0x11; ALPHABET_VALUES['J'] = 0x12; ALPHABET_VALUES['K'] = 0x13; ALPHABET_VALUES['M'] = 0x14; ALPHABET_VALUES['N'] = 0x15; ALPHABET_VALUES['P'] = 0x16; ALPHABET_VALUES['Q'] = 0x17; ALPHABET_VALUES['R'] = 0x18; ALPHABET_VALUES['S'] = 0x19; ALPHABET_VALUES['T'] = 0x1a; ALPHABET_VALUES['V'] = 0x1b; ALPHABET_VALUES['W'] = 0x1c; ALPHABET_VALUES['X'] = 0x1d; ALPHABET_VALUES['Y'] = 0x1e; ALPHABET_VALUES['Z'] = 0x1f; // Upper case OIL ALPHABET_VALUES['O'] = 0x00; ALPHABET_VALUES['I'] = 0x01; ALPHABET_VALUES['L'] = 0x01; } private static final long serialVersionUID = 2625269413446854731L; public Ulid(long mostSignificantBits, long leastSignificantBits) { this.msb = mostSignificantBits; this.lsb = leastSignificantBits; } public static Ulid of(Ulid ulid) { return new Ulid(ulid.getMostSignificantBits(), ulid.getLeastSignificantBits()); } public static Ulid of(UUID uuid) { return new Ulid(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); } // TODO: test public static Ulid of(byte[] bytes) { if (bytes == null || bytes.length != ULID_BYTES_LENGTH) { throw new IllegalArgumentException("Invalid ULID bytes"); } long long0 = 0; long long1 = 0; long0 |= (bytes[0x0] & 0xffL) << 56; long0 |= (bytes[0x1] & 0xffL) << 48; long0 |= (bytes[0x2] & 0xffL) << 40; long0 |= (bytes[0x3] & 0xffL) << 32; long0 |= (bytes[0x4] & 0xffL) << 24; long0 |= (bytes[0x5] & 0xffL) << 16; long0 |= (bytes[0x6] & 0xffL) << 8; long0 |= (bytes[0x7] & 0xffL); long1 |= (bytes[0x8] & 0xffL) << 56; long1 |= (bytes[0x9] & 0xffL) << 48; long1 |= (bytes[0xa] & 0xffL) << 40; long1 |= (bytes[0xb] & 0xffL) << 32; long1 |= (bytes[0xc] & 0xffL) << 24; long1 |= (bytes[0xd] & 0xffL) << 16; long1 |= (bytes[0xe] & 0xffL) << 8; long1 |= (bytes[0xf] & 0xffL); return new Ulid(long0, long1); } // TODO: optimize public static Ulid of(String string) { final char[] chars = toCharArray(string); long tm = 0; long r1 = 0; long r2 = 0; tm |= ALPHABET_VALUES[chars[0x00]] << 45; tm |= ALPHABET_VALUES[chars[0x01]] << 40; tm |= ALPHABET_VALUES[chars[0x02]] << 35; tm |= ALPHABET_VALUES[chars[0x03]] << 30; tm |= ALPHABET_VALUES[chars[0x04]] << 25; tm |= ALPHABET_VALUES[chars[0x05]] << 20; tm |= ALPHABET_VALUES[chars[0x06]] << 15; tm |= ALPHABET_VALUES[chars[0x07]] << 10; tm |= ALPHABET_VALUES[chars[0x08]] << 5; tm |= ALPHABET_VALUES[chars[0x09]]; r1 |= ALPHABET_VALUES[chars[0x0a]] << 35; r1 |= ALPHABET_VALUES[chars[0x0b]] << 30; r1 |= ALPHABET_VALUES[chars[0x0c]] << 25; r1 |= ALPHABET_VALUES[chars[0x0d]] << 20; r1 |= ALPHABET_VALUES[chars[0x0e]] << 15; r1 |= ALPHABET_VALUES[chars[0x0f]] << 10; r1 |= ALPHABET_VALUES[chars[0x10]] << 5; r1 |= ALPHABET_VALUES[chars[0x11]]; r2 |= ALPHABET_VALUES[chars[0x12]] << 35; r2 |= ALPHABET_VALUES[chars[0x13]] << 30; r2 |= ALPHABET_VALUES[chars[0x14]] << 25; r2 |= ALPHABET_VALUES[chars[0x15]] << 20; r2 |= ALPHABET_VALUES[chars[0x16]] << 15; r2 |= ALPHABET_VALUES[chars[0x17]] << 10; r2 |= ALPHABET_VALUES[chars[0x18]] << 5; r2 |= ALPHABET_VALUES[chars[0x19]]; final long msb = (tm << 16) | (r1 >>> 24); final long lsb = (r1 << 40) | (r2 & 0xffffffffffL); return new Ulid(msb, lsb); } public static Ulid of(long time, byte[] random) { if ((time & 0xffff000000000000L) != 0) { throw new IllegalArgumentException("Invalid time value"); } if (random == null || random.length != RANDOM_BYTES_LENGTH) { throw new IllegalArgumentException("Invalid random bytes"); } long msb = 0; long lsb = 0; msb |= time << 16; msb |= (long) (random[0x0] & 0xff) << 8; msb |= (long) (random[0x1] & 0xff); lsb |= (long) (random[0x2] & 0xff) << 56; lsb |= (long) (random[0x3] & 0xff) << 48; lsb |= (long) (random[0x4] & 0xff) << 40; lsb |= (long) (random[0x5] & 0xff) << 32; lsb |= (long) (random[0x6] & 0xff) << 24; lsb |= (long) (random[0x7] & 0xff) << 16; lsb |= (long) (random[0x8] & 0xff) << 8; lsb |= (long) (random[0x9] & 0xff); return new Ulid(msb, lsb); } public UUID toUuid() { return new UUID(this.msb, this.lsb); } // TODO: test public UUID toUuid4() { final long msb4 = (this.msb & 0xffffffffffff0fffL) | 0x0000000000004000L; // apply version 4 final long lsb4 = (this.lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // apply variant RFC-4122 return new UUID(msb4, lsb4); } // TODO: test public byte[] toBytes() { final byte[] bytes = new byte[ULID_BYTES_LENGTH]; 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; } // TODO: test public byte[] toBytes4() { return Ulid.of(this.toUuid4()).toBytes(); } @Override public String toString() { return this.toUpperCase(); } // TODO: test public String toUpperCase() { return toString(ALPHABET_UPPERCASE); } // TODO: test public String toUpperCase4() { return Ulid.of(this.toUuid4()).toUpperCase(); } // TODO: test public String toLowerCase() { return toString(ALPHABET_LOWERCASE); } // TODO: test public String toLowerCase4() { return Ulid.of(this.toUuid4()).toLowerCase(); } public long getTime() { return this.msb >>> 16; } public Instant getInstant() { return Instant.ofEpochMilli(this.getTime()); } public long getMostSignificantBits() { return this.msb; } public long getLeastSignificantBits() { return this.lsb; } // TODO: test public Ulid increment() { long msb1 = this.msb; long lsb1 = this.lsb + 1; // Increment the LSB if (lsb1 == INCREMENT_OVERFLOW) { // Increment the random bits of the MSB msb1 = (msb1 & 0xffffffffffff0000L) | ((msb1 + 1) & 0x000000000000ffffL); } return new Ulid(msb1, lsb1); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (lsb ^ (lsb >>> 32)); result = prime * result + (int) (msb ^ (msb >>> 32)); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Ulid other = (Ulid) obj; if (lsb != other.lsb) return false; if (msb != other.msb) return false; return true; } @Override public int compareTo(Ulid other) { if (this.msb < other.msb) return -1; if (this.msb > other.msb) return 1; if (this.lsb < other.lsb) return -1; if (this.lsb > other.lsb) return 1; return 0; } // TODO: optimize protected String toString(char[] alphabet) { final char[] chars = new char[ULID_LENGTH]; long time = this.msb >>> 16; long random1 = ((this.msb & 0xffffL) << 24) | (this.lsb >>> 40); long random2 = (this.lsb & 0xffffffffffL); chars[0x00] = alphabet[(int) (time >>> 45 & 0b11111)]; chars[0x01] = alphabet[(int) (time >>> 40 & 0b11111)]; chars[0x02] = alphabet[(int) (time >>> 35 & 0b11111)]; chars[0x03] = alphabet[(int) (time >>> 30 & 0b11111)]; chars[0x04] = alphabet[(int) (time >>> 25 & 0b11111)]; chars[0x05] = alphabet[(int) (time >>> 20 & 0b11111)]; chars[0x06] = alphabet[(int) (time >>> 15 & 0b11111)]; chars[0x07] = alphabet[(int) (time >>> 10 & 0b11111)]; chars[0x08] = alphabet[(int) (time >>> 5 & 0b11111)]; chars[0x09] = alphabet[(int) (time & 0b11111)]; chars[0x0a] = alphabet[(int) (random1 >>> 35 & 0b11111)]; chars[0x0b] = alphabet[(int) (random1 >>> 30 & 0b11111)]; chars[0x0c] = alphabet[(int) (random1 >>> 25 & 0b11111)]; chars[0x0d] = alphabet[(int) (random1 >>> 20 & 0b11111)]; chars[0x0e] = alphabet[(int) (random1 >>> 15 & 0b11111)]; chars[0x0f] = alphabet[(int) (random1 >>> 10 & 0b11111)]; chars[0x10] = alphabet[(int) (random1 >>> 5 & 0b11111)]; chars[0x11] = alphabet[(int) (random1 & 0b11111)]; chars[0x12] = alphabet[(int) (random2 >>> 35 & 0b11111)]; chars[0x13] = alphabet[(int) (random2 >>> 30 & 0b11111)]; chars[0x14] = alphabet[(int) (random2 >>> 25 & 0b11111)]; chars[0x15] = alphabet[(int) (random2 >>> 20 & 0b11111)]; chars[0x16] = alphabet[(int) (random2 >>> 15 & 0b11111)]; chars[0x17] = alphabet[(int) (random2 >>> 10 & 0b11111)]; chars[0x18] = alphabet[(int) (random2 >>> 5 & 0b11111)]; chars[0x19] = alphabet[(int) (random2 & 0b11111)]; return new String(chars); } public static boolean isValidString(String string) { return isValidArray(string == null ? null : string.toCharArray()); } /** * Checks if the string is a valid ULID. * * A valid ULID string is a sequence of 26 characters from Crockford's base 32 * alphabet. * * @param chars a char array * @return boolean true if valid */ protected static boolean isValidArray(final char[] chars) { if (chars == null || chars.length != ULID_LENGTH) { return false; // null or wrong size! } // the two extra bits added by base-32 encoding must be zero if ((ALPHABET_VALUES[chars[0]] & 0b11000) != 0) { return false; // overflow! } for (int i = 0; i < chars.length; i++) { if (ALPHABET_VALUES[chars[i]] == -1) { return false; // invalid character! } } return true; // It seems to be OK. } protected static char[] toCharArray(String string) { char[] chars = string == null ? new char[0] : string.toCharArray(); if (!isValidArray(chars)) { throw new IllegalArgumentException(String.format("Invalid ULID: \"%s\"", string)); } return chars; } }