Introduce typed UUIDs.
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 25 Jan 2022 09:21:16 +0000 (10:21 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 25 Jan 2022 09:21:16 +0000 (10:21 +0100)
org.argeo.api.uuid/src/org/argeo/api/uuid/NameUuid.java [new file with mode: 0644]
org.argeo.api.uuid/src/org/argeo/api/uuid/RandomUuid.java [new file with mode: 0644]
org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuid.java [new file with mode: 0644]
org.argeo.api.uuid/src/org/argeo/api/uuid/TypedUuid.java [new file with mode: 0644]
org.argeo.api.uuid/src/org/argeo/api/uuid/UnkownNameUuid.java [new file with mode: 0644]
org.argeo.api.uuid/src/org/argeo/api/uuid/UuidHolder.java [new file with mode: 0644]

diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/NameUuid.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/NameUuid.java
new file mode 100644 (file)
index 0000000..a1c64a1
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.api.uuid;
+
+import java.nio.charset.Charset;
+import java.util.UUID;
+
+/** A name {@link UUID} whose values used for its construction are known. */
+public class NameUuid extends UnkownNameUuid {
+       private static final long serialVersionUID = APM.SERIAL;
+
+       protected final TypedUuid namespace;
+       protected final String name;
+       protected final Charset encoding;
+
+       protected NameUuid(UUID uuid, TypedUuid namespace, String name, Charset encoding) {
+               super(uuid);
+               assert namespace != null;
+               assert name != null;
+               assert encoding != null;
+               this.namespace = namespace;
+               this.name = name;
+               this.encoding = encoding;
+       }
+
+       public static long getSerialversionuid() {
+               return serialVersionUID;
+       }
+
+       /** The namespace used to build this UUID. */
+       public final TypedUuid getNamespace() {
+               return namespace;
+       }
+
+       /** The name of this UUID. */
+       public final String getName() {
+               return name;
+       }
+
+       /** The encoding used to create this UUID. */
+       public final Charset getEncoding() {
+               return encoding;
+       }
+
+       /** Always returns <code>false</code> since construction values are known. */
+       @Override
+       public final boolean isOpaque() {
+               return false;
+       }
+
+}
diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/RandomUuid.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/RandomUuid.java
new file mode 100644 (file)
index 0000000..ea71c48
--- /dev/null
@@ -0,0 +1,29 @@
+package org.argeo.api.uuid;
+
+import java.util.UUID;
+
+/** An opaque variant 2 random {@link UUID} (v4). */
+public final class RandomUuid extends TypedUuid {
+       private static final long serialVersionUID = APM.SERIAL;
+
+       public RandomUuid(UUID uuid) {
+               super(uuid);
+               if (uuid.version() != 4 && uuid.variant() != 2)
+                       throw new IllegalArgumentException("The provided UUID is not a time-based UUID.");
+       }
+
+       /**
+        * Always returns <code>true</code> since random UUIDs are by definition not
+        * opaque.
+        */
+       @Override
+       public final boolean isOpaque() {
+               return true;
+       }
+
+       /** Creates a new {@link RandomUuid} using {@link UUID#randomUUID()}. */
+       public static RandomUuid newRandomUuid() {
+               return new RandomUuid(UUID.randomUUID());
+       }
+
+}
diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuid.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuid.java
new file mode 100644 (file)
index 0000000..2f3a73f
--- /dev/null
@@ -0,0 +1,78 @@
+package org.argeo.api.uuid;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.UUID;
+
+/**
+ * A time based UUID, whose content can therefore be usefully interpreted as
+ * time and node identifier information.
+ */
+public class TimeUuid extends TypedUuid {
+       private static final long serialVersionUID = APM.SERIAL;
+       /**
+        * Start of the Gregorian time on October 15th 1582, equivalent to
+        * <code>{@link UUID#timestamp()} == 0</code>.
+        */
+       public final static Instant TIMESTAMP_ZERO = ZonedDateTime.of(1582, 10, 15, 0, 0, 0, 0, ZoneOffset.UTC).toInstant();
+
+       public TimeUuid(UUID uuid) {
+               super(uuid);
+               if (uuid.version() != 1 && uuid.variant() != 2)
+                       throw new IllegalArgumentException("The provided UUID is not a time based UUID.");
+       }
+
+       /** {@link UUID#timestamp()} as an {@link Instant}. */
+       public final Instant getInstant() {
+               long timestamp = uuid.timestamp();
+               return TIMESTAMP_ZERO.plus(timestampDifferenceToDuration(timestamp));
+       }
+
+       /** {@link UUID#node()} as an hex string. */
+       public final String getNodeId() {
+               return Long.toHexString(uuid.node());
+       }
+
+       /** {@link UUID#clockSequence()} as an hex string. */
+       public final String getClockSequence() {
+               return Long.toHexString(uuid.clockSequence());
+       }
+
+       /**
+        * Always returns <code>false</code> since time UUIDs are by definition not
+        * opaque.
+        */
+       @Override
+       public final boolean isOpaque() {
+               return false;
+       }
+
+       /*
+        * STATIC UTILITIES
+        */
+       /** Converts from duration in the time UUID timestamp format. */
+       public static Duration timestampDifferenceToDuration(long timestampDifference) {
+               long seconds = timestampDifference / 10000000;
+               long nano = (timestampDifference % 10000000) * 100;
+               return Duration.ofSeconds(seconds, nano);
+       }
+
+       /**
+        * A duration expressed in the time UUID timestamp format based on units of 100
+        * ns.
+        */
+       public static long durationToTimestamp(Duration duration) {
+               return (duration.getSeconds() * 10000000 + duration.getNano() / 100);
+       }
+
+       /**
+        * An instant expressed in the time UUID timestamp format based on units of 100
+        * ns since {@link #TIMESTAMP_ZERO}.
+        */
+       public static long instantToTimestamp(Instant instant) {
+               Duration duration = Duration.between(TimeUuid.TIMESTAMP_ZERO, instant);
+               return durationToTimestamp(duration);
+       }
+}
diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/TypedUuid.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/TypedUuid.java
new file mode 100644 (file)
index 0000000..55a67ab
--- /dev/null
@@ -0,0 +1,60 @@
+package org.argeo.api.uuid;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Base class for objects which are explicitly typed, based on the various UUID
+ * versions. Such a derivation hierarchy still represents the {@link UUID}
+ * itself, not the objects, data or concept that it identifies. Just like
+ * {@link UUID}s, {@link TypedUuid} should be used as identifier, not as base
+ * class for complex objects. It should rather be seen as a framework to build
+ * richer IDs, which are strictly compliant with the UUID specifications.
+ */
+public abstract class TypedUuid extends UuidHolder {
+       private static final long serialVersionUID = APM.SERIAL;
+
+       /** Default constructor. */
+       public TypedUuid(UUID uuid) {
+               super(uuid);
+       }
+
+       /**
+        * Whether this {@link UUID} has no meaning in itself (RFC4122 v3, v4 and v5,
+        * and Microsoft GUID). Only RFC4122 v1 and v2 can be interpreted.
+        */
+       public boolean isOpaque() {
+               if (uuid.variant() == 2) {// RFC4122
+                       return uuid.version() == 4 || uuid.version() == 5 || uuid.version() == 3;
+               } else if (uuid.variant() == 6) {// Microsoft
+                       return true;
+               } else {
+                       return true;
+               }
+       }
+
+       /**
+        * Constructs a {@link TypedUuid} of the most appropriate subtype, based on this
+        * {@link UUID}.
+        */
+       public static TypedUuid of(UUID uuid) {
+               Objects.requireNonNull(uuid, "UUID cannot be null");
+               if (uuid.variant() == 2) {// RFC 4122
+                       switch (uuid.version()) {
+                       case 1:
+                               return new TimeUuid(uuid);
+                       case 4:
+                               return new RandomUuid(uuid);
+                       case 3:
+                       case 5:
+                               return new UnkownNameUuid(uuid);
+                       default:
+                               throw new IllegalArgumentException("UUIDs with version " + uuid.version() + " are not supported.");
+                       }
+               } else if (uuid.variant() == 6) {// Microsoft
+                       throw new IllegalArgumentException("Microsoft UUIDs (aka. GUIDs) are not supported.");
+               } else {
+                       throw new IllegalArgumentException("UUIDs with variant " + uuid.variant() + " are not supported.");
+               }
+       }
+}
diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/UnkownNameUuid.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/UnkownNameUuid.java
new file mode 100644 (file)
index 0000000..623de91
--- /dev/null
@@ -0,0 +1,31 @@
+package org.argeo.api.uuid;
+
+import java.util.UUID;
+
+/** A name-based UUID (v3 or v5) whose construction values are not known. */
+public class UnkownNameUuid extends TypedUuid {
+       private static final long serialVersionUID = APM.SERIAL;
+
+       public UnkownNameUuid(UUID uuid) {
+               super(uuid);
+               if ((uuid.version() != 5 && uuid.version() != 3) || uuid.variant() != 2)
+                       throw new IllegalArgumentException("The provided UUID is not a name-based UUID.");
+       }
+
+       /**
+        * Always returns <code>true</true> since it is unknown from which values it was
+        * constructed..
+        */
+       @Override
+       public boolean isOpaque() {
+               return true;
+       }
+
+       /**
+        * Whether the hash of this name UUID was generated with SHA-1 (v5) or with MD5
+        * (v3).
+        */
+       public boolean isSha1() {
+               return uuid.version() == 5;
+       }
+}
diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidHolder.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidHolder.java
new file mode 100644 (file)
index 0000000..253f0ad
--- /dev/null
@@ -0,0 +1,71 @@
+package org.argeo.api.uuid;
+
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+/**
+ * An immutable wrapper for an {@link UUID}, which can be used as a base for a
+ * derivation hierarchy, while strongly enforcing semantic equality with the
+ * underlying {@link UUID}. It is therefore immutable, and all base methods are
+ * directly and trivially based on {@link UUID} methods; they do represent the
+ * same unique "thing" (be it an entity, a point in time, etc.), consistently
+ * with the fundamental concept of uuid.
+ */
+public class UuidHolder implements Supplier<UUID>, Serializable {
+       private static final long serialVersionUID = APM.SERIAL;
+
+       /**
+        * The wrapped {@link UUID}. Protected rather than private, since it is
+        * immutable and a {@link UUID} is itself immutable.
+        */
+       protected final UUID uuid;
+
+       /**
+        * Constructs a new {@link UuidHolder} based on this uuid.
+        * 
+        * @param uuid the UUID to wrap, cannot be null.
+        * @throws NullPointerException if the provided uuid is null.
+        */
+       protected UuidHolder(UUID uuid) {
+               Objects.requireNonNull(uuid, "UUID cannot be null");
+               this.uuid = uuid;
+       }
+
+       /** The wrapped {@link UUID}. */
+       public final UUID getUuid() {
+               return uuid;
+       }
+
+       /** The wrapped {@link UUID}. */
+       @Override
+       public final UUID get() {
+               return getUuid();
+       }
+
+       /** Calls {@link UUID#hashCode()} on the wrapped {@link UUID}. */
+       @Override
+       public final int hashCode() {
+               return uuid.hashCode();
+       }
+
+       /**
+        * Equals only with non-null {@link UuidHolder} if and only if their wrapped
+        * uuid are equals by calling {@link UUID#equals(Object)}.
+        */
+       @Override
+       public final boolean equals(Object obj) {
+               if (obj == null || !(obj instanceof UuidHolder))
+                       return false;
+               UuidHolder typedUuid = (UuidHolder) obj;
+               return uuid.equals(typedUuid.uuid);
+       }
+
+       /** Calls {@link UUID#toString()} on the wrapped {@link UUID}. */
+       @Override
+       public final String toString() {
+               return uuid.toString();
+       }
+
+}