From: Mathieu Baudier Date: Mon, 24 Jan 2022 04:45:39 +0000 (+0100) Subject: Integrate UUID API X-Git-Tag: argeo-commons-2.3.5~67 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=580e8ff2af47001eb3e0947488395b48773be469 Integrate UUID API --- diff --git a/dep/org.argeo.dep.cms.base/pom.xml b/dep/org.argeo.dep.cms.base/pom.xml index 3a2af17f1..80ad7f793 100644 --- a/dep/org.argeo.dep.cms.base/pom.xml +++ b/dep/org.argeo.dep.cms.base/pom.xml @@ -13,7 +13,7 @@ CMS Base - + org.argeo.commons org.argeo.init @@ -25,6 +25,12 @@ 2.3-SNAPSHOT + + + org.argeo.commons + org.argeo.api.uuid + 2.3-SNAPSHOT + org.argeo.commons org.argeo.api.acr @@ -35,6 +41,8 @@ org.argeo.api.cms 2.3-SNAPSHOT + + org.argeo.commons org.argeo.cms.tp diff --git a/org.argeo.api.acr/pom.xml b/org.argeo.api.acr/pom.xml index bc2383831..dcb30e025 100644 --- a/org.argeo.api.acr/pom.xml +++ b/org.argeo.api.acr/pom.xml @@ -10,4 +10,11 @@ org.argeo.api.acr ACR API jar + + + org.argeo.commons + org.argeo.api.uuid + 2.3-SNAPSHOT + + \ No newline at end of file diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/A2.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/A2.java deleted file mode 100644 index 706edd160..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/A2.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.argeo.api.acr.uuid; - -import java.io.Serializable; - -/** A2 metadata for this package. */ -class A2 implements Serializable { - static final int MAJOR = 2; - static final int MINOR = 3; - - static final long serialVersionUID = (long) MAJOR << 32 | MINOR & 0xFFFFFFFFL; - - static { -// assert MAJOR == (int) (serialVersionUID >> 32); -// assert MINOR == (int) serialVersionUID; - } -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AbstractAsyncUuidFactory.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AbstractAsyncUuidFactory.java deleted file mode 100644 index 09becbaec..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AbstractAsyncUuidFactory.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.argeo.api.acr.uuid; - -import java.security.SecureRandom; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinTask; -import java.util.concurrent.ThreadLocalRandom; - -/** - * Execute {@link UUID} creations in {@link ForkJoinPool#commonPool()}. The goal - * is to provide good performance while staying within the parallelism defined - * for the system, so as to overwhelm it if many UUIDs are requested. - * Additionally, with regard to time based UUIDs, since we use - * {@link ConcurrentTimeUuidState}, which maintains one "clock sequence" per - * thread, we want to limit the number of threads accessing the actual - * generation method. - */ -public abstract class AbstractAsyncUuidFactory extends AbstractUuidFactory implements AsyncUuidFactory { - private SecureRandom secureRandom; - protected TimeUuidState timeUuidState; - - public AbstractAsyncUuidFactory() { - secureRandom = newSecureRandom(); - timeUuidState = new ConcurrentTimeUuidState(secureRandom, null); - } - /* - * ABSTRACT METHODS - */ - - protected abstract UUID newTimeUUID(); - - protected abstract UUID newTimeUUIDwithMacAddress(); - - protected abstract SecureRandom newSecureRandom(); - - /* - * SYNC OPERATIONS - */ - protected UUID newRandomUUIDStrong() { - return newRandomUUID(secureRandom); - } - - public UUID randomUUIDWeak() { - return newRandomUUID(ThreadLocalRandom.current()); - } - - /* - * ASYNC OPERATIONS (heavy) - */ - protected CompletionStage request(ForkJoinTask newUuid) { - return CompletableFuture.supplyAsync(newUuid::invoke).minimalCompletionStage(); - } - - @Override - public CompletionStage requestNameUUIDv5(UUID namespace, byte[] data) { - return request(futureNameUUIDv5(namespace, data)); - } - - @Override - public CompletionStage requestNameUUIDv3(UUID namespace, byte[] data) { - return request(futureNameUUIDv3(namespace, data)); - } - - @Override - public CompletionStage requestRandomUUIDStrong() { - return request(futureRandomUUIDStrong()); - } - - @Override - public CompletionStage requestTimeUUID() { - return request(futureTimeUUID()); - } - - @Override - public CompletionStage requestTimeUUIDwithMacAddress() { - return request(futureTimeUUIDwithMacAddress()); - } - - /* - * ASYNC OPERATIONS (light) - */ - protected ForkJoinTask submit(Callable newUuid) { - return ForkJoinTask.adapt(newUuid); - } - - @Override - public ForkJoinTask futureNameUUIDv5(UUID namespace, byte[] data) { - return submit(() -> newNameUUIDv5(namespace, data)); - } - - @Override - public ForkJoinTask futureNameUUIDv3(UUID namespace, byte[] data) { - return submit(() -> newNameUUIDv3(namespace, data)); - } - - @Override - public ForkJoinTask futureRandomUUIDStrong() { - return submit(this::newRandomUUIDStrong); - } - - @Override - public ForkJoinTask futureTimeUUID() { - return submit(this::newTimeUUID); - } - - @Override - public ForkJoinTask futureTimeUUIDwithMacAddress() { - return submit(this::newTimeUUIDwithMacAddress); - } - -// @Override -// public UUID timeUUID() { -// if (ConcurrentTimeUuidState.isTimeUuidThread.get()) -// return newTimeUUID(); -// else -// return futureTimeUUID().join(); -// } -// -// @Override -// public UUID timeUUIDwithMacAddress() { -// if (ConcurrentTimeUuidState.isTimeUuidThread.get()) -// return newTimeUUIDwithMacAddress(); -// else -// return futureTimeUUIDwithMacAddress().join(); -// } - -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AbstractUuidFactory.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AbstractUuidFactory.java deleted file mode 100644 index 1ecb64e12..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AbstractUuidFactory.java +++ /dev/null @@ -1,336 +0,0 @@ -package org.argeo.api.acr.uuid; - -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.time.Duration; -import java.time.temporal.Temporal; -import java.util.Objects; -import java.util.Random; -import java.util.UUID; - -/** - * Implementation of the basic RFC4122 algorithms. - * - * @see https://datatracker.ietf.org/doc/html/rfc4122 - */ -public abstract class AbstractUuidFactory implements UuidFactory { - - /* - * TIME-BASED (version 1) - */ - - private final static long MOST_SIG_VERSION1 = (1l << 12); - private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63); - - protected UUID newTimeUUID(long timestamp, long clockSequence, byte[] node, int offset) { - Objects.requireNonNull(node, "Node array cannot be null"); - if (node.length < offset + 6) - throw new IllegalArgumentException("Node array must be at least 6 bytes long"); - - long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID - | ((timestamp & 0xFFFFFFFFL) << 32) // time_low - | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid - | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version - - long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID - | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res - | ((clockSequence & 0xFF) << 48) // clk_seq_low - | (node[offset] & 0xFFL) // - | ((node[offset + 1] & 0xFFL) << 8) // - | ((node[offset + 2] & 0xFFL) << 16) // - | ((node[offset + 3] & 0xFFL) << 24) // - | ((node[offset + 4] & 0xFFL) << 32) // - | ((node[offset + 5] & 0xFFL) << 40); // - UUID uuid = new UUID(mostSig, leastSig); - - // tests -// assert uuid.node() == BitSet.valueOf(node).toLongArray()[0]; - // assert uuid.node() == longFromBytes(node); - assert uuid.timestamp() == timestamp; - assert uuid.clockSequence() == clockSequence - : "uuid.clockSequence()=" + uuid.clockSequence() + " clockSequence=" + clockSequence; - assert uuid.version() == 1; - assert uuid.variant() == 2; - return uuid; - } - - public UUID timeUUID(Temporal time, long clockSequence, byte[] node, int offset) { - // TODO add checks - Duration duration = Duration.between(TimeUuidState.GREGORIAN_START, time); - // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000 - long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100; - return newTimeUUID(timestamp, clockSequence, node, offset); - } - - protected byte[] getHardwareAddress() { - InetAddress localHost; - try { - localHost = InetAddress.getLocalHost(); - try { - NetworkInterface nic = NetworkInterface.getByInetAddress(localHost); - return nic.getHardwareAddress(); - } catch (SocketException e) { - return null; - } - } catch (UnknownHostException e) { - return null; - } - - } - /* - * NAME BASED (version 3 and 5) - */ - - protected UUID newNameUUIDv5(UUID namespace, byte[] name) { - Objects.requireNonNull(namespace, "Namespace cannot be null"); - Objects.requireNonNull(name, "Name cannot be null"); - - byte[] bytes = sha1(toBytes(namespace), name); - bytes[6] &= 0x0f; - bytes[6] |= 0x50;// v5 - bytes[8] &= 0x3f; - bytes[8] |= 0x80;// variant 1 - UUID result = fromBytes(bytes, 0); - return result; - } - - protected UUID newNameUUIDv3(UUID namespace, byte[] name) { - Objects.requireNonNull(namespace, "Namespace cannot be null"); - Objects.requireNonNull(name, "Name cannot be null"); - - byte[] arr = new byte[name.length + 16]; - copyBytes(namespace, arr, 0); - System.arraycopy(name, 0, arr, 16, name.length); - return UUID.nameUUIDFromBytes(arr); - } - - /* - * RANDOM v4 - */ - protected UUID newRandomUUID(Random random) { - byte[] arr = new byte[16]; - random.nextBytes(arr); - arr[6] &= 0x0f; - arr[6] |= 0x40;// v4 - arr[8] &= 0x3f; - arr[8] |= 0x80;// variant 1 - return fromBytes(arr); - } - - /* - * DIGEST UTILITIES - */ - - private final static String MD5 = "MD5"; - private final static String SHA1 = "SHA1"; - - protected byte[] sha1(byte[]... bytes) { - MessageDigest digest = getSha1Digest(); - for (byte[] arr : bytes) - digest.update(arr); - byte[] checksum = digest.digest(); - return checksum; - } - - protected byte[] md5(byte[]... bytes) { - MessageDigest digest = getMd5Digest(); - for (byte[] arr : bytes) - digest.update(arr); - byte[] checksum = digest.digest(); - return checksum; - } - - protected MessageDigest getSha1Digest() { - return getDigest(SHA1); - } - - protected MessageDigest getMd5Digest() { - return getDigest(MD5); - } - - private MessageDigest getDigest(String name) { - try { - return MessageDigest.getInstance(name); - } catch (NoSuchAlgorithmException e) { - throw new UnsupportedOperationException(name + " digest is not avalaible", e); - } - } - - /* - * UTILITIES - */ - /** - * Convert bytes to an UUID. Byte array must not be null and be exactly of - * length 16. - */ - protected UUID fromBytes(byte[] data) { - Objects.requireNonNull(data, "Byte array must not be null"); - if (data.length != 16) - throw new IllegalArgumentException("Byte array as length " + data.length); - return fromBytes(data, 0); - } - - /** - * Convert bytes to an UUID, starting to read the array at this offset. - */ - protected UUID fromBytes(byte[] data, int offset) { - Objects.requireNonNull(data, "Byte array cannot be null"); - long msb = 0; - long lsb = 0; - for (int i = offset; i < 8 + offset; i++) - msb = (msb << 8) | (data[i] & 0xff); - for (int i = 8 + offset; i < 16 + offset; i++) - lsb = (lsb << 8) | (data[i] & 0xff); - return new UUID(msb, lsb); - } - - protected long longFromBytes(byte[] data) { - long msb = 0; - for (int i = 0; i < data.length; i++) - msb = (msb << 8) | (data[i] & 0xff); - return msb; - } - - protected byte[] toBytes(UUID uuid) { - Objects.requireNonNull(uuid, "UUID cannot be null"); - long msb = uuid.getMostSignificantBits(); - long lsb = uuid.getLeastSignificantBits(); - return toBytes(msb, lsb); - } - - protected void copyBytes(UUID uuid, byte[] arr, int offset) { - Objects.requireNonNull(uuid, "UUID cannot be null"); - long msb = uuid.getMostSignificantBits(); - long lsb = uuid.getLeastSignificantBits(); - copyBytes(msb, lsb, arr, offset); - } - - /** - * Converts an UUID hex representation without '-' to the standard form (with - * '-'). - */ - public String compactToStd(String compact) { - if (compact.length() != 32) - throw new IllegalArgumentException( - "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32."); - StringBuilder sb = new StringBuilder(36); - for (int i = 0; i < 32; i++) { - if (i == 8 || i == 12 || i == 16 || i == 20) - sb.append('-'); - sb.append(compact.charAt(i)); - } - String std = sb.toString(); - assert std.length() == 36; - assert UUID.fromString(std).toString().equals(std); - return std; - } - - /** - * Converts an UUID hex representation without '-' to an {@link UUID}. - */ - public UUID fromCompact(String compact) { - return UUID.fromString(compactToStd(compact)); - } - - /** To a 32 characters hex string without '-'. */ - public String toCompact(UUID uuid) { - return toHexString(toBytes(uuid)); - } - - final protected static char[] hexArray = "0123456789abcdef".toCharArray(); - - /** Convert two longs to a byte array with length 16. */ - protected byte[] toBytes(long long1, long long2) { - byte[] result = new byte[16]; - for (int i = 0; i < 8; i++) - result[i] = (byte) ((long1 >> ((7 - i) * 8)) & 0xff); - for (int i = 8; i < 16; i++) - result[i] = (byte) ((long2 >> ((15 - i) * 8)) & 0xff); - return result; - } - - protected void copyBytes(long long1, long long2, byte[] arr, int offset) { - assert arr.length >= 16 + offset; - for (int i = offset; i < 8 + offset; i++) - arr[i] = (byte) ((long1 >> ((7 - i) * 8)) & 0xff); - for (int i = 8 + offset; i < 16 + offset; i++) - arr[i] = (byte) ((long2 >> ((15 - i) * 8)) & 0xff); - } - - /** Converts a byte array to an hex String. */ - protected String toHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - protected byte[] toNodeId(byte[] source, int offset) { - if (source == null) - return null; - if (offset < 0 || offset + 6 > source.length) - throw new ArrayIndexOutOfBoundsException(offset); - byte[] nodeId = new byte[6]; - System.arraycopy(source, offset, nodeId, 0, 6); - return nodeId; - } - - /* - * STATIC UTILITIES - */ - /** - * Converts an UUID to a binary string (list of 0 and 1), with a separator to - * make it more readable. - */ - public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) { - Objects.requireNonNull(uuid, "UUID cannot be null"); - String binaryString = toBinaryString(uuid); - StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment)); - for (int i = 0; i < binaryString.length(); i++) { - if (i != 0 && i % charsPerSegment == 0) - sb.append(separator); - sb.append(binaryString.charAt(i)); - } - return sb.toString(); - } - - /** Converts an UUID to a binary string (list of 0 and 1). */ - public static String toBinaryString(UUID uuid) { - Objects.requireNonNull(uuid, "UUID cannot be null"); - String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits())); - String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits())); - String binaryString = most + least; - assert binaryString.length() == 128; - return binaryString; - } - - /** - * Force this node id to be identified as no MAC address. - * - * @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.5 - */ - public static void forceToNoMacAddress(byte[] nodeId, int offset) { - assert nodeId != null && offset < nodeId.length; - nodeId[offset] = (byte) (nodeId[offset] | 1); - } - - private static String zeroTo64Chars(String str) { - assert str.length() <= 64; - if (str.length() < 64) { - StringBuilder sb = new StringBuilder(64); - for (int i = 0; i < 64 - str.length(); i++) - sb.append('0'); - sb.append(str); - return sb.toString(); - } else - return str; - } - -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AsyncUuidFactory.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AsyncUuidFactory.java deleted file mode 100644 index 97751a6b0..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/AsyncUuidFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.argeo.api.acr.uuid; - -import java.util.UUID; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ForkJoinTask; - -/** A {@link UUID} factory which creates the UUIDs asynchronously. */ -public interface AsyncUuidFactory extends UuidFactory { - /* - * TIME-BASED (version 1) - */ - CompletionStage requestTimeUUID(); - - CompletionStage requestTimeUUIDwithMacAddress(); - - ForkJoinTask futureTimeUUID(); - - ForkJoinTask futureTimeUUIDwithMacAddress(); - - /* - * NAME BASED (version 3 and 5) - */ - CompletionStage requestNameUUIDv5(UUID namespace, byte[] data); - - CompletionStage requestNameUUIDv3(UUID namespace, byte[] data); - - ForkJoinTask futureNameUUIDv5(UUID namespace, byte[] data); - - ForkJoinTask futureNameUUIDv3(UUID namespace, byte[] data); - - /* - * RANDOM (version 4) - */ - CompletionStage requestRandomUUIDStrong(); - - ForkJoinTask futureRandomUUIDStrong(); - - /* - * DEFAULTS - */ - @Override - default UUID randomUUIDStrong() { - return futureRandomUUIDStrong().invoke(); - } - - @Override - default UUID timeUUID() { - return futureTimeUUID().invoke(); - } - - @Override - default UUID timeUUIDwithMacAddress() { - return futureTimeUUIDwithMacAddress().invoke(); - } - - @Override - default UUID nameUUIDv5(UUID namespace, byte[] data) { - return futureNameUUIDv5(namespace, data).invoke(); - } - - @Override - default UUID nameUUIDv3(UUID namespace, byte[] data) { - return futureNameUUIDv3(namespace, data).invoke(); - } -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/ConcurrentTimeUuidState.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/ConcurrentTimeUuidState.java deleted file mode 100644 index db72a4b21..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/ConcurrentTimeUuidState.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.argeo.api.acr.uuid; - -import java.security.SecureRandom; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.util.Objects; - -/** - * A simple base implementation of {@link TimeUuidState}, which maintains - * different clock sequences for each thread. - */ -public class ConcurrentTimeUuidState implements TimeUuidState { -// public final static ThreadLocal isTimeUuidThread = new ThreadLocal<>() { -// -// @Override -// protected Boolean initialValue() { -// return false; -// } -// }; - - /** The maximum possible value of the clocksequence. */ - private final static int MAX_CLOCKSEQUENCE = 16384; - - private final ThreadLocal holder; - - private final Instant startInstant; - /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */ - private final long startTimeStamp; - - private final Clock clock; - private final boolean useClockForMeasurement; - - private final SecureRandom secureRandom; - - public ConcurrentTimeUuidState(SecureRandom secureRandom, Clock clock) { - useClockForMeasurement = clock != null; - this.clock = clock != null ? clock : Clock.systemUTC(); - - Objects.requireNonNull(secureRandom); - this.secureRandom = secureRandom; - - // compute the start reference - startInstant = Instant.now(this.clock); - long nowVm = nowVm(); - Duration duration = Duration.between(TimeUuidState.GREGORIAN_START, startInstant); - startTimeStamp = durationToUuidTimestamp(duration) - nowVm; - - // initalise a state per thread - holder = new ThreadLocal<>() { - - @Override - protected Holder initialValue() { - Holder value = new Holder(); - value.lastTimestamp = startTimeStamp; - value.clockSequence = newClockSequence(); -// isTimeUuidThread.set(true); - return value; - } - }; - } - - /* - * TIME OPERATIONS - */ - - public long useTimestamp() { - - long previousTimestamp = holder.get().lastTimestamp; - long now = computeNow(); - - // rare case where we are sooner - // (e.g. if system time has changed in between and we use the clock) - if (previousTimestamp > now) { - long newClockSequence = newClockSequence(); - for (int i = 0; i < 64; i++) { - if (newClockSequence != holder.get().clockSequence) - break; - newClockSequence = newClockSequence(); - } - if (newClockSequence != holder.get().clockSequence) - throw new IllegalStateException("Cannot change clock sequence"); - holder.get().clockSequence = newClockSequence; - } - - // very unlikely case where it took less than 100ns between both - if (previousTimestamp == now) { - try { - Thread.sleep(0, 100); - } catch (InterruptedException e) { - // silent - } - now = computeNow(); - assert previousTimestamp != now; - } - holder.get().lastTimestamp = now; - return now; - } - - protected long computeNow() { - if (useClockForMeasurement) { - Duration duration = Duration.between(TimeUuidState.GREGORIAN_START, Instant.now(clock)); - return durationToUuidTimestamp(duration); - } else { - return startTimeStamp + nowVm(); - } - } - - private long nowVm() { - return System.nanoTime() / 100; - } - - protected long durationToUuidTimestamp(Duration duration) { - return (duration.getSeconds() * 10000000 + duration.getNano() / 100); - } - - /* - * STATE OPERATIONS - */ - - protected long newClockSequence() { - return secureRandom.nextInt(ConcurrentTimeUuidState.MAX_CLOCKSEQUENCE); - } - - /* - * ACCESSORS - */ - -// @Override -// public byte[] getNodeId() { -// byte[] arr = new byte[6]; -// System.arraycopy(holder.get().nodeId, 0, arr, 0, 6); -// return arr; -// } - - @Override - public long getClockSequence() { - return holder.get().clockSequence; - } -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/SimpleUuidFactory.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/SimpleUuidFactory.java deleted file mode 100644 index cffd446bd..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/SimpleUuidFactory.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.argeo.api.acr.uuid; - -import static java.lang.System.Logger.Level.DEBUG; -import static java.lang.System.Logger.Level.WARNING; - -import java.lang.System.Logger; -import java.security.DrbgParameters; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.time.Clock; -import java.util.UUID; - -/** - * Simple implementation of an {@link UuidFactory}, which can be used as a base - * class for more optimised implementations. - * - * @see https://datatracker.ietf.org/doc/html/rfc4122 - */ -public class SimpleUuidFactory extends AbstractAsyncUuidFactory { - private final static Logger logger = System.getLogger(SimpleUuidFactory.class.getName()); - public final static UuidFactory DEFAULT = new SimpleUuidFactory(null, -1, null); - -// private NodeId macAddressNodeId; -// private NodeId defaultNodeId; - private byte[] macAddressNodeId; - private byte[] defaultNodeId; - - public SimpleUuidFactory(byte[] nodeId, int offset, Clock clock) { - byte[] hardwareAddress = getHardwareAddress(); -// macAddressNodeId = hardwareAddress != null ? new NodeId(hardwareAddress, 0) : null; - macAddressNodeId = toNodeId(hardwareAddress, 0); - -// defaultNodeId = nodeId != null ? new NodeId(nodeId, offset) : macAddressNodeId; - defaultNodeId = nodeId != null ? toNodeId(nodeId, offset) : toNodeId(macAddressNodeId, 0); - if (defaultNodeId == null) - throw new IllegalStateException("No default node id specified"); - } - - @Override - protected SecureRandom newSecureRandom() { - SecureRandom secureRandom; - try { - secureRandom = SecureRandom.getInstance("DRBG", - DrbgParameters.instantiation(256, DrbgParameters.Capability.PR_AND_RESEED, "UUID".getBytes())); - } catch (NoSuchAlgorithmException e) { - try { - logger.log(DEBUG, "DRBG secure random not found, using strong"); - secureRandom = SecureRandom.getInstanceStrong(); - } catch (NoSuchAlgorithmException e1) { - logger.log(WARNING, "No strong secure random was found, using default"); - secureRandom = new SecureRandom(); - } - } - return secureRandom; - } - - /* - * TIME-BASED (version 1) - */ - - @Override - public UUID newTimeUUIDwithMacAddress() { - if (macAddressNodeId == null) - throw new UnsupportedOperationException("No MAC address is available"); - return newTimeUUID(timeUuidState.useTimestamp(), timeUuidState.getClockSequence(), macAddressNodeId, 0); - } - - @Override - public UUID newTimeUUID() { - return newTimeUUID(timeUuidState.useTimestamp(), timeUuidState.getClockSequence(), defaultNodeId, 0); - } - - /* - * RANDOM v4 - */ -// @Override -// public UUID randomUUID(Random random) { -// return newRandomUUID(random); -// } - -// @Override -// public UUID randomUUID() { -// return randomUUID(secureRandom); -// } -// -// static class NodeId extends ThreadLocal { -// private byte[] source; -// private int offset; -// -// public NodeId(byte[] source, int offset) { -// Objects.requireNonNull(source); -// this.source = source; -// this.offset = offset; -// if (offset < 0 || offset + 6 > source.length) -// throw new ArrayIndexOutOfBoundsException(offset); -// } -// -// @Override -// protected byte[] initialValue() { -// byte[] value = new byte[6]; -// System.arraycopy(source, offset, value, 0, 6); -// return value; -// } -// -// } -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/TimeUuidState.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/TimeUuidState.java deleted file mode 100644 index 74e6b6980..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/TimeUuidState.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.argeo.api.acr.uuid; - -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -/** - * The state of a time based UUID generator, as described and discussed in - * section 4.2.1 of RFC4122. - * - * @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.2.1 - */ -public interface TimeUuidState { - - /** Start of the Gregorian time, used by time-based UUID (v1). */ - final static Instant GREGORIAN_START = ZonedDateTime.of(1582, 10, 15, 0, 0, 0, 0, ZoneOffset.UTC).toInstant(); - - long useTimestamp(); - - long getClockSequence(); - - static boolean isNoMacAddressNodeId(byte[] nodeId) { - return (nodeId[0] & 1) != 0; - } - - static class Holder { - long lastTimestamp; - long clockSequence; - - public long getLastTimestamp() { - return lastTimestamp; - } - - public void setLastTimestamp(long lastTimestamp) { - this.lastTimestamp = lastTimestamp; - } - - public long getClockSequence() { - return clockSequence; - } - - public void setClockSequence(long clockSequence) { - this.clockSequence = clockSequence; - } - - } -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/UuidFactory.java b/org.argeo.api.acr/src/org/argeo/api/acr/uuid/UuidFactory.java deleted file mode 100644 index 94e5158df..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/uuid/UuidFactory.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.argeo.api.acr.uuid; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.Supplier; - -/** - * A provider of RFC 4122 {@link UUID}s. Only the RFC 4122 variant (also known - * as Leach–Salz variant) is supported. The default, returned by the - * {@link Supplier#get()} method MUST be a v4 UUID (random). - * - * @see UUID - * @see https://datatracker.ietf.org/doc/html/rfc4122 - */ -public interface UuidFactory extends Supplier { - /* - * TIME-BASED (version 1) - */ - - UUID timeUUID(); - - UUID timeUUIDwithMacAddress(); - - /* - * NAME BASED (version 3 and 5) - */ - - UUID nameUUIDv5(UUID namespace, byte[] data); - - UUID nameUUIDv3(UUID namespace, byte[] data); - - default UUID nameUUIDv5(UUID namespace, String name) { - Objects.requireNonNull(name, "Name cannot be null"); - return nameUUIDv5(namespace, name.getBytes(UTF_8)); - } - - default UUID nameUUIDv3(UUID namespace, String name) { - Objects.requireNonNull(name, "Name cannot be null"); - return nameUUIDv3(namespace, name.getBytes(UTF_8)); - } - - /* - * RANDOM (version 4) - */ - /** A random UUID at least as good as {@link UUID#randomUUID()}. */ - UUID randomUUIDStrong(); - - /** - * An {@link UUID} generated based on {@link ThreadLocalRandom}. Implementations - * should always provide it synchronously. - */ - UUID randomUUIDWeak(); - - /** - * The default random {@link UUID} (v4) generator to use. This default - * implementation returns {@link #randomUUIDStrong()}. - */ - default UUID randomUUID() { - return randomUUIDStrong(); - } - - /** - * The default {@link UUID} to provide, either random (v4) or time based (v1). - * This default implementations returns {@link #randomUUID()}. - */ - @Override - default UUID get() { - return randomUUID(); - } - - /* - * STANDARD UUIDs - */ - - /** Nil UUID (00000000-0000-0000-0000-000000000000). */ - final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); - /** - * Standard DNS namespace ID for type 3 or 5 UUID (as defined in Appendix C of - * RFC4122). - */ - final static UUID NS_DNS = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); - /** - * Standard URL namespace ID for type 3 or 5 UUID (as defined in Appendix C of - * RFC4122). - */ - final static UUID NS_URL = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); - /** - * Standard OID namespace ID (typically an LDAP type) for type 3 or 5 UUID (as - * defined in Appendix C of RFC4122). - */ - final static UUID NS_OID = UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8"); - /** - * Standard X500 namespace ID (typically an LDAP DN) for type 3 or 5 UUID (as - * defined in Appendix C of RFC4122). - */ - final static UUID NS_X500 = UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"); - - /* - * UTILITIES - */ - - static boolean isRandom(UUID uuid) { - return uuid.version() == 4; - } - - static boolean isTimeBased(UUID uuid) { - return uuid.version() == 1; - } - - /** - * Whether this UUID is time based but was not generated from an IEEE 802 - * address, as per Section 4.5 of RFC4122. - * - * @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.5 - */ - static boolean isTimeBasedWithMacAddress(UUID uuid) { - if (uuid.version() == 1) { - return (uuid.node() & 1L) == 0; - } else - return false; - } - - static boolean isNameBased(UUID uuid) { - return uuid.version() == 3 || uuid.version() == 5; - } -} diff --git a/org.argeo.api.cms/pom.xml b/org.argeo.api.cms/pom.xml index b1cfca0d3..6c0666140 100644 --- a/org.argeo.api.cms/pom.xml +++ b/org.argeo.api.cms/pom.xml @@ -12,4 +12,11 @@ org.argeo.api.cms CMS API jar + + + org.argeo.commons + org.argeo.api.acr + 2.3-SNAPSHOT + + \ No newline at end of file diff --git a/org.argeo.api.uuid/bnd.bnd b/org.argeo.api.uuid/bnd.bnd new file mode 100644 index 000000000..e69de29bb diff --git a/org.argeo.api.uuid/pom.xml b/org.argeo.api.uuid/pom.xml new file mode 100644 index 000000000..4db0ba67a --- /dev/null +++ b/org.argeo.api.uuid/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + org.argeo.commons + argeo-commons + 2.3-SNAPSHOT + .. + + org.argeo.api.uuid + UUID API + jar + \ No newline at end of file diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/A2.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/A2.java new file mode 100644 index 000000000..e2d47278d --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/A2.java @@ -0,0 +1,16 @@ +package org.argeo.api.uuid; + +import java.io.Serializable; + +/** A2 metadata for this package. */ +class A2 implements Serializable { + static final int MAJOR = 2; + static final int MINOR = 3; + + static final long serialVersionUID = (long) MAJOR << 32 | MINOR & 0xFFFFFFFFL; + + static { +// assert MAJOR == (int) (serialVersionUID >> 32); +// assert MINOR == (int) serialVersionUID; + } +} diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractAsyncUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractAsyncUuidFactory.java new file mode 100644 index 000000000..2cdb59f77 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractAsyncUuidFactory.java @@ -0,0 +1,130 @@ +package org.argeo.api.uuid; + +import java.security.SecureRandom; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Execute {@link UUID} creations in {@link ForkJoinPool#commonPool()}. The goal + * is to provide good performance while staying within the parallelism defined + * for the system, so as to overwhelm it if many UUIDs are requested. + * Additionally, with regard to time based UUIDs, since we use + * {@link ConcurrentTimeUuidState}, which maintains one "clock sequence" per + * thread, we want to limit the number of threads accessing the actual + * generation method. + */ +public abstract class AbstractAsyncUuidFactory extends AbstractUuidFactory implements AsyncUuidFactory { + private SecureRandom secureRandom; + protected TimeUuidState timeUuidState; + + public AbstractAsyncUuidFactory() { + secureRandom = newSecureRandom(); + timeUuidState = new ConcurrentTimeUuidState(secureRandom, null); + } + /* + * ABSTRACT METHODS + */ + + protected abstract UUID newTimeUUID(); + + protected abstract UUID newTimeUUIDwithMacAddress(); + + protected abstract SecureRandom newSecureRandom(); + + /* + * SYNC OPERATIONS + */ + protected UUID newRandomUUIDStrong() { + return newRandomUUID(secureRandom); + } + + public UUID randomUUIDWeak() { + return newRandomUUID(ThreadLocalRandom.current()); + } + + /* + * ASYNC OPERATIONS (heavy) + */ + protected CompletionStage request(ForkJoinTask newUuid) { + return CompletableFuture.supplyAsync(newUuid::invoke).minimalCompletionStage(); + } + + @Override + public CompletionStage requestNameUUIDv5(UUID namespace, byte[] data) { + return request(futureNameUUIDv5(namespace, data)); + } + + @Override + public CompletionStage requestNameUUIDv3(UUID namespace, byte[] data) { + return request(futureNameUUIDv3(namespace, data)); + } + + @Override + public CompletionStage requestRandomUUIDStrong() { + return request(futureRandomUUIDStrong()); + } + + @Override + public CompletionStage requestTimeUUID() { + return request(futureTimeUUID()); + } + + @Override + public CompletionStage requestTimeUUIDwithMacAddress() { + return request(futureTimeUUIDwithMacAddress()); + } + + /* + * ASYNC OPERATIONS (light) + */ + protected ForkJoinTask submit(Callable newUuid) { + return ForkJoinTask.adapt(newUuid); + } + + @Override + public ForkJoinTask futureNameUUIDv5(UUID namespace, byte[] data) { + return submit(() -> newNameUUIDv5(namespace, data)); + } + + @Override + public ForkJoinTask futureNameUUIDv3(UUID namespace, byte[] data) { + return submit(() -> newNameUUIDv3(namespace, data)); + } + + @Override + public ForkJoinTask futureRandomUUIDStrong() { + return submit(this::newRandomUUIDStrong); + } + + @Override + public ForkJoinTask futureTimeUUID() { + return submit(this::newTimeUUID); + } + + @Override + public ForkJoinTask futureTimeUUIDwithMacAddress() { + return submit(this::newTimeUUIDwithMacAddress); + } + +// @Override +// public UUID timeUUID() { +// if (ConcurrentTimeUuidState.isTimeUuidThread.get()) +// return newTimeUUID(); +// else +// return futureTimeUUID().join(); +// } +// +// @Override +// public UUID timeUUIDwithMacAddress() { +// if (ConcurrentTimeUuidState.isTimeUuidThread.get()) +// return newTimeUUIDwithMacAddress(); +// else +// return futureTimeUUIDwithMacAddress().join(); +// } + +} diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractUuidFactory.java new file mode 100644 index 000000000..ba2196add --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/AbstractUuidFactory.java @@ -0,0 +1,336 @@ +package org.argeo.api.uuid; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.temporal.Temporal; +import java.util.Objects; +import java.util.Random; +import java.util.UUID; + +/** + * Implementation of the basic RFC4122 algorithms. + * + * @see https://datatracker.ietf.org/doc/html/rfc4122 + */ +public abstract class AbstractUuidFactory implements UuidFactory { + + /* + * TIME-BASED (version 1) + */ + + private final static long MOST_SIG_VERSION1 = (1l << 12); + private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63); + + protected UUID newTimeUUID(long timestamp, long clockSequence, byte[] node, int offset) { + Objects.requireNonNull(node, "Node array cannot be null"); + if (node.length < offset + 6) + throw new IllegalArgumentException("Node array must be at least 6 bytes long"); + + long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID + | ((timestamp & 0xFFFFFFFFL) << 32) // time_low + | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid + | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version + + long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID + | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res + | ((clockSequence & 0xFF) << 48) // clk_seq_low + | (node[offset] & 0xFFL) // + | ((node[offset + 1] & 0xFFL) << 8) // + | ((node[offset + 2] & 0xFFL) << 16) // + | ((node[offset + 3] & 0xFFL) << 24) // + | ((node[offset + 4] & 0xFFL) << 32) // + | ((node[offset + 5] & 0xFFL) << 40); // + UUID uuid = new UUID(mostSig, leastSig); + + // tests +// assert uuid.node() == BitSet.valueOf(node).toLongArray()[0]; + // assert uuid.node() == longFromBytes(node); + assert uuid.timestamp() == timestamp; + assert uuid.clockSequence() == clockSequence + : "uuid.clockSequence()=" + uuid.clockSequence() + " clockSequence=" + clockSequence; + assert uuid.version() == 1; + assert uuid.variant() == 2; + return uuid; + } + + public UUID timeUUID(Temporal time, long clockSequence, byte[] node, int offset) { + // TODO add checks + Duration duration = Duration.between(TimeUuidState.GREGORIAN_START, time); + // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000 + long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100; + return newTimeUUID(timestamp, clockSequence, node, offset); + } + + protected byte[] getHardwareAddress() { + InetAddress localHost; + try { + localHost = InetAddress.getLocalHost(); + try { + NetworkInterface nic = NetworkInterface.getByInetAddress(localHost); + return nic.getHardwareAddress(); + } catch (SocketException e) { + return null; + } + } catch (UnknownHostException e) { + return null; + } + + } + /* + * NAME BASED (version 3 and 5) + */ + + protected UUID newNameUUIDv5(UUID namespace, byte[] name) { + Objects.requireNonNull(namespace, "Namespace cannot be null"); + Objects.requireNonNull(name, "Name cannot be null"); + + byte[] bytes = sha1(toBytes(namespace), name); + bytes[6] &= 0x0f; + bytes[6] |= 0x50;// v5 + bytes[8] &= 0x3f; + bytes[8] |= 0x80;// variant 1 + UUID result = fromBytes(bytes, 0); + return result; + } + + protected UUID newNameUUIDv3(UUID namespace, byte[] name) { + Objects.requireNonNull(namespace, "Namespace cannot be null"); + Objects.requireNonNull(name, "Name cannot be null"); + + byte[] arr = new byte[name.length + 16]; + copyBytes(namespace, arr, 0); + System.arraycopy(name, 0, arr, 16, name.length); + return UUID.nameUUIDFromBytes(arr); + } + + /* + * RANDOM v4 + */ + protected UUID newRandomUUID(Random random) { + byte[] arr = new byte[16]; + random.nextBytes(arr); + arr[6] &= 0x0f; + arr[6] |= 0x40;// v4 + arr[8] &= 0x3f; + arr[8] |= 0x80;// variant 1 + return fromBytes(arr); + } + + /* + * DIGEST UTILITIES + */ + + private final static String MD5 = "MD5"; + private final static String SHA1 = "SHA1"; + + protected byte[] sha1(byte[]... bytes) { + MessageDigest digest = getSha1Digest(); + for (byte[] arr : bytes) + digest.update(arr); + byte[] checksum = digest.digest(); + return checksum; + } + + protected byte[] md5(byte[]... bytes) { + MessageDigest digest = getMd5Digest(); + for (byte[] arr : bytes) + digest.update(arr); + byte[] checksum = digest.digest(); + return checksum; + } + + protected MessageDigest getSha1Digest() { + return getDigest(SHA1); + } + + protected MessageDigest getMd5Digest() { + return getDigest(MD5); + } + + private MessageDigest getDigest(String name) { + try { + return MessageDigest.getInstance(name); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(name + " digest is not avalaible", e); + } + } + + /* + * UTILITIES + */ + /** + * Convert bytes to an UUID. Byte array must not be null and be exactly of + * length 16. + */ + protected UUID fromBytes(byte[] data) { + Objects.requireNonNull(data, "Byte array must not be null"); + if (data.length != 16) + throw new IllegalArgumentException("Byte array as length " + data.length); + return fromBytes(data, 0); + } + + /** + * Convert bytes to an UUID, starting to read the array at this offset. + */ + protected UUID fromBytes(byte[] data, int offset) { + Objects.requireNonNull(data, "Byte array cannot be null"); + long msb = 0; + long lsb = 0; + for (int i = offset; i < 8 + offset; i++) + msb = (msb << 8) | (data[i] & 0xff); + for (int i = 8 + offset; i < 16 + offset; i++) + lsb = (lsb << 8) | (data[i] & 0xff); + return new UUID(msb, lsb); + } + + protected long longFromBytes(byte[] data) { + long msb = 0; + for (int i = 0; i < data.length; i++) + msb = (msb << 8) | (data[i] & 0xff); + return msb; + } + + protected byte[] toBytes(UUID uuid) { + Objects.requireNonNull(uuid, "UUID cannot be null"); + long msb = uuid.getMostSignificantBits(); + long lsb = uuid.getLeastSignificantBits(); + return toBytes(msb, lsb); + } + + protected void copyBytes(UUID uuid, byte[] arr, int offset) { + Objects.requireNonNull(uuid, "UUID cannot be null"); + long msb = uuid.getMostSignificantBits(); + long lsb = uuid.getLeastSignificantBits(); + copyBytes(msb, lsb, arr, offset); + } + + /** + * Converts an UUID hex representation without '-' to the standard form (with + * '-'). + */ + public String compactToStd(String compact) { + if (compact.length() != 32) + throw new IllegalArgumentException( + "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32."); + StringBuilder sb = new StringBuilder(36); + for (int i = 0; i < 32; i++) { + if (i == 8 || i == 12 || i == 16 || i == 20) + sb.append('-'); + sb.append(compact.charAt(i)); + } + String std = sb.toString(); + assert std.length() == 36; + assert UUID.fromString(std).toString().equals(std); + return std; + } + + /** + * Converts an UUID hex representation without '-' to an {@link UUID}. + */ + public UUID fromCompact(String compact) { + return UUID.fromString(compactToStd(compact)); + } + + /** To a 32 characters hex string without '-'. */ + public String toCompact(UUID uuid) { + return toHexString(toBytes(uuid)); + } + + final protected static char[] hexArray = "0123456789abcdef".toCharArray(); + + /** Convert two longs to a byte array with length 16. */ + protected byte[] toBytes(long long1, long long2) { + byte[] result = new byte[16]; + for (int i = 0; i < 8; i++) + result[i] = (byte) ((long1 >> ((7 - i) * 8)) & 0xff); + for (int i = 8; i < 16; i++) + result[i] = (byte) ((long2 >> ((15 - i) * 8)) & 0xff); + return result; + } + + protected void copyBytes(long long1, long long2, byte[] arr, int offset) { + assert arr.length >= 16 + offset; + for (int i = offset; i < 8 + offset; i++) + arr[i] = (byte) ((long1 >> ((7 - i) * 8)) & 0xff); + for (int i = 8 + offset; i < 16 + offset; i++) + arr[i] = (byte) ((long2 >> ((15 - i) * 8)) & 0xff); + } + + /** Converts a byte array to an hex String. */ + protected String toHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + protected byte[] toNodeId(byte[] source, int offset) { + if (source == null) + return null; + if (offset < 0 || offset + 6 > source.length) + throw new ArrayIndexOutOfBoundsException(offset); + byte[] nodeId = new byte[6]; + System.arraycopy(source, offset, nodeId, 0, 6); + return nodeId; + } + + /* + * STATIC UTILITIES + */ + /** + * Converts an UUID to a binary string (list of 0 and 1), with a separator to + * make it more readable. + */ + public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) { + Objects.requireNonNull(uuid, "UUID cannot be null"); + String binaryString = toBinaryString(uuid); + StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment)); + for (int i = 0; i < binaryString.length(); i++) { + if (i != 0 && i % charsPerSegment == 0) + sb.append(separator); + sb.append(binaryString.charAt(i)); + } + return sb.toString(); + } + + /** Converts an UUID to a binary string (list of 0 and 1). */ + public static String toBinaryString(UUID uuid) { + Objects.requireNonNull(uuid, "UUID cannot be null"); + String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits())); + String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits())); + String binaryString = most + least; + assert binaryString.length() == 128; + return binaryString; + } + + /** + * Force this node id to be identified as no MAC address. + * + * @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.5 + */ + public static void forceToNoMacAddress(byte[] nodeId, int offset) { + assert nodeId != null && offset < nodeId.length; + nodeId[offset] = (byte) (nodeId[offset] | 1); + } + + private static String zeroTo64Chars(String str) { + assert str.length() <= 64; + if (str.length() < 64) { + StringBuilder sb = new StringBuilder(64); + for (int i = 0; i < 64 - str.length(); i++) + sb.append('0'); + sb.append(str); + return sb.toString(); + } else + return str; + } + +} diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/AsyncUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/AsyncUuidFactory.java new file mode 100644 index 000000000..dd40f81e9 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/AsyncUuidFactory.java @@ -0,0 +1,65 @@ +package org.argeo.api.uuid; + +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ForkJoinTask; + +/** A {@link UUID} factory which creates the UUIDs asynchronously. */ +public interface AsyncUuidFactory extends UuidFactory { + /* + * TIME-BASED (version 1) + */ + CompletionStage requestTimeUUID(); + + CompletionStage requestTimeUUIDwithMacAddress(); + + ForkJoinTask futureTimeUUID(); + + ForkJoinTask futureTimeUUIDwithMacAddress(); + + /* + * NAME BASED (version 3 and 5) + */ + CompletionStage requestNameUUIDv5(UUID namespace, byte[] data); + + CompletionStage requestNameUUIDv3(UUID namespace, byte[] data); + + ForkJoinTask futureNameUUIDv5(UUID namespace, byte[] data); + + ForkJoinTask futureNameUUIDv3(UUID namespace, byte[] data); + + /* + * RANDOM (version 4) + */ + CompletionStage requestRandomUUIDStrong(); + + ForkJoinTask futureRandomUUIDStrong(); + + /* + * DEFAULTS + */ + @Override + default UUID randomUUIDStrong() { + return futureRandomUUIDStrong().invoke(); + } + + @Override + default UUID timeUUID() { + return futureTimeUUID().invoke(); + } + + @Override + default UUID timeUUIDwithMacAddress() { + return futureTimeUUIDwithMacAddress().invoke(); + } + + @Override + default UUID nameUUIDv5(UUID namespace, byte[] data) { + return futureNameUUIDv5(namespace, data).invoke(); + } + + @Override + default UUID nameUUIDv3(UUID namespace, byte[] data) { + return futureNameUUIDv3(namespace, data).invoke(); + } +} diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/ConcurrentTimeUuidState.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/ConcurrentTimeUuidState.java new file mode 100644 index 000000000..9a414e8fa --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/ConcurrentTimeUuidState.java @@ -0,0 +1,140 @@ +package org.argeo.api.uuid; + +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; + +/** + * A simple base implementation of {@link TimeUuidState}, which maintains + * different clock sequences for each thread. + */ +public class ConcurrentTimeUuidState implements TimeUuidState { +// public final static ThreadLocal isTimeUuidThread = new ThreadLocal<>() { +// +// @Override +// protected Boolean initialValue() { +// return false; +// } +// }; + + /** The maximum possible value of the clocksequence. */ + private final static int MAX_CLOCKSEQUENCE = 16384; + + private final ThreadLocal holder; + + private final Instant startInstant; + /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */ + private final long startTimeStamp; + + private final Clock clock; + private final boolean useClockForMeasurement; + + private final SecureRandom secureRandom; + + public ConcurrentTimeUuidState(SecureRandom secureRandom, Clock clock) { + useClockForMeasurement = clock != null; + this.clock = clock != null ? clock : Clock.systemUTC(); + + Objects.requireNonNull(secureRandom); + this.secureRandom = secureRandom; + + // compute the start reference + startInstant = Instant.now(this.clock); + long nowVm = nowVm(); + Duration duration = Duration.between(TimeUuidState.GREGORIAN_START, startInstant); + startTimeStamp = durationToUuidTimestamp(duration) - nowVm; + + // initalise a state per thread + holder = new ThreadLocal<>() { + + @Override + protected Holder initialValue() { + Holder value = new Holder(); + value.lastTimestamp = startTimeStamp; + value.clockSequence = newClockSequence(); +// isTimeUuidThread.set(true); + return value; + } + }; + } + + /* + * TIME OPERATIONS + */ + + public long useTimestamp() { + + long previousTimestamp = holder.get().lastTimestamp; + long now = computeNow(); + + // rare case where we are sooner + // (e.g. if system time has changed in between and we use the clock) + if (previousTimestamp > now) { + long newClockSequence = newClockSequence(); + for (int i = 0; i < 64; i++) { + if (newClockSequence != holder.get().clockSequence) + break; + newClockSequence = newClockSequence(); + } + if (newClockSequence != holder.get().clockSequence) + throw new IllegalStateException("Cannot change clock sequence"); + holder.get().clockSequence = newClockSequence; + } + + // very unlikely case where it took less than 100ns between both + if (previousTimestamp == now) { + try { + Thread.sleep(0, 100); + } catch (InterruptedException e) { + // silent + } + now = computeNow(); + assert previousTimestamp != now; + } + holder.get().lastTimestamp = now; + return now; + } + + protected long computeNow() { + if (useClockForMeasurement) { + Duration duration = Duration.between(TimeUuidState.GREGORIAN_START, Instant.now(clock)); + return durationToUuidTimestamp(duration); + } else { + return startTimeStamp + nowVm(); + } + } + + private long nowVm() { + return System.nanoTime() / 100; + } + + protected long durationToUuidTimestamp(Duration duration) { + return (duration.getSeconds() * 10000000 + duration.getNano() / 100); + } + + /* + * STATE OPERATIONS + */ + + protected long newClockSequence() { + return secureRandom.nextInt(ConcurrentTimeUuidState.MAX_CLOCKSEQUENCE); + } + + /* + * ACCESSORS + */ + +// @Override +// public byte[] getNodeId() { +// byte[] arr = new byte[6]; +// System.arraycopy(holder.get().nodeId, 0, arr, 0, 6); +// return arr; +// } + + @Override + public long getClockSequence() { + return holder.get().clockSequence; + } +} diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/SimpleUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/SimpleUuidFactory.java new file mode 100644 index 000000000..71f8a134c --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/SimpleUuidFactory.java @@ -0,0 +1,106 @@ +package org.argeo.api.uuid; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.WARNING; + +import java.lang.System.Logger; +import java.security.DrbgParameters; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.time.Clock; +import java.util.UUID; + +/** + * Simple implementation of an {@link UuidFactory}, which can be used as a base + * class for more optimised implementations. + * + * @see https://datatracker.ietf.org/doc/html/rfc4122 + */ +public class SimpleUuidFactory extends AbstractAsyncUuidFactory { + private final static Logger logger = System.getLogger(SimpleUuidFactory.class.getName()); + public final static UuidFactory DEFAULT = new SimpleUuidFactory(null, -1, null); + +// private NodeId macAddressNodeId; +// private NodeId defaultNodeId; + private byte[] macAddressNodeId; + private byte[] defaultNodeId; + + public SimpleUuidFactory(byte[] nodeId, int offset, Clock clock) { + byte[] hardwareAddress = getHardwareAddress(); +// macAddressNodeId = hardwareAddress != null ? new NodeId(hardwareAddress, 0) : null; + macAddressNodeId = toNodeId(hardwareAddress, 0); + +// defaultNodeId = nodeId != null ? new NodeId(nodeId, offset) : macAddressNodeId; + defaultNodeId = nodeId != null ? toNodeId(nodeId, offset) : toNodeId(macAddressNodeId, 0); + if (defaultNodeId == null) + throw new IllegalStateException("No default node id specified"); + } + + @Override + protected SecureRandom newSecureRandom() { + SecureRandom secureRandom; + try { + secureRandom = SecureRandom.getInstance("DRBG", + DrbgParameters.instantiation(256, DrbgParameters.Capability.PR_AND_RESEED, "UUID".getBytes())); + } catch (NoSuchAlgorithmException e) { + try { + logger.log(DEBUG, "DRBG secure random not found, using strong"); + secureRandom = SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e1) { + logger.log(WARNING, "No strong secure random was found, using default"); + secureRandom = new SecureRandom(); + } + } + return secureRandom; + } + + /* + * TIME-BASED (version 1) + */ + + @Override + public UUID newTimeUUIDwithMacAddress() { + if (macAddressNodeId == null) + throw new UnsupportedOperationException("No MAC address is available"); + return newTimeUUID(timeUuidState.useTimestamp(), timeUuidState.getClockSequence(), macAddressNodeId, 0); + } + + @Override + public UUID newTimeUUID() { + return newTimeUUID(timeUuidState.useTimestamp(), timeUuidState.getClockSequence(), defaultNodeId, 0); + } + + /* + * RANDOM v4 + */ +// @Override +// public UUID randomUUID(Random random) { +// return newRandomUUID(random); +// } + +// @Override +// public UUID randomUUID() { +// return randomUUID(secureRandom); +// } +// +// static class NodeId extends ThreadLocal { +// private byte[] source; +// private int offset; +// +// public NodeId(byte[] source, int offset) { +// Objects.requireNonNull(source); +// this.source = source; +// this.offset = offset; +// if (offset < 0 || offset + 6 > source.length) +// throw new ArrayIndexOutOfBoundsException(offset); +// } +// +// @Override +// protected byte[] initialValue() { +// byte[] value = new byte[6]; +// System.arraycopy(source, offset, value, 0, 6); +// return value; +// } +// +// } +} diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuidState.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuidState.java new file mode 100644 index 000000000..4c9eec5a6 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuidState.java @@ -0,0 +1,47 @@ +package org.argeo.api.uuid; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +/** + * The state of a time based UUID generator, as described and discussed in + * section 4.2.1 of RFC4122. + * + * @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.2.1 + */ +public interface TimeUuidState { + + /** Start of the Gregorian time, used by time-based UUID (v1). */ + final static Instant GREGORIAN_START = ZonedDateTime.of(1582, 10, 15, 0, 0, 0, 0, ZoneOffset.UTC).toInstant(); + + long useTimestamp(); + + long getClockSequence(); + + static boolean isNoMacAddressNodeId(byte[] nodeId) { + return (nodeId[0] & 1) != 0; + } + + static class Holder { + long lastTimestamp; + long clockSequence; + + public long getLastTimestamp() { + return lastTimestamp; + } + + public void setLastTimestamp(long lastTimestamp) { + this.lastTimestamp = lastTimestamp; + } + + public long getClockSequence() { + return clockSequence; + } + + public void setClockSequence(long clockSequence) { + this.clockSequence = clockSequence; + } + + } +} diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidFactory.java new file mode 100644 index 000000000..a29706184 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidFactory.java @@ -0,0 +1,129 @@ +package org.argeo.api.uuid; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Supplier; + +/** + * A provider of RFC 4122 {@link UUID}s. Only the RFC 4122 variant (also known + * as Leach–Salz variant) is supported. The default, returned by the + * {@link Supplier#get()} method MUST be a v4 UUID (random). + * + * @see UUID + * @see https://datatracker.ietf.org/doc/html/rfc4122 + */ +public interface UuidFactory extends Supplier { + /* + * TIME-BASED (version 1) + */ + + UUID timeUUID(); + + UUID timeUUIDwithMacAddress(); + + /* + * NAME BASED (version 3 and 5) + */ + + UUID nameUUIDv5(UUID namespace, byte[] data); + + UUID nameUUIDv3(UUID namespace, byte[] data); + + default UUID nameUUIDv5(UUID namespace, String name) { + Objects.requireNonNull(name, "Name cannot be null"); + return nameUUIDv5(namespace, name.getBytes(UTF_8)); + } + + default UUID nameUUIDv3(UUID namespace, String name) { + Objects.requireNonNull(name, "Name cannot be null"); + return nameUUIDv3(namespace, name.getBytes(UTF_8)); + } + + /* + * RANDOM (version 4) + */ + /** A random UUID at least as good as {@link UUID#randomUUID()}. */ + UUID randomUUIDStrong(); + + /** + * An {@link UUID} generated based on {@link ThreadLocalRandom}. Implementations + * should always provide it synchronously. + */ + UUID randomUUIDWeak(); + + /** + * The default random {@link UUID} (v4) generator to use. This default + * implementation returns {@link #randomUUIDStrong()}. + */ + default UUID randomUUID() { + return randomUUIDStrong(); + } + + /** + * The default {@link UUID} to provide, either random (v4) or time based (v1). + * This default implementations returns {@link #randomUUID()}. + */ + @Override + default UUID get() { + return randomUUID(); + } + + /* + * STANDARD UUIDs + */ + + /** Nil UUID (00000000-0000-0000-0000-000000000000). */ + final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + /** + * Standard DNS namespace ID for type 3 or 5 UUID (as defined in Appendix C of + * RFC4122). + */ + final static UUID NS_DNS = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); + /** + * Standard URL namespace ID for type 3 or 5 UUID (as defined in Appendix C of + * RFC4122). + */ + final static UUID NS_URL = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + /** + * Standard OID namespace ID (typically an LDAP type) for type 3 or 5 UUID (as + * defined in Appendix C of RFC4122). + */ + final static UUID NS_OID = UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8"); + /** + * Standard X500 namespace ID (typically an LDAP DN) for type 3 or 5 UUID (as + * defined in Appendix C of RFC4122). + */ + final static UUID NS_X500 = UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"); + + /* + * UTILITIES + */ + + static boolean isRandom(UUID uuid) { + return uuid.version() == 4; + } + + static boolean isTimeBased(UUID uuid) { + return uuid.version() == 1; + } + + /** + * Whether this UUID is time based but was not generated from an IEEE 802 + * address, as per Section 4.5 of RFC4122. + * + * @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.5 + */ + static boolean isTimeBasedWithMacAddress(UUID uuid) { + if (uuid.version() == 1) { + return (uuid.node() & 1L) == 0; + } else + return false; + } + + static boolean isNameBased(UUID uuid) { + return uuid.version() == 3 || uuid.version() == 5; + } +} diff --git a/org.argeo.cms/pom.xml b/org.argeo.cms/pom.xml index 38112fc3b..4b06b0132 100644 --- a/org.argeo.cms/pom.xml +++ b/org.argeo.cms/pom.xml @@ -16,11 +16,6 @@ org.argeo.api.cms 2.3-SNAPSHOT - - org.argeo.commons - org.argeo.api.acr - 2.3-SNAPSHOT - org.argeo.commons org.argeo.util diff --git a/pom.xml b/pom.xml index c0edf6c4d..0a265cf64 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ org.argeo.init org.argeo.util + org.argeo.api.uuid org.argeo.api.acr org.argeo.api.cms