diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/UUIDFactory.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/UUIDFactory.java index ef8fb9ca..d0e5b3f8 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/UUIDFactory.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/util/UUIDFactory.java @@ -35,16 +35,25 @@ import java.net.SocketException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.UUID; +import java.util.*; /** * A factory for creating UUIDs not directly supported by {@link java.util.UUID}. + *

* This class can create - * version 1 (time based, using either MAC aka IEEE 802 address or Random "node" value) - * and version 5 (SHA1 hash based) UUIDs. + * version 1 time based, using either IEEE 802 (mac) address or random "node" value + * and version 5 SHA1 hash based UUIDs. + *

+ *

+ * The node value for version 1 UUIDs will, by default, reflect the IEEE 802 (mac) address of one of + * the network interfaces of the local computer. + * This node value can be manually overridden by setting + * the system property {@code "com.twelvemonkeys.util.UUID.node"} to a valid IEEE 802 address, on the form + * {@code 12:34:56:78:9a:bc} or {@code 12-34-45-78-9a-bc}. + *

+ *

+ * The node value for the random "node" based version 1 UUIDs will be stable for the lifetime of the VM. + *

* * @author Harald Kuhr * @author last modified by $Author: haraldk$ @@ -54,14 +63,14 @@ import java.util.UUID; * @see Wikipedia * @see java.util.UUID */ -public class UUIDFactory { +public final class UUIDFactory { private static final String NODE_PROPERTY = "com.twelvemonkeys.util.UUID.node"; /** * Nil UUID: {@code "00000000-0000-0000-0000-000000000000"}. * * The nil UUID is special form of UUID that is specified to have all - * 128 bits set to zero. + * 128 bits set to zero. Not particularly useful. * * @see RFC 4122 */ @@ -69,8 +78,9 @@ public class UUIDFactory { private static final SecureRandom SECURE_RANDOM = new SecureRandom(); - // Assumes MAC address is constant, which it may not be on clients moving from ethernet to wifi etc... - // TODO: Update at some interval + private static final Comparator COMPARATOR = new UUIDComparator(); + + // Assumes MAC address is constant, which it may not be if a network card is replaced static final long MAC_ADDRESS_NODE = getMacAddressNode(); static final long SECURE_RANDOM_NODE = getSecureRandomNode(); @@ -134,7 +144,7 @@ public class UUIDFactory { String nodesString = nodesStrings[i]; try { - String[] nodes = nodesString.split("(?<=(^|\\W)[0-9a-fA-F]{2})\\W(?=[0-9a-fA-F]{2}($|\\W))", 6); + String[] nodes = nodesString.split("(?<=(^|\\W)[0-9a-fA-F]{2})\\W(?=[0-9a-fA-F]{2}(\\W|$))", 6); long nodeAddress = 0; @@ -159,6 +169,8 @@ public class UUIDFactory { return addressNodes; } + private UUIDFactory() {} + // See also http://tools.ietf.org/html/rfc4122#appendix-B // See http://tools.ietf.org/html/rfc4122: 4.3. Algorithm for Creating a Name-Based UUID // TODO: Naming (of the method) @@ -213,6 +225,7 @@ public class UUIDFactory { // See http://tools.ietf.org/html/rfc4122#appendix-B // TODO: Naming (of the method) // TODO: Document that these can never clash with node based v1 UUIDs due to unicast/multicast bit + // However, no uniqueness between multiple mavhines/vms/nodes can be guaranteed. static UUID timeRandomBasedV1() { return new UUID(createTimeAndVersion(), createClockSeqAndNode(SECURE_RANDOM_NODE)); } @@ -241,8 +254,52 @@ public class UUIDFactory { return time; } - // TODO: Implement compare as spec'ed, see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7025832 - // - Probably create a comparator + /** + * Returns a comparator that compares UUIDs as 128 bit unsigned entities, as mentioned in RFC 4122. + * This is different than {@link UUID#compareTo(Object)} that compares the UUIDs as signed entities. + * + * @return a comparator that compares UUIDs as 128 bit unsigned entities. + * + * @see java.lang.UUID compareTo() does not do an unsigned compare + * @see + */ + public static Comparator comparator() { + return COMPARATOR; + } + + static final class UUIDComparator implements Comparator { + public int compare(UUID left, UUID right) { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two *UNSIGNED* numbers + if (left.getMostSignificantBits() >>> 32 < right.getMostSignificantBits() >>> 32) { + return -1; + } + else if (left.getMostSignificantBits() >>> 32 > right.getMostSignificantBits() >>> 32) { + return 1; + } + else if ((left.getMostSignificantBits() & 0xffffffffl) < (right.getMostSignificantBits() & 0xffffffffl)) { + return -1; + } + else if ((left.getMostSignificantBits() & 0xffffffffl) > (right.getMostSignificantBits() & 0xffffffffl)) { + return 1; + } + else if (left.getLeastSignificantBits() >>> 32 < right.getLeastSignificantBits() >>> 32) { + return -1; + } + else if (left.getLeastSignificantBits() >>> 32 > right.getLeastSignificantBits() >>> 32) { + return 1; + } + else if ((left.getLeastSignificantBits() & 0xffffffffl) < (right.getLeastSignificantBits() & 0xffffffffl)) { + return -1; + } + else if ((left.getLeastSignificantBits() & 0xffffffffl) > (right.getLeastSignificantBits() & 0xffffffffl)) { + return 1; + } + + return 0; + } + } + /** * A high-resolution timer for use in creating version 1 UUIDs. diff --git a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/UUIDFactoryTest.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/UUIDFactoryTest.java index cecce632..5f19c7ad 100644 --- a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/UUIDFactoryTest.java +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/util/UUIDFactoryTest.java @@ -1,10 +1,12 @@ package com.twelvemonkeys.util; +import org.junit.Ignore; import org.junit.Test; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.util.Arrays; +import java.util.Comparator; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -13,119 +15,186 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.*; public class UUIDFactoryTest { + private static final String EXAMPLE_COM_UUID = "http://www.example.com/uuid/"; // Nil UUID @Test public void testNilUUID() { - UUID nil = UUIDFactory.NIL; UUID a = UUID.fromString("00000000-0000-0000-0000-000000000000"); UUID b = new UUID(0l, 0l); - assertEquals(nil, b); - assertEquals(nil, a); - assertEquals(a, b); + assertEquals(UUIDFactory.NIL, b); + assertEquals(UUIDFactory.NIL, a); + assertEquals(a, b); // Sanity - assertEquals(0, nil.variant()); - assertEquals(0, nil.version()); + assertEquals(0, UUIDFactory.NIL.variant()); + assertEquals(0, UUIDFactory.NIL.version()); } // Version 3 UUIDs (for comparison with v5) @Test - public void testVersion3NameBasedMD5() throws UnsupportedEncodingException { - String name = "http://www.example.com/uuid/"; - UUID a = UUID.nameUUIDFromBytes(name.getBytes("UTF-8")); - assertEquals(3, a.version()); + public void testVersion3NameBasedMD5VariantVersion() throws UnsupportedEncodingException { + UUID a = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); assertEquals(2, a.variant()); + assertEquals(3, a.version()); + } - UUID b = UUID.nameUUIDFromBytes(name.getBytes("UTF-8")); + @Test + public void testVersion3NameBasedMD5Equals() throws UnsupportedEncodingException { + UUID a = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + UUID b = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); assertEquals(a, b); + } - assertFalse(a.equals(UUIDFactory.nameUUIDFromBytesSHA1(name.getBytes("UTF-8")))); + @Test + public void testVersion3NameBasedMD5NotEqualSHA1() throws UnsupportedEncodingException { + UUID a = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertFalse(a.equals(UUIDFactory.nameUUIDFromBytesSHA1(EXAMPLE_COM_UUID.getBytes("UTF-8")))); + } + @Test + public void testVersion3NameBasedMD5FromStringRep() throws UnsupportedEncodingException { + UUID a = UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")); assertEquals(a, UUID.fromString(a.toString())); } // Version 5 UUIDs @Test - public void testVersion5NameBasedSHA1() throws UnsupportedEncodingException { - String name = "http://www.example.com/uuid/"; - UUID a = UUIDFactory.nameUUIDFromBytesSHA1(name.getBytes("UTF-8")); - assertEquals(5, a.version()); + public void testVersion5NameBasedSHA1VariantVersion() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDFromBytesSHA1(EXAMPLE_COM_UUID.getBytes("UTF-8")); assertEquals(2, a.variant()); + assertEquals(5, a.version()); + } - UUID b = UUIDFactory.nameUUIDFromBytesSHA1(name.getBytes("UTF-8")); + @Test + public void testVersion5NameBasedSHA1Equals() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDFromBytesSHA1(EXAMPLE_COM_UUID.getBytes("UTF-8")); + UUID b = UUIDFactory.nameUUIDFromBytesSHA1(EXAMPLE_COM_UUID.getBytes("UTF-8")); assertEquals(a, b); + } - assertFalse(a.equals(UUID.nameUUIDFromBytes(name.getBytes("UTF-8")))); + @Test + public void testVersion5NameBasedSHA1NotEqualMD5() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDFromBytesSHA1(EXAMPLE_COM_UUID.getBytes("UTF-8")); + assertFalse(a.equals(UUID.nameUUIDFromBytes(EXAMPLE_COM_UUID.getBytes("UTF-8")))); + } + + @Test + public void testVersion5NameBasedSHA1FromStringRep() throws UnsupportedEncodingException { + UUID a = UUIDFactory.nameUUIDFromBytesSHA1(EXAMPLE_COM_UUID.getBytes("UTF-8")); assertEquals(a, UUID.fromString(a.toString())); } // Version 1 UUIDs @Test - public void testVersion1NodeBased() { + public void testVersion1NodeBasedVariantVersion() { UUID uuid = UUIDFactory.timeNodeBasedV1(); - System.err.println("uuid: " + uuid); - - assertEquals(1, uuid.version()); assertEquals(2, uuid.variant()); + assertEquals(1, uuid.version()); + } + @Test + public void testVersion1NodeBasedMacAddress() { + UUID uuid = UUIDFactory.timeNodeBasedV1(); assertEquals(UUIDFactory.MAC_ADDRESS_NODE, uuid.node()); - // TODO: Test that this is actually a Mac address from the local computer, or specified through system property + // TODO: Test that this is actually a Mac address from the local computer, or specified through system property? + } + @Test + public void testVersion1NodeBasedFromStringRep() { + UUID uuid = UUIDFactory.timeNodeBasedV1(); assertEquals(uuid, UUID.fromString(uuid.toString())); + } + @Test + public void testVersion1NodeBasedClockSeq() { + UUID uuid = UUIDFactory.timeNodeBasedV1(); assertEquals(UUIDFactory.Clock.getClockSequence(), uuid.clockSequence()); + // Test time fields (within reasonable limits +/- 100 ms or so?) // TODO: Compare with system clock for sloppier resolution assertEquals(UUIDFactory.Clock.currentTimeHundredNanos(), uuid.timestamp(), 1e6); + } - assertEquals(0, (uuid.node() >> 40) & 1); + @Test + public void testVersion1NodeBasedTimestamp() { + UUID uuid = UUIDFactory.timeNodeBasedV1(); + // Test time fields (within reasonable limits +/- 100 ms or so?) + assertEquals(UUIDFactory.Clock.currentTimeHundredNanos(), uuid.timestamp(), 1e6); + } + + @Test + public void testVersion1NodeBasedUniMulticastBitUnset() { + // Do it a couple of times, to avoid accidentally have correct bit + for (int i = 0; i < 100; i++) { + UUID uuid = UUIDFactory.timeNodeBasedV1(); + assertEquals(0, (uuid.node() >> 40) & 1); + } } @Test public void testVersion1NodeBasedUnique() { - UUID a = UUIDFactory.timeNodeBasedV1(); - UUID b = UUIDFactory.timeNodeBasedV1(); - - System.err.println("a: " + a); - System.err.println("b: " + b); - - assertFalse(a.equals(b)); + for (int i = 0; i < 100; i++) { + UUID a = UUIDFactory.timeNodeBasedV1(); + UUID b = UUIDFactory.timeNodeBasedV1(); + assertFalse(a.equals(b)); + } } @Test - public void testVersion1SecureRandom() { + public void testVersion1SecureRandomVariantVersion() { UUID uuid = UUIDFactory.timeRandomBasedV1(); - System.err.println("uuid: " + uuid); - assertEquals(1, uuid.version()); assertEquals(2, uuid.variant()); + assertEquals(1, uuid.version()); + } + @Test + public void testVersion1SecureRandomNode() { + UUID uuid = UUIDFactory.timeRandomBasedV1(); assertEquals(UUIDFactory.SECURE_RANDOM_NODE, uuid.node()); + } + @Test + public void testVersion1SecureRandomFromStringRep() { + UUID uuid = UUIDFactory.timeRandomBasedV1(); assertEquals(uuid, UUID.fromString(uuid.toString())); + } + @Test + public void testVersion1SecureRandomClockSeq() { + UUID uuid = UUIDFactory.timeRandomBasedV1(); assertEquals(UUIDFactory.Clock.getClockSequence(), uuid.clockSequence()); + } - // TODO: Test time fields (within reasonable limits +/- 100 ms or so?) + @Test + public void testVersion1SecureRandomTimestamp() { + UUID uuid = UUIDFactory.timeRandomBasedV1(); + + // Test time fields (within reasonable limits +/- 100 ms or so?) assertEquals(UUIDFactory.Clock.currentTimeHundredNanos(), uuid.timestamp(), 1e6); + } - assertEquals(1, (uuid.node() >> 40) & 1); + @Test + public void testVersion1SecureRandomUniMulticastBit() { + // Do it a couple of times, to avoid accidentally have correct bit + for (int i = 0; i < 100; i++) { + UUID uuid = UUIDFactory.timeRandomBasedV1(); + assertEquals(1, (uuid.node() >> 40) & 1); + } } @Test public void testVersion1SecureRandomUnique() { - UUID a = UUIDFactory.timeRandomBasedV1(); - UUID b = UUIDFactory.timeRandomBasedV1(); - - System.err.println("a: " + a); - System.err.println("b: " + b); - - assertFalse(a.equals(b)); + for (int i = 0; i < 100; i++) { + UUID a = UUIDFactory.timeRandomBasedV1(); + UUID b = UUIDFactory.timeRandomBasedV1(); + assertFalse(a.equals(b)); + } } // Clock tests @@ -242,10 +311,66 @@ public class UUIDFactoryTest { UUIDFactory.parseMacAddressNodes("00:11:22:33:44:55:99"); } - // Various testing + // Comparator test + @Test + public void testComparator() { + UUID min = new UUID(0, 0); + // Long.MAX_VALUE and MIN_VALUE are really adjacent values when comparing unsigned... + UUID midLow = new UUID(Long.MAX_VALUE, Long.MAX_VALUE); + UUID midHigh = new UUID(Long.MIN_VALUE, Long.MIN_VALUE); + UUID max = new UUID(-1l, -1l); + + Comparator comparator = UUIDFactory.comparator(); + + assertEquals(0, comparator.compare(min, min)); + assertEquals(-1, comparator.compare(min, midLow)); + assertEquals(-1, comparator.compare(min, midHigh)); + assertEquals(-1, comparator.compare(min, max)); + + assertEquals(1, comparator.compare(midLow, min)); + assertEquals(0, comparator.compare(midLow, midLow)); + assertEquals(-1, comparator.compare(midLow, midHigh)); + assertEquals(-1, comparator.compare(midLow, max)); + + assertEquals(1, comparator.compare(midHigh, min)); + assertEquals(1, comparator.compare(midHigh, midLow)); + assertEquals(0, comparator.compare(midHigh, midHigh)); + assertEquals(-1, comparator.compare(midHigh, max)); + + assertEquals(1, comparator.compare(max, min)); + assertEquals(1, comparator.compare(max, midLow)); + assertEquals(1, comparator.compare(max, midHigh)); + assertEquals(0, comparator.compare(max, max)); + } + + @Test + public void testComparatorRandom() { + final Comparator comparator = UUIDFactory.comparator(); + + for (int i = 0; i < 1000; i++) { + UUID one = UUID.randomUUID(); + UUID two = UUID.randomUUID(); + + if (one.getMostSignificantBits() < 0 && two.getMostSignificantBits() >= 0 + || one.getMostSignificantBits() >= 0 && two.getMostSignificantBits() < 0 + || one.getLeastSignificantBits() < 0 && two.getLeastSignificantBits() >= 0 + || one.getLeastSignificantBits() >= 0 && two.getLeastSignificantBits() < 0) { + // These will differ due to the differing signs + assertEquals(-one.compareTo(two), comparator.compare(one, two)); + } + else { + assertEquals(one.compareTo(two), comparator.compare(one, two)); + } + } + } + + // Various testing + + @Ignore("Development testing only") @Test public void testOracleSYS_GUID() { + // TODO: Consider including this as a "fromCompactString" or similar... String str = "AEB87F28E222D08AE043803BD559D08A"; BigInteger bigInteger = new BigInteger(str, 16); // ALT: Create byte array of every 2 chars. long msb = bigInteger.shiftRight(64).longValue();