diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ee02c..0f1e157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +Add Hash ULID generator methods. #25 Add a MIN and MAX constants and methods. #26 ## [5.2.0] - 2023-??-?? diff --git a/README.md b/README.md index e45b87e..907e7ca 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Add these lines to your `pom.xml`. com.github.f4b6a3 ulid-creator - 5.1.0 + 5.2.0 ``` See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java index 8a1aa0d..bac1633 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2020-2022 Fabio Lima + * Copyright (c) 2020-2023 Fabio Lima * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,11 +24,19 @@ package com.github.f4b6a3.ulid; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + /** * A class that generates ULIDs. *

* Both types of ULID can be easily created by this generator, i.e. monotonic * and non-monotonic. + *

+ * In addition, a "non-standard" hash-based ULID can also be generated, in which + * the random component is replaced with the first 10 bytes of an SHA-256 hash. */ public final class UlidCreator { @@ -37,6 +45,8 @@ public final class UlidCreator { /** * Returns a ULID. + *

+ * The random component is reset for each new ULID generated. * * @return a ULID */ @@ -45,7 +55,9 @@ public final class UlidCreator { } /** - * Returns a ULID with a given time. + * Returns a ULID. + *

+ * The random component is reset for each new ULID generated. * * @param time a number of milliseconds since 1970-01-01 (Unix epoch). * @return a ULID @@ -56,6 +68,9 @@ public final class UlidCreator { /** * Returns a Monotonic ULID. + *

+ * The random component is incremented for each new ULID generated in the same + * millisecond. * * @return a ULID */ @@ -64,7 +79,10 @@ public final class UlidCreator { } /** - * Returns a Monotonic ULID with a given time. + * Returns a Monotonic ULID. + *

+ * The random component is incremented for each new ULID generated in the same + * millisecond. * * @param time a number of milliseconds since 1970-01-01 (Unix epoch). * @return a ULID @@ -73,6 +91,68 @@ public final class UlidCreator { return MonotonicFactoryHolder.INSTANCE.create(time); } + /** + * Returns a Hash ULID. + *

+ * The random component is replaced with the first 10 bytes of an SHA-256 hash. + *

+ * It always returns the same ULID for a specific pair of {@code time} and + * {@code string}. + *

+ * Usage example: + * + *

{@code
+	 * long time = file.getCreatedAt();
+	 * String name = file.getFileName();
+	 * Ulid ulid = HashUlid.generate(time, name);
+	 * }
+ * + * @param time a number of milliseconds since 1970-01-01 (Unix epoch). + * @param string a string to be hashed using SHA-256 algorithm. + * @return a ULID + * @since 5.2.0 + */ + public static Ulid getHashUlid(final long time, String string) { + byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + return getHashUlid(time, bytes); + } + + /** + * Returns a Hash ULID. + *

+ * The random component is replaced with the first 10 bytes of an SHA-256 hash. + *

+ * It always returns the same ULID for a specific pair of {@code time} and + * {@code bytes}. + *

+ * Usage example: + * + *

{@code
+	 * long time = file.getCreatedAt();
+	 * byte[] bytes = file.getFileBinary();
+	 * Ulid ulid = HashUlid.generate(time, bytes);
+	 * }
+ * + * @param time a number of milliseconds since 1970-01-01 (Unix epoch). + * @param bytes a byte array to be hashed using SHA-256 algorithm. + * @return a ULID + * @since 5.2.0 + */ + public static Ulid getHashUlid(final long time, byte[] bytes) { + // Calculate the hash and take the first 10 bytes + byte[] hash = hasher("SHA-256").digest(bytes); + byte[] rand = Arrays.copyOf(hash, 10); + return new Ulid(time, rand); + } + + private static MessageDigest hasher(final String algorithm) { + try { + return MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(String.format("%s not supported", algorithm)); + } + } + private static class UlidFactoryHolder { static final UlidFactory INSTANCE = UlidFactory.newInstance(); } diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java index 0337bf3..f1d66e0 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UlidTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java @@ -629,6 +629,32 @@ public class UlidTest extends UlidFactoryTest { checkCreationTime(list, startTime, endTime); } + @Test + public void testGetHashUlid() throws NoSuchAlgorithmException { + + Ulid prev = Ulid.MIN; + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + long time = (new Random()).nextLong() >>> 16; + String string = UUID.randomUUID().toString(); + Ulid ulid = UlidCreator.getHashUlid(time, string); + + assertNotNull(ulid); + assertNotEquals(prev, ulid); + assertNotEquals(Ulid.MIN, ulid); + assertNotEquals(Ulid.MAX, ulid); + assertEquals(time, ulid.getTime()); + assertEquals(Arrays.toString(ulid.getRandom()), Arrays.toString(ulid.getRandom())); + + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] utf8 = string.getBytes(StandardCharsets.UTF_8); + byte[] hash = Arrays.copyOf(md.digest(utf8), 10); + assertEquals(Arrays.toString(ulid.getRandom()), Arrays.toString(hash)); + + prev = ulid; + } + } + public static Ulid fromString(String string) { long time = 0;