--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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());
+ }
+
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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.");
+ }
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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();
+ }
+
+}