Skip to content

Commit 328e11c

Browse files
Copilotsimbo1905
andcommitted
Add UUIDGenerator with UUIDv7 support and configurable modes
Co-authored-by: simbo1905 <[email protected]>
1 parent 295cac5 commit 328e11c

File tree

4 files changed

+541
-0
lines changed

4 files changed

+541
-0
lines changed

json-java21/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
<artifactId>junit-jupiter-engine</artifactId>
3535
<scope>test</scope>
3636
</dependency>
37+
<dependency>
38+
<groupId>org.junit.jupiter</groupId>
39+
<artifactId>junit-jupiter-params</artifactId>
40+
<scope>test</scope>
41+
</dependency>
3742
<dependency>
3843
<groupId>org.assertj</groupId>
3944
<artifactId>assertj-core</artifactId>
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package jdk.sandbox.java.util.json;
2+
3+
import java.nio.ByteBuffer;
4+
import java.security.SecureRandom;
5+
import java.util.UUID;
6+
import java.util.logging.Logger;
7+
8+
/// Provides UUID generation utilities supporting both UUIDv7 and alternative generation modes.
9+
///
10+
/// This class supports two UUID generation strategies:
11+
/// 1. **UUIDv7** (default): Time-ordered UUIDs using Unix Epoch milliseconds (backported from Java 26)
12+
/// 2. **Unique-Then-Time**: Custom format with unique MSB and time-based LSB
13+
///
14+
/// The generation mode can be configured via the system property {@code jdk.sandbox.uuid.generator.mode}
15+
/// with values {@code "v7"} (default) or {@code "unique-then-time"}.
16+
///
17+
/// @since Backport from Java 26 (JDK-8334015)
18+
public final class UUIDGenerator {
19+
20+
private static final Logger LOGGER = Logger.getLogger(UUIDGenerator.class.getName());
21+
22+
/// System property key for configuring UUID generation mode
23+
public static final String MODE_PROPERTY = "jdk.sandbox.uuid.generator.mode";
24+
25+
/// Mode value for UUIDv7 generation
26+
public static final String MODE_V7 = "v7";
27+
28+
/// Mode value for unique-then-time generation
29+
public static final String MODE_UNIQUE_THEN_TIME = "unique-then-time";
30+
31+
/// Enum representing the UUID generation mode
32+
public enum Mode {
33+
/// UUIDv7 mode using Unix Epoch timestamp
34+
V7,
35+
/// Unique-then-time mode with custom format
36+
UNIQUE_THEN_TIME
37+
}
38+
39+
/// Lazy initialization holder for SecureRandom
40+
private static final class LazyRandom {
41+
static final SecureRandom RANDOM = new SecureRandom();
42+
}
43+
44+
private static final Mode DEFAULT_MODE = Mode.V7;
45+
private static final Mode CONFIGURED_MODE;
46+
47+
static {
48+
final String propertyValue = System.getProperty(MODE_PROPERTY);
49+
Mode mode = DEFAULT_MODE;
50+
51+
if (propertyValue != null) {
52+
final String normalized = propertyValue.trim().toLowerCase();
53+
mode = switch (normalized) {
54+
case MODE_V7 -> {
55+
LOGGER.fine(() -> "UUID generator mode set to V7 via system property");
56+
yield Mode.V7;
57+
}
58+
case MODE_UNIQUE_THEN_TIME -> {
59+
LOGGER.fine(() -> "UUID generator mode set to UNIQUE_THEN_TIME via system property");
60+
yield Mode.UNIQUE_THEN_TIME;
61+
}
62+
default -> {
63+
LOGGER.warning(() -> "Invalid UUID generator mode: " + propertyValue +
64+
". Using default mode: " + DEFAULT_MODE);
65+
yield DEFAULT_MODE;
66+
}
67+
};
68+
} else {
69+
LOGGER.fine(() -> "UUID generator mode not specified, using default: " + DEFAULT_MODE);
70+
}
71+
72+
CONFIGURED_MODE = mode;
73+
}
74+
75+
/// Private constructor to prevent instantiation
76+
private UUIDGenerator() {
77+
throw new AssertionError("UUIDGenerator cannot be instantiated");
78+
}
79+
80+
/// Generates a UUID using the configured mode.
81+
///
82+
/// The mode is determined by the system property {@code jdk.sandbox.uuid.generator.mode}.
83+
/// If not specified, defaults to UUIDv7 mode.
84+
///
85+
/// @return a {@code UUID} generated according to the configured mode
86+
public static UUID generateUUID() {
87+
return switch (CONFIGURED_MODE) {
88+
case V7 -> ofEpochMillis(System.currentTimeMillis());
89+
case UNIQUE_THEN_TIME -> uniqueThenTime(generateUniqueMsb());
90+
};
91+
}
92+
93+
/// Creates a type 7 UUID (UUIDv7) {@code UUID} from the given Unix Epoch timestamp.
94+
///
95+
/// The returned {@code UUID} will have the given {@code timestamp} in
96+
/// the first 6 bytes, followed by the version and variant bits representing {@code UUIDv7},
97+
/// and the remaining bytes will contain random data from a cryptographically strong
98+
/// pseudo-random number generator.
99+
///
100+
/// @apiNote {@code UUIDv7} values are created by allocating a Unix timestamp in milliseconds
101+
/// in the most significant 48 bits, allocating the required version (4 bits) and variant (2-bits)
102+
/// and filling the remaining 74 bits with random bits. As such, this method rejects {@code timestamp}
103+
/// values that do not fit into 48 bits.
104+
/// <p>
105+
/// Monotonicity (each subsequent value being greater than the last) is a primary characteristic
106+
/// of {@code UUIDv7} values. This is due to the {@code timestamp} value being part of the {@code UUID}.
107+
/// Callers of this method that wish to generate monotonic {@code UUIDv7} values are expected to
108+
/// ensure that the given {@code timestamp} value is monotonic.
109+
///
110+
/// @param timestamp the number of milliseconds since midnight 1 Jan 1970 UTC,
111+
/// leap seconds excluded.
112+
///
113+
/// @return a {@code UUID} constructed using the given {@code timestamp}
114+
///
115+
/// @throws IllegalArgumentException if the timestamp is negative or greater than {@code (1L << 48) - 1}
116+
///
117+
/// @since Backport from Java 26 (JDK-8334015)
118+
public static UUID ofEpochMillis(final long timestamp) {
119+
if ((timestamp >> 48) != 0) {
120+
throw new IllegalArgumentException("Supplied timestamp: " + timestamp + " does not fit within 48 bits");
121+
}
122+
123+
final byte[] randomBytes = new byte[16];
124+
LazyRandom.RANDOM.nextBytes(randomBytes);
125+
126+
// Embed the timestamp into the first 6 bytes
127+
randomBytes[0] = (byte)(timestamp >> 40);
128+
randomBytes[1] = (byte)(timestamp >> 32);
129+
randomBytes[2] = (byte)(timestamp >> 24);
130+
randomBytes[3] = (byte)(timestamp >> 16);
131+
randomBytes[4] = (byte)(timestamp >> 8);
132+
randomBytes[5] = (byte)(timestamp);
133+
134+
// Set version to 7
135+
randomBytes[6] &= 0x0f;
136+
randomBytes[6] |= 0x70;
137+
138+
// Set variant to IETF
139+
randomBytes[8] &= 0x3f;
140+
randomBytes[8] |= (byte) 0x80;
141+
142+
// Convert byte array to UUID using ByteBuffer
143+
final ByteBuffer buffer = ByteBuffer.wrap(randomBytes);
144+
final long msb = buffer.getLong();
145+
final long lsb = buffer.getLong();
146+
return new UUID(msb, lsb);
147+
}
148+
149+
/// Creates a UUID with unique MSB and time-based LSB.
150+
///
151+
/// Format:
152+
/// ```
153+
/// ┌──────────────────────────────────────────────────────────────────────────────┐
154+
/// │ unique (64 bits) │ time+counter (44 bits) │ random (20 bits) │
155+
/// └──────────────────────────────────────────────────────────────────────────────┘
156+
/// ```
157+
///
158+
/// The LSB contains:
159+
/// - 44 most significant bits: time counter for ordering
160+
/// - 20 least significant bits: random data
161+
///
162+
/// @param uniqueMsb the unique 64-bit value for the MSB
163+
/// @return a {@code UUID} with the specified MSB and time-ordered LSB
164+
public static UUID uniqueThenTime(final long uniqueMsb) {
165+
final int timeBits = 44;
166+
final int randomBits = 20;
167+
final int randomMask = (1 << randomBits) - 1;
168+
final long timeCounter = timeCounterBits();
169+
final long msb = uniqueMsb;
170+
// Take the most significant 44 bits of timeCounter to preserve time ordering
171+
final long timeComponent = timeCounter >> (64 - timeBits); // timeBits is 44
172+
final long lsb = (timeComponent << randomBits) | (LazyRandom.RANDOM.nextInt() & randomMask);
173+
return new UUID(msb, lsb);
174+
}
175+
176+
/// Generates a time-based counter value using current time and nano precision.
177+
///
178+
/// Combines milliseconds since epoch with nano adjustment for higher precision ordering.
179+
///
180+
/// @return a 64-bit time counter value
181+
private static long timeCounterBits() {
182+
final long currentTimeMillis = System.currentTimeMillis();
183+
final long nanoTime = System.nanoTime();
184+
// Combine milliseconds with nano adjustment for better ordering
185+
return (currentTimeMillis << 20) | (nanoTime & 0xFFFFF);
186+
}
187+
188+
/// Generates a unique 64-bit MSB value using cryptographically strong random data.
189+
///
190+
/// @return a unique 64-bit value
191+
private static long generateUniqueMsb() {
192+
return LazyRandom.RANDOM.nextLong();
193+
}
194+
195+
/// Returns the currently configured UUID generation mode.
196+
///
197+
/// @return the configured {@code Mode}
198+
public static Mode getConfiguredMode() {
199+
return CONFIGURED_MODE;
200+
}
201+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package jdk.sandbox.java.util.json;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.DisplayName;
5+
6+
import java.util.UUID;
7+
import java.util.logging.Logger;
8+
9+
import static org.assertj.core.api.Assertions.*;
10+
11+
/// Tests for {@link UUIDGenerator} system property configuration.
12+
///
13+
/// This test verifies that system properties can control the UUID generation mode.
14+
/// These tests run in a separate JVM via Maven Surefire configuration.
15+
class UUIDGeneratorConfigTest {
16+
17+
private static final Logger LOGGER = Logger.getLogger(UUIDGeneratorConfigTest.class.getName());
18+
19+
@Test
20+
@DisplayName("Verify default mode is V7 when no system property is set")
21+
void testDefaultModeIsV7() {
22+
LOGGER.info("Executing testDefaultModeIsV7");
23+
// This test assumes no system property was set at JVM startup
24+
// In the default configuration, mode should be V7
25+
final UUIDGenerator.Mode mode = UUIDGenerator.getConfiguredMode();
26+
LOGGER.info(() -> "Configured mode: " + mode);
27+
28+
// Generate a UUID and verify it's a valid UUIDv7
29+
final UUID uuid = UUIDGenerator.generateUUID();
30+
assertThat(uuid).isNotNull();
31+
32+
// If mode is V7, the UUID should have version 7
33+
if (mode == UUIDGenerator.Mode.V7) {
34+
assertThat(uuid.version()).isEqualTo(7);
35+
}
36+
}
37+
38+
@Test
39+
@DisplayName("Generate multiple UUIDs and verify consistency")
40+
void testMultipleUUIDsWithConfiguredMode() {
41+
LOGGER.info("Executing testMultipleUUIDsWithConfiguredMode");
42+
final UUIDGenerator.Mode mode = UUIDGenerator.getConfiguredMode();
43+
LOGGER.info(() -> "Configured mode: " + mode);
44+
45+
// Generate multiple UUIDs
46+
final UUID uuid1 = UUIDGenerator.generateUUID();
47+
final UUID uuid2 = UUIDGenerator.generateUUID();
48+
final UUID uuid3 = UUIDGenerator.generateUUID();
49+
50+
assertThat(uuid1).isNotNull();
51+
assertThat(uuid2).isNotNull();
52+
assertThat(uuid3).isNotNull();
53+
54+
// All should be unique
55+
assertThat(uuid1).isNotEqualTo(uuid2);
56+
assertThat(uuid2).isNotEqualTo(uuid3);
57+
assertThat(uuid1).isNotEqualTo(uuid3);
58+
59+
// If V7 mode, all should have version 7
60+
if (mode == UUIDGenerator.Mode.V7) {
61+
assertThat(uuid1.version()).isEqualTo(7);
62+
assertThat(uuid2.version()).isEqualTo(7);
63+
assertThat(uuid3.version()).isEqualTo(7);
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)