Skip to content

Commit e93d1e0

Browse files
authored
Merge pull request #10 from azam/develop
Release v1.0.3
2 parents aac9049 + f05628b commit e93d1e0

File tree

5 files changed

+330
-7
lines changed

5 files changed

+330
-7
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>io.azam.ulidj</groupId>
55
<artifactId>ulidj</artifactId>
6-
<version>1.0.2</version>
6+
<version>1.0.3</version>
77
<name>ulidj</name>
88
<description>ULID (Universally Unique Lexicographically Sortable Identifier) generator and parser for Java.</description>
99
<url>https://github.com/azam/ulidj</url>

readme.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
ULID (Universally Unique Lexicographically Sortable Identifier) generator and parser for Java.
88

9-
Refer [alizain/ulid](https://github.com/alizain/ulid) for a more detailed ULID specification.
9+
Refer [ulid/spec](https://github.com/ulid/spec) for a more detailed ULID specification.
1010

1111
## License
1212

@@ -42,7 +42,7 @@ Add the following tag to `dependencies` tag in your `pom.xml` file. Change the v
4242
<dependency>
4343
<groupId>io.azam.ulidj</groupId>
4444
<artifactId>ulidj</artifactId>
45-
<version>1.0.2</version>
45+
<version>1.0.3</version>
4646
</dependency>
4747
```
4848

@@ -68,6 +68,15 @@ assert ts == 123456789000L;
6868
byte[] entropy = ULID.getEntropy(ulid);
6969
```
7070

71+
Monotonic ULID generation example:
72+
73+
```java
74+
MonotonicULID ulid = new MonotonicULID();
75+
String ulid1 = ulid.next();
76+
String ulid2 = ulid.next();
77+
String ulid3 = ulid.next();
78+
```
79+
7180
## Develop
7281

7382
Please run the following before sending a PR:
@@ -78,4 +87,4 @@ Please run the following before sending a PR:
7887
## Prior Art
7988

8089
- [Lewiscowles1986/jULID](https://github.com/Lewiscowles1986/jULID)
81-
- [alizain/ulid](https://github.com/alizain/ulid)
90+
- [ulid/spec](https://github.com/ulid/spec)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2016 Azamshul Azizy
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7+
* associated documentation files (the "Software"), to deal in the Software without restriction,
8+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
9+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
16+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package io.azam.ulidj;
22+
23+
import java.security.SecureRandom;
24+
import java.util.Random;
25+
26+
/**
27+
* Monotonic instance of ULID. ULID spec defines monotonicity behavior as if a ULID is to be
28+
* generated in the same millisecond, the entropy(random) component is to be incremented by 1-bit in
29+
* the least significant bit with carryover.<br>
30+
* <br>
31+
* In practice this behavior is however applicable to ULID's generated from the same source (in
32+
* Java, from the same instance), else an external synchronization is needed. Hence, instance of
33+
* this class will produce ULID in monotonic order only if called from the same instance.<br>
34+
* <br>
35+
* Usage example:<br>
36+
* <br>
37+
*
38+
* <pre>
39+
* MonotonicULID ulid = new MonotonicULID();
40+
* String ulid1 = ulid.next();
41+
* String ulid2 = ulid.next();
42+
* String ulid3 = ulid.next();
43+
* </pre>
44+
*
45+
* @see <a href="https://github.com/ulid/spec">ULID</a>
46+
*
47+
* @author azam
48+
* @since 1.0.3
49+
*/
50+
public class MonotonicULID {
51+
private final Random random;
52+
private long lastTimestamp;
53+
private byte[] lastEntropy;
54+
55+
/**
56+
* Generate a monotonic ULID generator instance, backed by {@link java.security.SecureRandom}
57+
* instance.
58+
*/
59+
public MonotonicULID() {
60+
this(new SecureRandom());
61+
}
62+
63+
/**
64+
* Generate a monotonic ULID generator instance.
65+
*
66+
* @param random {@link java.util.Random} instance
67+
*/
68+
public MonotonicULID(Random random) {
69+
if (random == null)
70+
throw new IllegalArgumentException("java.util.Random instance must not be null");
71+
this.random = random;
72+
this.lastEntropy = new byte[ULID.ENTROPY_LENGTH];
73+
this.lastTimestamp = -1L;
74+
}
75+
76+
/**
77+
* Generate ULID string monotonicly. If this method is called within the same millisecond, last
78+
* entropy will be incremented by 1 and the ULID string of incremented value is returned.<br>
79+
* <br>
80+
* This method will throw a {@link java.lang.IllegalStateException} exception if incremented value
81+
* overflows entropy length (80b-its/10-bytes)
82+
*
83+
* @return ULID string
84+
*/
85+
public synchronized String generate() {
86+
long now = System.currentTimeMillis();
87+
if (now == this.lastTimestamp) {
88+
// Entropy is big-endian (network byte order) per ULID spec
89+
// Increment last entropy by 1
90+
boolean carry = true;
91+
for (int i = ULID.ENTROPY_LENGTH - 1; i >= 0; i--) {
92+
if (carry) {
93+
byte work = this.lastEntropy[i];
94+
work = (byte) (work + 0x01);
95+
carry = this.lastEntropy[i] == (byte) 0xff && carry;
96+
this.lastEntropy[i] = work;
97+
}
98+
}
99+
// Last byte has carry over
100+
if (carry) {
101+
// Throw error if entropy overflows in same millisecond per ULID spec
102+
throw new IllegalStateException("ULID entropy overflowed for same millisecond");
103+
}
104+
} else {
105+
// Generate new entropy
106+
this.lastTimestamp = now;
107+
this.random.nextBytes(this.lastEntropy);
108+
}
109+
return ULID.generate(this.lastTimestamp, this.lastEntropy);
110+
}
111+
}

src/main/java/io/azam/ulidj/ULID.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,19 @@
5151
* @since 0.0.1
5252
*
5353
* @see <a href="http://www.crockford.com/wrmg/base32.html">Base32 Encoding</a>
54-
* @see <a href="https://github.com/alizain/ulid">ULID</a>
54+
* @see <a href="https://github.com/ulid/spec">ULID</a>
5555
*/
5656
public class ULID {
5757
/**
5858
* ULID string length.
5959
*/
6060
public static final int ULID_LENGTH = 26;
6161

62+
/**
63+
* ULID entropy byte length.
64+
*/
65+
public static final int ENTROPY_LENGTH = 10;
66+
6267
/**
6368
* Minimum allowed timestamp value.
6469
*/
@@ -76,7 +81,8 @@ public class ULID {
7681
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, //
7782
0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, //
7883
0x47, 0x48, 0x4a, 0x4b, 0x4d, 0x4e, 0x50, 0x51, //
79-
0x52, 0x53, 0x54, 0x56, 0x57, 0x58, 0x59, 0x5a};
84+
0x52, 0x53, 0x54, 0x56, 0x57, 0x58, 0x59, 0x5a //
85+
};
8086

8187
/**
8288
* {@code char} to {@code byte} O(1) mapping with alternative chars mapping
@@ -182,7 +188,7 @@ public static String random(Random random) {
182188
* @return ULID string
183189
*/
184190
public static String generate(long time, byte[] entropy) {
185-
if (time < MIN_TIME || time > MAX_TIME || entropy == null || entropy.length < 10) {
191+
if (time < MIN_TIME || time > MAX_TIME || entropy == null || entropy.length < ENTROPY_LENGTH) {
186192
throw new IllegalArgumentException(
187193
"Time is too long, or entropy is less than 10 bytes or null");
188194
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2016 Azamshul Azizy
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7+
* associated documentation files (the "Software"), to deal in the Software without restriction,
8+
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
9+
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
16+
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package io.azam.ulidj;
22+
23+
import java.security.SecureRandom;
24+
import java.util.ArrayList;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Random;
29+
30+
import org.junit.Assert;
31+
import org.junit.Test;
32+
33+
/**
34+
* Test class for {@link io.azam.ulidj.MonotonicULID}
35+
*
36+
* @author azam
37+
* @since 1.0.3
38+
*/
39+
public class MonotonicULIDTest {
40+
@Test
41+
public void testConstructor() {
42+
Assert.assertNotNull(new MonotonicULID());
43+
Assert.assertNotNull(new MonotonicULID(new Random()));
44+
Assert.assertNotNull(new MonotonicULID(new SecureRandom()));
45+
}
46+
47+
@Test(expected = IllegalArgumentException.class)
48+
public void testConstructorNullRandom() {
49+
new MonotonicULID(null);
50+
}
51+
52+
@Test
53+
public void testGenerate() {
54+
MonotonicULID ulid = new MonotonicULID();
55+
String id = ulid.generate();
56+
Assert.assertNotNull(id);
57+
Assert.assertTrue(ULID.isValid(id));
58+
}
59+
60+
@Test
61+
public void testGenerateConcurrent() {
62+
MonotonicULID ulid = new MonotonicULID();
63+
boolean hasSameTimestamp = false;
64+
// This test might not end if we cannot generate multiple ULID in the same
65+
// milliseconds. Unless we are running on really slow CPU, we should be OK.
66+
while (!hasSameTimestamp) {
67+
List<String> values = new ArrayList<String>();
68+
// Generate a bunch of ULIDS
69+
// Values are inserted in order
70+
for (int i = 0; i < 10000; i++) {
71+
values.add(ulid.generate());
72+
}
73+
// Group into timestamp bucket
74+
Map<Long, List<byte[]>> groups = new HashMap<Long, List<byte[]>>();
75+
for (String value : values) {
76+
Assert.assertNotNull(value);
77+
Assert.assertTrue(ULID.isValid(value));
78+
long ts = ULID.getTimestamp(value);
79+
byte[] entropy = ULID.getEntropy(value);
80+
if (!groups.containsKey(ts)) {
81+
groups.put(ts, new ArrayList<byte[]>());
82+
}
83+
groups.get(ts).add(entropy);
84+
}
85+
// For each timestamp bucket check if entropy is monotonic
86+
for (long ts : groups.keySet()) {
87+
// Loop until we have a bucket of 5 ids on the same timestamp
88+
if (groups.get(ts).size() > 4) {
89+
// Escape loop on next while eval
90+
hasSameTimestamp = true;
91+
List<byte[]> bucketValues = groups.get(ts);
92+
// Values are inserted in order so we don't have to sort
93+
byte[] prev = bucketValues.get(0);
94+
for (int i = 1; i < bucketValues.size(); i++) {
95+
byte[] curr = bucketValues.get(i);
96+
// The next value on the same timestamp is an increment of 1-bit if the previous
97+
// value
98+
Assert.assertArrayEquals(incrementBytes(prev), curr);
99+
prev = curr;
100+
}
101+
}
102+
}
103+
}
104+
}
105+
106+
@Test(expected = IllegalStateException.class)
107+
public void testGenerateOverflow() {
108+
// Using a random generator that always return 0xff... so that next increment on
109+
// the same timestamp will throw exception
110+
MonotonicULID ulid = new MonotonicULID(new FixedRandom(new byte[] { //
111+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, //
112+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff //
113+
}));
114+
List<String> values = new ArrayList<String>();
115+
//
116+
for (int i = 0; i < 1000000; i++) {
117+
values.add(ulid.generate());
118+
}
119+
}
120+
121+
@Test
122+
public void testIncrementBytes() {
123+
Assert.assertArrayEquals(new byte[] { //
124+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, //
125+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 //
126+
}, incrementBytes(new byte[] { //
127+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, //
128+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 //
129+
}));
130+
Assert.assertArrayEquals(new byte[] { //
131+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, //
132+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff //
133+
}, incrementBytes(new byte[] { //
134+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, //
135+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfe //
136+
}));
137+
Assert.assertArrayEquals(new byte[] { //
138+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, //
139+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00 //
140+
}, incrementBytes(new byte[] { //
141+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, //
142+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff //
143+
}));
144+
Assert.assertArrayEquals(new byte[] { //
145+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, //
146+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff //
147+
}, incrementBytes(new byte[] { //
148+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, //
149+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xfe //
150+
}));
151+
Assert.assertArrayEquals(null, incrementBytes(new byte[] { //
152+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, //
153+
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff //
154+
}));
155+
}
156+
157+
byte[] incrementBytes(byte[] bytes) {
158+
if (bytes == null || bytes.length != ULID.ENTROPY_LENGTH)
159+
return null;
160+
byte[] value = new byte[ULID.ENTROPY_LENGTH];
161+
162+
boolean carry = true;
163+
for (int i = ULID.ENTROPY_LENGTH - 1; i >= 0; i--) {
164+
byte work = bytes[i];
165+
if (carry) {
166+
work = (byte) (work + 0x01);
167+
carry = bytes[i] == (byte) 0xff && carry;
168+
}
169+
value[i] = work;
170+
}
171+
// Last byte has carry over
172+
if (carry) {
173+
return null;
174+
}
175+
176+
return value;
177+
}
178+
179+
class FixedRandom extends Random {
180+
private final byte[] bytes;
181+
182+
public FixedRandom(byte[] bytes) {
183+
this.bytes = bytes;
184+
}
185+
186+
@Override
187+
public void nextBytes(byte[] out) {
188+
for (int i = 0; i < out.length; i++) {
189+
if (i < this.bytes.length)
190+
out[i] = this.bytes[i];
191+
else
192+
out[i] = 0x00;
193+
}
194+
}
195+
}
196+
197+
}

0 commit comments

Comments
 (0)