From: Mathieu Baudier Date: Tue, 25 Jan 2022 09:21:16 +0000 (+0100) Subject: Introduce typed UUIDs. X-Git-Tag: argeo-commons-2.3.5~57 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=e8e2c65f356b30b35e4d8a1de66691a789c183bb Introduce typed UUIDs. --- 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 index 000000000..a1c64a1a1 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/NameUuid.java @@ -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 false 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 index 000000000..ea71c4833 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/RandomUuid.java @@ -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 true 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 index 000000000..2f3a73fff --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/TimeUuid.java @@ -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 + * {@link UUID#timestamp()} == 0. + */ + 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 false 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 index 000000000..55a67abef --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/TypedUuid.java @@ -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 index 000000000..623de91f6 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/UnkownNameUuid.java @@ -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 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 index 000000000..253f0ad92 --- /dev/null +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/UuidHolder.java @@ -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, 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(); + } + +}