Skip to content

Commit 850b49c

Browse files
Copilotsimbo1905
andcommitted
Add UUIDGenerator with LazyRandom initialization and comprehensive tests
Co-authored-by: simbo1905 <[email protected]>
1 parent 0a0573a commit 850b49c

File tree

2 files changed

+382
-0
lines changed

2 files changed

+382
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// SPDX-FileCopyrightText: 2025 Simon Massey
2+
// SPDX-License-Identifier: MIT
3+
4+
package jdk.sandbox.demo;
5+
6+
import java.nio.ByteBuffer;
7+
import java.security.SecureRandom;
8+
import java.math.BigInteger;
9+
import java.util.concurrent.atomic.AtomicInteger;
10+
import java.util.UUID;
11+
12+
/// UUID Generator providing time-ordered globally unique IDs.
13+
///
14+
/// Generation:
15+
/// - timeThenRandom: Time-ordered globally unique 128-bit identifier
16+
/// - uniqueThenTime: User-ID-then-time-ordered 128-bit identifier
17+
///
18+
/// Formatting:
19+
/// - formatAsUUID: RFC 4122 format with dashes, 36 characters, uppercase or lowercase
20+
/// - formatAsDenseKey: Base62 encoded, 22 characters, zero-padded fixed-width
21+
///
22+
/// Note: Intended usage is one instance per JVM process. Multiple instances
23+
/// in the same process do not guarantee uniqueness due to shared sequence counter.
24+
///
25+
/// Note: 22-character keys are larger than Firebase push IDs (20 characters)
26+
/// but provide full 128-bit time-ordered randomized identifiers.
27+
public class UUIDGenerator {
28+
static final AtomicInteger sequence = new AtomicInteger(0);
29+
30+
/// LazyRandom holder using double-checked locking pattern for thread-safe lazy initialization
31+
private static volatile SecureRandom random;
32+
private static final Object randomLock = new Object();
33+
34+
static SecureRandom getRandom() {
35+
SecureRandom result = random;
36+
if (result == null) {
37+
synchronized (randomLock) {
38+
result = random;
39+
if (result == null) {
40+
random = result = new SecureRandom();
41+
}
42+
}
43+
}
44+
return result;
45+
}
46+
47+
static long timeCounterBits() {
48+
long ms = System.currentTimeMillis();
49+
int seq = sequence.incrementAndGet() & 0xFFFFF; // 20-bit counter
50+
return (ms << 20) | seq;
51+
}
52+
53+
// Generation - Public API
54+
55+
/// ┌──────────────────────────────────────────────────────────────────────────────┐
56+
/// │ time+counter (64 bits) │ random (64 bits) │
57+
/// └──────────────────────────────────────────────────────────────────────────────┘
58+
public static UUID timeThenRandom() {
59+
long msb = timeCounterBits();
60+
long lsb = getRandom().nextLong();
61+
return new UUID(msb, lsb);
62+
}
63+
64+
/// ┌──────────────────────────────────────────────────────────────────────────────┐
65+
/// │ unique (64 bits) │ time+counter (44 bits) │ random (20 bits) │
66+
/// └──────────────────────────────────────────────────────────────────────────────┘
67+
public static UUID uniqueThenTime(long uniqueMsb) {
68+
final int timeBits = 44;
69+
final long timeMask = (1L << timeBits) - 1;
70+
final int randomBits = 20;
71+
final int randomMask = (1 << randomBits) - 1;
72+
long timeCounter = timeCounterBits();
73+
long msb = uniqueMsb;
74+
long lsb = ((timeCounter & timeMask) << randomBits) | (getRandom().nextInt() & randomMask);
75+
return new UUID(msb, lsb);
76+
}
77+
78+
// Formatting - Public API
79+
80+
/// Standard RFC 4122 UUID layout with dashes.
81+
/// Returns fixed-length 36 character string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
82+
public static String formatAsUUID(UUID uuid, boolean uppercase) {
83+
ByteBuffer buffer = ByteBuffer.allocate(16);
84+
buffer.putLong(uuid.getMostSignificantBits());
85+
buffer.putLong(uuid.getLeastSignificantBits());
86+
87+
StringBuilder hex = new StringBuilder();
88+
for (byte b : buffer.array()) {
89+
hex.append(String.format("%02x", b));
90+
}
91+
92+
String hexStr = hex.toString();
93+
String formatted = String.format("%s-%s-%s-%s-%s",
94+
hexStr.substring(0, 8),
95+
hexStr.substring(8, 12),
96+
hexStr.substring(12, 16),
97+
hexStr.substring(16, 20),
98+
hexStr.substring(20));
99+
100+
return uppercase ? formatted.toUpperCase() : formatted.toLowerCase();
101+
}
102+
103+
public static String formatAsUUID(UUID uuid) {
104+
return formatAsUUID(uuid, false);
105+
}
106+
107+
/// Alphanumeric base62 encoding.
108+
/// Returns fixed-length 22 character string for lexicographic sorting.
109+
public static String formatAsDenseKey(UUID uuid) {
110+
final String alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
111+
final int base = alphabet.length();
112+
final int expectedLength = 22;
113+
114+
ByteBuffer buffer = ByteBuffer.allocate(16);
115+
buffer.putLong(uuid.getMostSignificantBits());
116+
buffer.putLong(uuid.getLeastSignificantBits());
117+
118+
BigInteger value = new BigInteger(1, buffer.array());
119+
StringBuilder sb = new StringBuilder();
120+
121+
while (value.compareTo(BigInteger.ZERO) > 0) {
122+
BigInteger[] divMod = value.divideAndRemainder(BigInteger.valueOf(base));
123+
sb.append(alphabet.charAt(divMod[1].intValue()));
124+
value = divMod[0];
125+
}
126+
127+
String encoded = sb.reverse().toString();
128+
while (encoded.length() < expectedLength) {
129+
encoded = "0" + encoded;
130+
}
131+
132+
return encoded;
133+
}
134+
135+
public static void main(String[] args) {
136+
UUID uuid1 = UUIDGenerator.timeThenRandom();
137+
System.out.println("UUID: " + UUIDGenerator.formatAsUUID(uuid1));
138+
System.out.println("Dense: " + UUIDGenerator.formatAsDenseKey(uuid1));
139+
140+
UUID uuid2 = UUIDGenerator.uniqueThenTime(0x123456789ABCDEF0L);
141+
System.out.println("Unique UUID: " + UUIDGenerator.formatAsUUID(uuid2, true));
142+
System.out.println("Unique Dense: " + UUIDGenerator.formatAsDenseKey(uuid2));
143+
}
144+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package jdk.sandbox.demo;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.RepeatedTest;
5+
import org.junit.jupiter.api.parallel.Execution;
6+
import org.junit.jupiter.api.parallel.ExecutionMode;
7+
8+
import java.util.UUID;
9+
import java.util.HashSet;
10+
import java.util.Set;
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.CountDownLatch;
13+
import java.util.concurrent.atomic.AtomicInteger;
14+
import java.util.regex.Pattern;
15+
16+
import static org.junit.jupiter.api.Assertions.*;
17+
18+
class UUIDGeneratorTest {
19+
20+
@Test
21+
void testTimeThenRandomGeneratesUUID() {
22+
UUID uuid = UUIDGenerator.timeThenRandom();
23+
assertNotNull(uuid);
24+
}
25+
26+
@Test
27+
void testTimeThenRandomGeneratesUniqueUUIDs() {
28+
Set<UUID> uuids = new HashSet<>();
29+
for (int i = 0; i < 1000; i++) {
30+
UUID uuid = UUIDGenerator.timeThenRandom();
31+
assertTrue(uuids.add(uuid), "Generated duplicate UUID: " + uuid);
32+
}
33+
}
34+
35+
@Test
36+
void testTimeThenRandomIncreasingOrder() {
37+
UUID prev = UUIDGenerator.timeThenRandom();
38+
for (int i = 0; i < 100; i++) {
39+
UUID current = UUIDGenerator.timeThenRandom();
40+
// MSB should be increasing (time+counter)
41+
assertTrue(current.getMostSignificantBits() >= prev.getMostSignificantBits(),
42+
"UUIDs should be time-ordered");
43+
prev = current;
44+
}
45+
}
46+
47+
@Test
48+
void testUniqueThenTimeGeneratesUUID() {
49+
long uniqueMsb = 0x123456789ABCDEF0L;
50+
UUID uuid = UUIDGenerator.uniqueThenTime(uniqueMsb);
51+
assertNotNull(uuid);
52+
assertEquals(uniqueMsb, uuid.getMostSignificantBits());
53+
}
54+
55+
@Test
56+
void testUniqueThenTimeWithDifferentUserIds() {
57+
long userId1 = 1L;
58+
long userId2 = 2L;
59+
60+
UUID uuid1 = UUIDGenerator.uniqueThenTime(userId1);
61+
UUID uuid2 = UUIDGenerator.uniqueThenTime(userId2);
62+
63+
assertNotEquals(uuid1, uuid2);
64+
assertEquals(userId1, uuid1.getMostSignificantBits());
65+
assertEquals(userId2, uuid2.getMostSignificantBits());
66+
}
67+
68+
@Test
69+
void testFormatAsUUIDLowercase() {
70+
UUID uuid = new UUID(0x123456789ABCDEF0L, 0xFEDCBA9876543210L);
71+
String formatted = UUIDGenerator.formatAsUUID(uuid);
72+
73+
assertEquals(36, formatted.length());
74+
assertTrue(formatted.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"),
75+
"Should match UUID format: " + formatted);
76+
assertEquals("12345678-9abc-def0-fedc-ba9876543210", formatted);
77+
}
78+
79+
@Test
80+
void testFormatAsUUIDUppercase() {
81+
UUID uuid = new UUID(0x123456789ABCDEF0L, 0xFEDCBA9876543210L);
82+
String formatted = UUIDGenerator.formatAsUUID(uuid, true);
83+
84+
assertEquals(36, formatted.length());
85+
assertTrue(formatted.matches("[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}"),
86+
"Should match uppercase UUID format: " + formatted);
87+
assertEquals("12345678-9ABC-DEF0-FEDC-BA9876543210", formatted);
88+
}
89+
90+
@Test
91+
void testFormatAsUUIDDefaultIsLowercase() {
92+
UUID uuid = new UUID(0x123456789ABCDEF0L, 0xFEDCBA9876543210L);
93+
String formatted = UUIDGenerator.formatAsUUID(uuid);
94+
String formattedExplicit = UUIDGenerator.formatAsUUID(uuid, false);
95+
96+
assertEquals(formatted, formattedExplicit);
97+
assertEquals(formatted, formatted.toLowerCase());
98+
}
99+
100+
@Test
101+
void testFormatAsDenseKeyLength() {
102+
UUID uuid = UUIDGenerator.timeThenRandom();
103+
String denseKey = UUIDGenerator.formatAsDenseKey(uuid);
104+
105+
assertEquals(22, denseKey.length(), "Dense key should be 22 characters");
106+
}
107+
108+
@Test
109+
void testFormatAsDenseKeyAlphanumeric() {
110+
UUID uuid = UUIDGenerator.timeThenRandom();
111+
String denseKey = UUIDGenerator.formatAsDenseKey(uuid);
112+
113+
assertTrue(denseKey.matches("[0-9A-Za-z]+"),
114+
"Dense key should only contain alphanumeric characters");
115+
}
116+
117+
@Test
118+
void testFormatAsDenseKeyUniqueness() {
119+
Set<String> denseKeys = new HashSet<>();
120+
for (int i = 0; i < 1000; i++) {
121+
UUID uuid = UUIDGenerator.timeThenRandom();
122+
String denseKey = UUIDGenerator.formatAsDenseKey(uuid);
123+
assertTrue(denseKeys.add(denseKey), "Generated duplicate dense key: " + denseKey);
124+
}
125+
}
126+
127+
@Test
128+
void testFormatAsDenseKeyZeroPadding() {
129+
// Test with UUID that has many leading zeros
130+
UUID uuid = new UUID(0L, 1L);
131+
String denseKey = UUIDGenerator.formatAsDenseKey(uuid);
132+
133+
assertEquals(22, denseKey.length(), "Dense key should be zero-padded to 22 characters");
134+
}
135+
136+
@Test
137+
void testFormatAsDenseKeyLexicographicOrdering() {
138+
// Time-ordered UUIDs should produce lexicographically ordered dense keys
139+
UUID uuid1 = UUIDGenerator.timeThenRandom();
140+
String key1 = UUIDGenerator.formatAsDenseKey(uuid1);
141+
142+
// Wait a bit to ensure time advances
143+
try {
144+
Thread.sleep(2);
145+
} catch (InterruptedException e) {
146+
Thread.currentThread().interrupt();
147+
}
148+
149+
UUID uuid2 = UUIDGenerator.timeThenRandom();
150+
String key2 = UUIDGenerator.formatAsDenseKey(uuid2);
151+
152+
assertTrue(key1.compareTo(key2) <= 0,
153+
"Dense keys should maintain lexicographic ordering for time-ordered UUIDs");
154+
}
155+
156+
@Test
157+
void testLazyRandomInitialization() {
158+
// This test verifies that the LazyRandom pattern works correctly
159+
// by generating multiple UUIDs and ensuring they are all different
160+
Set<UUID> uuids = new HashSet<>();
161+
for (int i = 0; i < 100; i++) {
162+
UUID uuid = UUIDGenerator.timeThenRandom();
163+
assertTrue(uuids.add(uuid), "LazyRandom should generate unique random values");
164+
}
165+
}
166+
167+
@Test
168+
void testThreadSafetyOfLazyRandom() throws InterruptedException {
169+
final int threadCount = 10;
170+
final int uuidsPerThread = 100;
171+
final CountDownLatch startLatch = new CountDownLatch(1);
172+
final CountDownLatch doneLatch = new CountDownLatch(threadCount);
173+
final Set<UUID> allUuids = ConcurrentHashMap.newKeySet();
174+
final AtomicInteger duplicateCount = new AtomicInteger(0);
175+
176+
for (int i = 0; i < threadCount; i++) {
177+
new Thread(() -> {
178+
try {
179+
startLatch.await(); // Wait for all threads to be ready
180+
for (int j = 0; j < uuidsPerThread; j++) {
181+
UUID uuid = UUIDGenerator.timeThenRandom();
182+
if (!allUuids.add(uuid)) {
183+
duplicateCount.incrementAndGet();
184+
}
185+
}
186+
} catch (InterruptedException e) {
187+
Thread.currentThread().interrupt();
188+
} finally {
189+
doneLatch.countDown();
190+
}
191+
}).start();
192+
}
193+
194+
startLatch.countDown(); // Start all threads
195+
doneLatch.await(); // Wait for all threads to complete
196+
197+
assertEquals(0, duplicateCount.get(), "Should not generate duplicate UUIDs in multi-threaded scenario");
198+
assertEquals(threadCount * uuidsPerThread, allUuids.size());
199+
}
200+
201+
@RepeatedTest(10)
202+
void testSequenceCounterWraparound() {
203+
// Generate many UUIDs quickly to test sequence counter
204+
Set<UUID> uuids = new HashSet<>();
205+
for (int i = 0; i < 1000; i++) {
206+
UUID uuid = UUIDGenerator.timeThenRandom();
207+
assertTrue(uuids.add(uuid), "Should generate unique UUIDs even with sequence counter");
208+
}
209+
}
210+
211+
@Test
212+
void testTimeCounterBitsFormat() {
213+
// Test that timeCounterBits produces reasonable values
214+
long bits1 = UUIDGenerator.timeCounterBits();
215+
long bits2 = UUIDGenerator.timeCounterBits();
216+
217+
// Should be increasing or equal (if same millisecond)
218+
assertTrue(bits2 >= bits1, "Time counter bits should be non-decreasing");
219+
}
220+
221+
@Test
222+
void testUniqueThenTimePreservesUniqueBits() {
223+
long uniqueMsb = 0xFFFFFFFFFFFFFFFL;
224+
UUID uuid = UUIDGenerator.uniqueThenTime(uniqueMsb);
225+
226+
assertEquals(uniqueMsb, uuid.getMostSignificantBits(),
227+
"uniqueThenTime should preserve the unique MSB");
228+
}
229+
230+
@Test
231+
void testFormatAsDenseKeyDeterministic() {
232+
UUID uuid = new UUID(0x123456789ABCDEF0L, 0xFEDCBA9876543210L);
233+
String key1 = UUIDGenerator.formatAsDenseKey(uuid);
234+
String key2 = UUIDGenerator.formatAsDenseKey(uuid);
235+
236+
assertEquals(key1, key2, "Same UUID should produce same dense key");
237+
}
238+
}

0 commit comments

Comments
 (0)