/*
* sulky-modules - several general-purpose modules.
* Copyright (C) 2007-2016 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2007-2016 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.sulky.ulid;
import java.io.Serializable;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.Random;
/*
* https://github.com/alizain/ulid
*/
public class ULID
{
private static final char[] ENCODING_CHARS = {
'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',
};
private static final byte[] DECODING_CHARS = new byte[128];
static
{
for(int i=0;i<DECODING_CHARS.length;i++)
{
DECODING_CHARS[i] = -1; // illegal value
}
DECODING_CHARS['0'] = 0;
DECODING_CHARS['O'] = 0;
DECODING_CHARS['o'] = 0;
DECODING_CHARS['1'] = 1;
DECODING_CHARS['I'] = 1;
DECODING_CHARS['i'] = 1;
DECODING_CHARS['L'] = 1;
DECODING_CHARS['l'] = 1;
DECODING_CHARS['2'] = 2;
DECODING_CHARS['3'] = 3;
DECODING_CHARS['4'] = 4;
DECODING_CHARS['5'] = 5;
DECODING_CHARS['6'] = 6;
DECODING_CHARS['7'] = 7;
DECODING_CHARS['8'] = 8;
DECODING_CHARS['9'] = 9;
DECODING_CHARS['A'] = 10;
DECODING_CHARS['a'] = 10;
DECODING_CHARS['B'] = 11;
DECODING_CHARS['b'] = 11;
DECODING_CHARS['C'] = 12;
DECODING_CHARS['c'] = 12;
DECODING_CHARS['D'] = 13;
DECODING_CHARS['d'] = 13;
DECODING_CHARS['E'] = 14;
DECODING_CHARS['e'] = 14;
DECODING_CHARS['F'] = 15;
DECODING_CHARS['f'] = 15;
DECODING_CHARS['G'] = 16;
DECODING_CHARS['g'] = 16;
DECODING_CHARS['H'] = 17;
DECODING_CHARS['h'] = 17;
DECODING_CHARS['J'] = 18;
DECODING_CHARS['j'] = 18;
DECODING_CHARS['K'] = 19;
DECODING_CHARS['k'] = 19;
DECODING_CHARS['M'] = 20;
DECODING_CHARS['m'] = 20;
DECODING_CHARS['N'] = 21;
DECODING_CHARS['n'] = 21;
DECODING_CHARS['P'] = 22;
DECODING_CHARS['p'] = 22;
DECODING_CHARS['P'] = 22;
DECODING_CHARS['p'] = 22;
DECODING_CHARS['Q'] = 23;
DECODING_CHARS['q'] = 23;
DECODING_CHARS['R'] = 24;
DECODING_CHARS['r'] = 24;
DECODING_CHARS['S'] = 25;
DECODING_CHARS['s'] = 25;
DECODING_CHARS['T'] = 26;
DECODING_CHARS['t'] = 26;
DECODING_CHARS['V'] = 27;
DECODING_CHARS['v'] = 27;
DECODING_CHARS['W'] = 28;
DECODING_CHARS['w'] = 28;
DECODING_CHARS['X'] = 29;
DECODING_CHARS['x'] = 29;
DECODING_CHARS['Y'] = 30;
DECODING_CHARS['y'] = 30;
DECODING_CHARS['Z'] = 31;
DECODING_CHARS['z'] = 31;
}
private static final int MASK = 0x1F;
private static final int MASK_BITS = 5;
private static final long TIMESTAMP_MASK = 0x0000_FFFF_FFFF_FFFFL;
/*
* http://crockford.com/wrmg/base32.html
*/
static void internalAppendCrockford(StringBuilder builder, long value, int count)
{
for(int i = count-1; i >= 0; i--)
{
int index = (int)((value >>> (i * MASK_BITS)) & MASK);
builder.append(ENCODING_CHARS[index]);
}
}
static long internalParseCrockford(String input)
{
Objects.requireNonNull(input, "input must not be null!");
long result = 0;
int length = input.length();
if(length > 12)
{
throw new IllegalArgumentException("input length must not exceed 12 but was "+length+"!");
}
for(int i=0;i<length;i++)
{
char current = input.charAt(i);
byte value = -1;
if(current < DECODING_CHARS.length)
{
value = DECODING_CHARS[current];
}
if(value < 0)
{
throw new IllegalArgumentException("Illegal character '"+current+"'!");
}
result |= ((long)value) << ((length - 1 - i)*MASK_BITS);
}
return result;
}
/*
* http://crockford.com/wrmg/base32.html
*/
static void internalWriteCrockford(char[] buffer, long value, int count, int offset)
{
for(int i = 0; i < count; i++)
{
int index = (int)((value >>> ((count - i - 1) * MASK_BITS)) & MASK);
buffer[offset+i] = ENCODING_CHARS[index];
}
}
static String internalUIDString(long timeStamp, Random random)
{
// this will be extremely important in the summer of 10889.
timeStamp = timeStamp & TIMESTAMP_MASK;
char[] buffer = new char[26];
internalWriteCrockford(buffer, timeStamp, 10, 0);
// could use nextBytes(byte[] bytes) instead
internalWriteCrockford(buffer, random.nextLong(), 8, 10);
internalWriteCrockford(buffer, random.nextLong(), 8, 18);
return new String(buffer);
}
static void internalAppendULID(StringBuilder builder, long timeStamp, Random random)
{
// this will be extremely important in the summer of 10889.
timeStamp = timeStamp & TIMESTAMP_MASK;
internalAppendCrockford(builder, timeStamp, 10);
// could use nextBytes(byte[] bytes) instead
internalAppendCrockford(builder, random.nextLong(), 8);
internalAppendCrockford(builder, random.nextLong(), 8);
}
static Value internalNextValue(long timeStamp, Random random)
{
// could use nextBytes(byte[] bytes) instead
long mostSignificantBits = random.nextLong();
long leastSignificantBits = random.nextLong();
mostSignificantBits &= 0xFFFF;
mostSignificantBits |= (timeStamp << 16);
return new Value(mostSignificantBits, leastSignificantBits);
}
private final Random random;
public ULID()
{
this(new SecureRandom());
}
public ULID(Random random)
{
Objects.requireNonNull(random, "random must not be null!");
this.random = random;
}
public void appendULID(StringBuilder stringBuilder)
{
Objects.requireNonNull(stringBuilder, "stringBuilder must not be null!");
internalAppendULID(stringBuilder, System.currentTimeMillis(), random);
}
public String nextULID()
{
return internalUIDString(System.currentTimeMillis(), random);
}
public Value nextValue()
{
return internalNextValue(System.currentTimeMillis(), random);
}
public static Value parseULID(String ulidString)
{
Objects.requireNonNull(ulidString, "ulidString must not be null!");
if(ulidString.length() != 26)
{
throw new IllegalArgumentException("ulidString must be exactly 26 chars long.");
}
String timeString = ulidString.substring(0, 10);
String part1String = ulidString.substring(10, 18);
String part2String = ulidString.substring(18);
long time = internalParseCrockford(timeString);
long part1 = internalParseCrockford(part1String);
long part2 = internalParseCrockford(part2String);
long most = (time << 16) | (part1 >>> 24);
long least = part2 | (part1 << 40);
return new Value(most, least);
}
public static Value fromBytes(byte[] data)
{
Objects.requireNonNull(data, "data must not be null!");
if(data.length != 16)
{
throw new IllegalArgumentException("data must be 16 bytes in length!");
}
long mostSignificantBits = 0;
long leastSignificantBits = 0;
for (int i=0; i<8; i++)
{
mostSignificantBits = (mostSignificantBits << 8) | (data[i] & 0xff);
}
for (int i=8; i<16; i++)
{
leastSignificantBits = (leastSignificantBits << 8) | (data[i] & 0xff);
}
return new Value(mostSignificantBits, leastSignificantBits);
}
public static class Value
implements Comparable<Value>, Serializable
{
private static final long serialVersionUID = -3563159514112487717L;
/*
* The most significant 64 bits of this ULID.
*/
private final long mostSignificantBits;
/*
* The least significant 64 bits of this ULID.
*/
private final long leastSignificantBits;
public Value(long mostSignificantBits, long leastSignificantBits)
{
this.mostSignificantBits = mostSignificantBits;
this.leastSignificantBits = leastSignificantBits;
}
/**
* Returns the most significant 64 bits of this ULID's 128 bit value.
*
* @return The most significant 64 bits of this ULID's 128 bit value
*/
public long getMostSignificantBits() {
return mostSignificantBits;
}
/**
* Returns the least significant 64 bits of this ULID's 128 bit value.
*
* @return The least significant 64 bits of this ULID's 128 bit value
*/
public long getLeastSignificantBits() {
return leastSignificantBits;
}
public long timestamp()
{
return mostSignificantBits >>> 16;
}
public byte[] toBytes()
{
byte[] result=new byte[16];
for (int i=0; i<8; i++)
{
result[i] = (byte)((mostSignificantBits >> ((7-i)*8)) & 0xFF);
}
for (int i=8; i<16; i++)
{
result[i] = (byte)((leastSignificantBits >> ((15-i)*8)) & 0xFF);
}
return result;
}
@Override
public int hashCode() {
long hilo = mostSignificantBits ^ leastSignificantBits;
return ((int)(hilo >> 32)) ^ (int) hilo;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Value value = (Value) o;
return mostSignificantBits == value.mostSignificantBits
&& leastSignificantBits == value.leastSignificantBits;
}
@Override
public int compareTo(Value val)
{
// The ordering is intentionally set up so that the ULIDs
// can simply be numerically compared as two numbers
return (this.mostSignificantBits < val.mostSignificantBits ? -1 :
(this.mostSignificantBits > val.mostSignificantBits ? 1 :
(this.leastSignificantBits < val.leastSignificantBits ? -1 :
(this.leastSignificantBits > val.leastSignificantBits ? 1 :
0))));
}
public String toString()
{
char[] buffer = new char[26];
internalWriteCrockford(buffer, timestamp(), 10, 0);
long value = ((mostSignificantBits & 0xFFFFL) << 24);
long interim = (leastSignificantBits >>> 40);
value = value | interim;
internalWriteCrockford(buffer, value, 8, 10);
internalWriteCrockford(buffer, leastSignificantBits, 8, 18);
return new String(buffer);
}
}
}