Skip to content

Commit a91ce54

Browse files
author
drighetto
committed
Add method to generate an UUID v7.
1 parent f5e18d2 commit a91ce54

File tree

2 files changed

+91
-1
lines changed

2 files changed

+91
-1
lines changed

src/main/java/eu/righettod/SecurityUtils.java

+52-1
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@
5353
import java.net.http.HttpClient;
5454
import java.net.http.HttpRequest;
5555
import java.net.http.HttpResponse;
56+
import java.nio.ByteBuffer;
5657
import java.nio.charset.Charset;
5758
import java.nio.charset.StandardCharsets;
5859
import java.nio.file.Files;
5960
import java.security.MessageDigest;
61+
import java.security.SecureRandom;
6062
import java.time.Duration;
6163
import java.util.*;
6264
import java.util.List;
@@ -76,7 +78,6 @@
7678
* </ul>
7779
*/
7880
public class SecurityUtils {
79-
8081
/**
8182
* Default constructor: Not needed as the class only provides static methods.
8283
*/
@@ -1328,4 +1329,54 @@ public static boolean isRegexSafe(String regex, String data, Optional<Integer> m
13281329
}
13291330
return isSafe;
13301331
}
1332+
1333+
/**
1334+
* Compute a UUID version 7 without using any external dependency.<br><br>
1335+
* <b>Below are my personal point of view and perhaps I'm totally wrong!</b>
1336+
* <br><br>
1337+
* Why such method?
1338+
* <ul>
1339+
* <li>Java <= 21 does not supports natively the generation of an UUID version 7.</li>
1340+
* <li>Import a library just to generate such value is overkill for me.</li>
1341+
* <li>Library that I have found, generating such version of an UUID, are not provided by entities commonly used in the java world, such as the SPRING framework provider.</li>
1342+
* </ul>
1343+
* <br>
1344+
* <b>Full credits for this implementation goes to the authors and contributors of the <a href="https://github.com/nalgeon/uuidv7">UUIDv7</a> project.</b>
1345+
* <br><br>
1346+
* Below are the java libraries that I have found but, for which, I do not trust enough the provider to use them directly:
1347+
* <ul>
1348+
* <li><a href="https://github.com/cowtowncoder/java-uuid-generator">java-uuid-generator</a></li>
1349+
* <li><a href="https://github.com/f4b6a3/uuid-creator">uuid-creator</a></li>
1350+
* </ul>
1351+
*
1352+
* @return A UUID object representing the UUID v7.
1353+
* @see "https://uuid7.com/"
1354+
* @see "https://antonz.org/uuidv7/"
1355+
* @see "https://mccue.dev/pages/3-11-25-life-altering-postgresql-patterns"
1356+
* @see "https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7"
1357+
* @see "https://www.baeldung.com/java-generating-time-based-uuids"
1358+
* @see "https://en.wikipedia.org/wiki/Universally_unique_identifier"
1359+
* @see "https://buildkite.com/resources/blog/goodbye-integers-hello-uuids/"
1360+
*/
1361+
public static UUID computeUUIDv7() {
1362+
SecureRandom secureRandom = new SecureRandom();
1363+
// Generate truly random bytes
1364+
byte[] value = new byte[16];
1365+
secureRandom.nextBytes(value);
1366+
// Get current timestamp in milliseconds
1367+
ByteBuffer timestamp = ByteBuffer.allocate(Long.BYTES);
1368+
timestamp.putLong(System.currentTimeMillis());
1369+
// Create the TIMESTAMP part of the UUID
1370+
System.arraycopy(timestamp.array(), 2, value, 0, 6);
1371+
// Create the VERSION and the VARIANT parts of the UUID
1372+
value[6] = (byte) ((value[6] & 0x0F) | 0x70);
1373+
value[8] = (byte) ((value[8] & 0x3F) | 0x80);
1374+
//Create the HIGH and LOW parts of the UUID
1375+
ByteBuffer buf = ByteBuffer.wrap(value);
1376+
long high = buf.getLong();
1377+
long low = buf.getLong();
1378+
//Create and return the UUID object
1379+
UUID uuidv7 = new UUID(high, low);
1380+
return uuidv7;
1381+
}
13311382
}

src/test/java/eu/righettod/TestSecurityUtils.java

+39
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import java.security.SecureRandom;
2525
import java.time.Duration;
2626
import java.time.Instant;
27+
import java.time.LocalDateTime;
28+
import java.time.ZoneId;
29+
import java.time.format.DateTimeFormatter;
2730
import java.util.*;
2831

2932
import static org.junit.jupiter.api.Assertions.*;
@@ -616,5 +619,41 @@ public void isRegexSafe() {
616619
result = SecurityUtils.isRegexSafe(testRegex, testData, Optional.empty());
617620
assertTrue(result, String.format(templateMsgFalsePositive, testRegex));
618621
}
622+
623+
@Test
624+
public void computeUUIDv7() {
625+
int candidatesCount = 1000;
626+
List<String> history = new ArrayList<>();
627+
UUID uuid;
628+
String uuidStr;
629+
String ref;
630+
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("YYYYMMddHHmmss");
631+
long mostSigBits;
632+
long timestampHigh;
633+
Instant instant;
634+
LocalDateTime localDateTime;
635+
for (int i = 0; i < candidatesCount; i++) {
636+
//Generate a UUID v7
637+
ref = LocalDateTime.now().format(dateFormat);
638+
uuid = SecurityUtils.computeUUIDv7();
639+
uuidStr = uuid.toString();
640+
//Apply validations
641+
//--Duplicate
642+
assertFalse(history.contains(uuidStr), "Duplicate generated UUID identified!");
643+
//--Version part
644+
assertEquals('7', uuidStr.charAt(14), "Invalid UUID version identified!");
645+
//--Timestamp part
646+
//----Retrieves the first 64 bits of the UUID via "getMostSignificantBits()"
647+
//----Perform a right shift by 16 bits to isolate the first 48 bits, which represent the timestamp
648+
//----Convert it to a Date object
649+
mostSigBits = uuid.getMostSignificantBits();
650+
timestampHigh = (mostSigBits >> 16) & 0xFFFFFFFFFFFFL;
651+
instant = Instant.ofEpochMilli(timestampHigh);
652+
localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
653+
assertEquals(ref, localDateTime.format(dateFormat), "Invalid UUID timestamp identified!");
654+
//Add it to the collection of generated UUID
655+
history.add(uuidStr);
656+
}
657+
}
619658
}
620659

0 commit comments

Comments
 (0)