Added UUID factory for creating various Version 1 and Version 5 UUIDs.

This commit is contained in:
Harald Kuhr 2012-03-01 15:22:07 +01:00
parent 19ed19633c
commit 9cb21dbfc9
2 changed files with 608 additions and 0 deletions

View File

@ -0,0 +1,350 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.util;
import com.twelvemonkeys.lang.StringUtil;
import java.net.NetworkInterface;
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;
/**
* 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.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: UUIDFactory.java,v 1.0 27.02.12 09:45 haraldk Exp$
*
* @see <a href="http://tools.ietf.org/html/rfc4122">RFC 4122</a>
* @see <a href="http://en.wikipedia.org/wiki/Universally_unique_identifier">Wikipedia</a>
* @see java.util.UUID
*/
public 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.
*
* @see <a href="http://tools.ietf.org/html/rfc4122#section-4.1.7">RFC 4122</a>
*/
public static final UUID NIL = new UUID(0l, 0l);
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
static final long MAC_ADDRESS_NODE = getMacAddressNode();
static final long SECURE_RANDOM_NODE = getSecureRandomNode();
private static long getSecureRandomNode() {
/*
Obtain a 47-bit cryptographic quality random
number and use it as the low 47 bits of the node ID, with the least
significant bit of the first octet of the node ID set to one. This
bit is the unicast/multicast bit, which will never be set in IEEE 802
addresses obtained from network cards. Hence, there can never be a
conflict between UUIDs generated by machines with and without network
cards. (Recall that the IEEE 802 spec talks about transmission
order)
*/
/*
In addition, items such as the computer's name and the name of the
operating system, while not strictly speaking random, will help
differentiate the results from those obtained by other systems.
The exact algorithm to generate a node ID using these data is system
specific, because both the data available and the functions to obtain
them are often very system specific. A generic approach, however, is
to accumulate as many sources as possible into a buffer, use a
message digest such as MD5 [4] or SHA-1 [8], take an arbitrary 6
bytes from the hash value, and set the multicast bit as described
above.
*/
// TODO: Verify that nextLong is still cryptographically strong after the bit masking
// TODO: Consider using the hashing approach above
return 1l << 40 | SECURE_RANDOM.nextLong() & 0xffffffffffffl;
}
private static long getMacAddressNode() {
long[] addressNodes;
String nodeProperty = System.getProperty(NODE_PROPERTY);
// Read mac address/node from system property, to allow user-specified node addresses.
if (!StringUtil.isEmpty(nodeProperty)) {
addressNodes = parseMacAddressNodes(nodeProperty);
}
else {
addressNodes = MacAddressFinder.getMacAddressNodes();
}
// TODO: The UUID spec allows us to use multiple nodes, when available, to create more UUIDs per time unit...
// For example in a round robin fashion?
return addressNodes != null && addressNodes.length > 0 ? addressNodes[0] : -1;
}
static long[] parseMacAddressNodes(final String nodeProperty) {
// Parse comma-separated list mac addresses on format 00:11:22:33:44:55 / 00-11-22-33-44-55
String[] nodesStrings = nodeProperty.trim().split(",\\W*");
long[] addressNodes = new long[nodesStrings.length];
for (int i = 0, nodesStringsLength = nodesStrings.length; i < nodesStringsLength; i++) {
String nodesString = nodesStrings[i];
try {
String[] nodes = nodesString.split("(?<=(^|\\W)[0-9a-fA-F]{2})\\W(?=[0-9a-fA-F]{2}($|\\W))", 6);
long nodeAddress = 0;
// Network byte order
nodeAddress |= (long) (Integer.parseInt(nodes[0], 16) & 0xff) << 40;
nodeAddress |= (long) (Integer.parseInt(nodes[1], 16) & 0xff) << 32;
nodeAddress |= (long) (Integer.parseInt(nodes[2], 16) & 0xff) << 24;
nodeAddress |= (long) (Integer.parseInt(nodes[3], 16) & 0xff) << 16;
nodeAddress |= (long) (Integer.parseInt(nodes[4], 16) & 0xff) << 8;
nodeAddress |= (long) (Integer.parseInt(nodes[5], 16) & 0xff);
addressNodes[i] = nodeAddress;
}
catch (RuntimeException e) {
// May be NumberformatException from parseInt or ArrayIndexOutOfBounds from nodes array
NumberFormatException formatException = new NumberFormatException(String.format("Bad IEEE 802 node address: '%s' (from system property %s)", nodesString, NODE_PROPERTY));
formatException.initCause(e);
throw formatException;
}
}
return addressNodes;
}
// 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)
// TODO: Read up on creating these UUIDs in RFC, mentions something about UUID for namespace as input..?
static UUID nameUUIDFromBytesSHA1(byte[] name) {
// Based on code from OpenJDK UUID#nameUUIDFromBytes + private byte[] constructor
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA1");
}
catch (NoSuchAlgorithmException nsae) {
throw new InternalError("SHA1 not supported");
}
byte[] sha1Bytes = md.digest(name);
sha1Bytes[6] &= 0x0f; /* clear version */
sha1Bytes[6] |= 0x50; /* set to version 5 */
sha1Bytes[8] &= 0x3f; /* clear variant */
sha1Bytes[8] |= 0x80; /* set to IETF variant */
long msb = 0;
long lsb = 0;
// NOTE: According to RFC 4122, only first 16 bytes are used, meaning
// bytes 17-20 in the 160 bit SHA1 hash are simply discarded in this case...
for (int i=0; i<8; i++) {
msb = (msb << 8) | (sha1Bytes[i] & 0xff);
}
for (int i=8; i<16; i++) {
lsb = (lsb << 8) | (sha1Bytes[i] & 0xff);
}
return new UUID(msb, lsb);
}
// Creatse version 1 node based UUIDs as specified in rfc422
// See http://tools.ietf.org/html/rfc4122#appendix-B
// See http://en.wikipedia.org/wiki/MAC_address
// TODO: Naming (of the method)
static UUID timeNodeBasedV1() {
if (MAC_ADDRESS_NODE == -1) {
// TODO: OR fall back to Random??
throw new IllegalStateException("Could not determine IEEE 802 (mac) address for node");
}
return new UUID(createTimeAndVersion(), createClockSeqAndNode(MAC_ADDRESS_NODE));
}
// Creates version 1 "node" based UUIDs, using 47 bit secure random number + lsb of first octet
// (unicast/multicast bit) set to 1 as described in rfc422: 4.5. Node IDs that Do Not Identify the Host
// 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
static UUID timeRandomBasedV1() {
return new UUID(createTimeAndVersion(), createClockSeqAndNode(SECURE_RANDOM_NODE));
}
// TODO: Version 2 UUID?
/*
Version 2 UUIDs are similar to Version 1 UUIDs, with the upper byte of the clock sequence replaced by the
identifier for a "local domain" (typically either the "POSIX UID domain" or the "POSIX GID domain")
and the first 4 bytes of the timestamp replaced by the user's POSIX UID or GID (with the "local domain"
identifier indicating which it is).[2][3]
*/
private static long createClockSeqAndNode(final long node) {
// Variant (2) + Clock seq high and low + node
return 0x8000000000000000l | (Clock.getClockSequence() << 48) | node & 0xffffffffffffl;
}
private static long createTimeAndVersion() {
long clockTime = Clock.currentTimeHundredNanos();
long time = clockTime << 32; // Time low
time |= (clockTime & 0xFFFF00000000L) >> 16; // Time mid
time |= ((clockTime >> 48) & 0x0FFF); // Time high
time |= 0x1000; // Version (1)
return time;
}
// TODO: Implement compare as spec'ed, see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7025832
// - Probably create a comparator
/**
* A high-resolution timer for use in creating version 1 UUIDs.
*/
static final class Clock {
// Java: 0:00, Jan. 1st, 1970 vs UUID: 0:00, Oct 15th, 1582
private static final long JAVA_EPOCH_OFFSET = 122192928000000000L;
private static int clockSeq = SECURE_RANDOM.nextInt();
private static long initialNanos;
private static long initialTime;
private static long lastMeasuredTime;
private static long lastTime;
static {
initClock();
}
private static void initClock() {
long millis = System.currentTimeMillis();
long nanos = System.nanoTime();
initialTime = JAVA_EPOCH_OFFSET + millis * 10000 + (nanos / 100) % 10000;
initialNanos = nanos;
}
public static synchronized long currentTimeHundredNanos() {
// Measure delta since init and compute accurate time
long time;
while ((time = initialTime + (System.nanoTime() - initialNanos) / 100) < lastMeasuredTime) {
// Reset clock seq (should happen VERY rarely)
initClock();
clockSeq++;
}
lastMeasuredTime = time;
if (time <= lastTime) {
// This typically means the clock isn't accurate enough, use auto-incremented time.
// It is possible that more timestamps than available are requested for
// each time unit in the system clock, but that is extremely unlikely.
// TODO: RFC 4122 says we should wait in the case of too many requests...
time = ++lastTime;
}
else {
lastTime = time;
}
return time;
}
public static synchronized long getClockSequence() {
return clockSeq & 0x3fff;
}
}
/**
* Static inner class for 1.5 compatibility.
*/
static final class MacAddressFinder {
public static long[] getMacAddressNodes() {
List<Long> nodeAddresses = new ArrayList<Long>();
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
if (interfaces != null) {
while (interfaces.hasMoreElements()) {
NetworkInterface nic = interfaces.nextElement();
if (!nic.isVirtual()) {
long nodeAddress = 0;
byte[] hardware = nic.getHardwareAddress(); // 1.6 method
if (hardware != null && hardware.length == 6 && hardware[1] != (byte) 0xff) {
// Network byte order
nodeAddress |= (long) (hardware[0] & 0xff) << 40;
nodeAddress |= (long) (hardware[1] & 0xff) << 32;
nodeAddress |= (long) (hardware[2] & 0xff) << 24;
nodeAddress |= (long) (hardware[3] & 0xff) << 16;
nodeAddress |= (long) (hardware[4] & 0xff) << 8;
nodeAddress |= (long) (hardware[5] & 0xff);
nodeAddresses.add(nodeAddress);
}
}
}
}
}
catch (SocketException ex) {
return null;
}
long[] unwrapped = new long[nodeAddresses.size()];
for (int i = 0, nodeAddressesSize = nodeAddresses.size(); i < nodeAddressesSize; i++) {
unwrapped[i] = nodeAddresses.get(i);
}
return unwrapped;
}
}
}

View File

@ -0,0 +1,258 @@
package com.twelvemonkeys.util;
import org.junit.Test;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
public class UUIDFactoryTest {
// 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(0, nil.variant());
assertEquals(0, 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());
assertEquals(2, a.variant());
UUID b = UUID.nameUUIDFromBytes(name.getBytes("UTF-8"));
assertEquals(a, b);
assertFalse(a.equals(UUIDFactory.nameUUIDFromBytesSHA1(name.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());
assertEquals(2, a.variant());
UUID b = UUIDFactory.nameUUIDFromBytesSHA1(name.getBytes("UTF-8"));
assertEquals(a, b);
assertFalse(a.equals(UUID.nameUUIDFromBytes(name.getBytes("UTF-8"))));
assertEquals(a, UUID.fromString(a.toString()));
}
// Version 1 UUIDs
@Test
public void testVersion1NodeBased() {
UUID uuid = UUIDFactory.timeNodeBasedV1();
System.err.println("uuid: " + uuid);
assertEquals(1, uuid.version());
assertEquals(2, uuid.variant());
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
assertEquals(uuid, UUID.fromString(uuid.toString()));
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 testVersion1NodeBasedUnique() {
UUID a = UUIDFactory.timeNodeBasedV1();
UUID b = UUIDFactory.timeNodeBasedV1();
System.err.println("a: " + a);
System.err.println("b: " + b);
assertFalse(a.equals(b));
}
@Test
public void testVersion1SecureRandom() {
UUID uuid = UUIDFactory.timeRandomBasedV1();
System.err.println("uuid: " + uuid);
assertEquals(1, uuid.version());
assertEquals(2, uuid.variant());
assertEquals(UUIDFactory.SECURE_RANDOM_NODE, uuid.node());
assertEquals(uuid, UUID.fromString(uuid.toString()));
assertEquals(UUIDFactory.Clock.getClockSequence(), uuid.clockSequence());
// TODO: 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 testVersion1SecureRandomUnique() {
UUID a = UUIDFactory.timeRandomBasedV1();
UUID b = UUIDFactory.timeRandomBasedV1();
System.err.println("a: " + a);
System.err.println("b: " + b);
assertFalse(a.equals(b));
}
// Clock tests
@Test(timeout = 10000l)
public void testClock() throws InterruptedException {
final long[] times = new long[100000];
ExecutorService service = Executors.newFixedThreadPool(20);
for (int i = 0; i < times.length; i++) {
final int index = i;
service.submit(new Runnable() {
public void run() {
times[index] = UUIDFactory.Clock.currentTimeHundredNanos();
}
});
}
service.shutdown();
assertTrue("Execution timed out", service.awaitTermination(10, TimeUnit.SECONDS));
Arrays.sort(times);
for (int i = 0, timesLength = times.length; i < timesLength; i++) {
if (i == 0) {
continue;
}
assertFalse(String.format("times[%d] == times[%d]: 0x%016x", i - 1, i, times[i]), times[i - 1] == times[i]);
}
}
@Test(timeout = 10000l)
public void testClockSkew() throws InterruptedException {
long clockSequence = UUIDFactory.Clock.getClockSequence();
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100000; i++) {
service.submit(new Runnable() {
public void run() {
UUIDFactory.Clock.currentTimeHundredNanos();
}
});
}
service.shutdown();
assertTrue("Execution timed out", service.awaitTermination(10, TimeUnit.SECONDS));
assertEquals(clockSequence, UUIDFactory.Clock.getClockSequence(), 1); // Verify that clock skew doesn't happen "often"
}
// Tests for node address system property
@Test
public void testParseNodeAddressesSingle() {
long[] nodes = UUIDFactory.parseMacAddressNodes("00:11:22:33:44:55");
assertEquals(1, nodes.length);
assertEquals(0x001122334455l, nodes[0]);
}
@Test
public void testParseNodeAddressesSingleWhitespace() {
long[] nodes = UUIDFactory.parseMacAddressNodes(" 00:11:22:33:44:55\r\n");
assertEquals(1, nodes.length);
assertEquals(0x001122334455l, nodes[0]);
}
@Test
public void testParseNodeAddressesMulti() {
long[] nodes = UUIDFactory.parseMacAddressNodes("00:11:22:33:44:55, aa:bb:cc:dd:ee:ff, \n\t 0a-1b-2c-3d-4e-5f,");
assertEquals(3, nodes.length);
assertEquals(0x001122334455l, nodes[0]);
assertEquals(0xaabbccddeeffl, nodes[1]);
assertEquals(0x0a1b2c3d4e5fl, nodes[2]);
}
@Test(expected = NullPointerException.class)
public void testParseNodeAddressesNull() {
UUIDFactory.parseMacAddressNodes(null);
}
@Test(expected = NumberFormatException.class)
public void testParseNodeAddressesEmpty() {
UUIDFactory.parseMacAddressNodes("");
}
@Test(expected = NumberFormatException.class)
public void testParseNodeAddressesNonAddress() {
UUIDFactory.parseMacAddressNodes("127.0.0.1");
}
@Test(expected = NumberFormatException.class)
public void testParseNodeAddressesBadAddress() {
UUIDFactory.parseMacAddressNodes("00a:11:22:33:44:55");
}
@Test(expected = NumberFormatException.class)
public void testParseNodeAddressesBadAddress4() {
long[] longs = UUIDFactory.parseMacAddressNodes("00:11:22:33:44:550");
System.err.println("Long: " + Long.toHexString(longs[0]));
}
@Test(expected = NumberFormatException.class)
public void testParseNodeAddressesBadAddress2() {
UUIDFactory.parseMacAddressNodes("0x:11:22:33:44:55");
}
@Test(expected = NumberFormatException.class)
public void testParseNodeAddressesBadAddress3() {
UUIDFactory.parseMacAddressNodes("00:11:22:33:44:55:99");
}
// Various testing
@Test
public void testOracleSYS_GUID() {
String str = "AEB87F28E222D08AE043803BD559D08A";
BigInteger bigInteger = new BigInteger(str, 16); // ALT: Create byte array of every 2 chars.
long msb = bigInteger.shiftRight(64).longValue();
long lsb = bigInteger.longValue();
UUID uuid = new UUID(msb, lsb);
System.err.println("uuid: " + uuid);
System.err.println("uuid.variant(): " + uuid.variant());
System.err.println("uuid.version(): " + uuid.version());
}
}