diff --git a/README.md b/README.md index 9c7a254..2a5d494 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,153 @@ + # ulid-creator -A Java library for generating and handling ULIDs + +A Java library for generating and handling ULIDs - _Universally Unique Lexicographically Sortable Identifiers_. + +How to Use +------------------------------------------------------ + +Create a ULID: + +```java +String ulid = UlidCreator.getUlid(); +``` + +Create a fast ULID: + +```java +String ulid = UlidCreator.getFastUlid(); +``` + +Create a ULID as GUID object: + +```java +UUID ulid = UlidCreator.getGuid(); +``` + +Create a fast ULID as GUID object: + +```java +UUID ulid = UlidCreator.getFastGuid(); +``` + +Create a ULID as byte sequence: + +```java +byte[] ulid = UlidCreator.getBytes(); +``` + +Create a fast ULID as byte sequence: + +```java +byte[] ulid = UlidCreator.getFastBytes(); +``` + +### Maven dependency + +Work in progress. + +Implementation +------------------------------------------------------ + +### ULID + +The ULID is a unique and sortable 26 char sequence. See the [ULID specification](https://github.com/ulid/spec) for more information. + +See the section on GUIDs to know how the 128 bits are generated in this library. + +```java +// ULIDs +String ulid = UlidCreator.getUlid(); +``` + +Examples of ULIDs: + +```text +01E1PPRTMSQ34W7JR5YSND6B8T +01E1PPRTMSQ34W7JR5YSND6B8V +01E1PPRTMSQ34W7JR5YSND6B8W +01E1PPRTMSQ34W7JR5YSND6B8X +01E1PPRTMSQ34W7JR5YSND6B8Y +01E1PPRTMSQ34W7JR5YSND6B8Z +01E1PPRTMSQ34W7JR5YSND6B90 +01E1PPRTMSQ34W7JR5YSND6B91 +01E1PPRTMTYMX8G17TWSJJZMEE < millisecond changed +01E1PPRTMTYMX8G17TWSJJZMEF +01E1PPRTMTYMX8G17TWSJJZMEG +01E1PPRTMTYMX8G17TWSJJZMEH +01E1PPRTMTYMX8G17TWSJJZMEJ +01E1PPRTMTYMX8G17TWSJJZMEK +01E1PPRTMTYMX8G17TWSJJZMEM +01E1PPRTMTYMX8G17TWSJJZMEN + ^ look ^ look + +|---------|--------------| + milli randomness +``` + +### GUID + +The GUIDs in this library are based on the ULID specification [9]. The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator. + +Every time the timestamp changes the random part is reset to a new random value. If the current timestamp is equal to the previous one, the random bits are incremented by 1. + +The default random number generator is `SecureRandom`, but it's possible to use any RNG that extends `Random`. + +```java +// GUID based on ULID spec +UUID guid = UlidCreator.getGuid(); +``` + +Examples of GUIDs based on ULID spec: + +```text +01706d6c-6aac-80bd-7ff5-f660c2dd58ea +01706d6c-6aac-80bd-7ff5-f660c2dd58eb +01706d6c-6aac-80bd-7ff5-f660c2dd58ec +01706d6c-6aac-80bd-7ff5-f660c2dd58ed +01706d6c-6aac-80bd-7ff5-f660c2dd58ee +01706d6c-6aac-80bd-7ff5-f660c2dd58ef +01706d6c-6aac-80bd-7ff5-f660c2dd58f0 +01706d6c-6aac-80bd-7ff5-f660c2dd58f1 +01706d6c-6aad-c795-370c-98d0be881bb8 < millisecond changed +01706d6c-6aad-c795-370c-98d0be881bb9 +01706d6c-6aad-c795-370c-98d0be881bba +01706d6c-6aad-c795-370c-98d0be881bbb +01706d6c-6aad-c795-370c-98d0be881bbc +01706d6c-6aad-c795-370c-98d0be881bbd +01706d6c-6aad-c795-370c-98d0be881bbe +01706d6c-6aad-c795-370c-98d0be881bbf + ^ look ^ look + +|------------|---------------------| + millisecs randomness +``` + +#### How use the `GuidCreator` directly + +These are some examples of using the `GuidCreator` to create ULIDs: + +```java + +// with java random generator (java.util.Random) +String ulid = UlidCreator.getGuidCreator().createUlid(); + +// with java random generator (java.util.Random) +String ulid = UlidCreator.getGuidCreator() + .withRandomGenerator(new Random()) + .createUlid(); + +// with fast random generator (Xorshift128Plus) +String ulid = UlidCreator.getGuidCreator() + .withFastRandomGenerator() + .createUlid(); + +// with fast random generator (Xorshift128Plus with salt) +int salt = (int) FingerprintUtil.getFingerprint(); +Random random = new Xorshift128PlusRandom(salt); +String ulid = UlidCreator.getGuidCreator() + .withRandomGenerator(random) + .createUlid(); +``` + +If you use the `GuidCreator` directly, you need do handle the `UlidCreatorException`, in the case that too many ULIDs are requested within the same millisecond. diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java index 3d71c18..2a2f58a 100644 --- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java @@ -26,47 +26,95 @@ package com.github.f4b6a3.ulid; import java.util.UUID; -import com.github.f4b6a3.ulid.factory.LexicalOrderGuidCreator; +import com.github.f4b6a3.ulid.exception.UlidCreatorException; +import com.github.f4b6a3.ulid.guid.GuidCreator; import com.github.f4b6a3.ulid.util.UlidUtil; /** * A factory for Universally Unique Lexicographically Sortable Identifiers. * * @see The ULID spec; https://github.com/ulid/spec - * */ public class UlidCreator { private UlidCreator() { } + /** + * Returns a ULID. + * + * @return a ULID + */ public static String getUlid() { - UUID guid = getLexicalOrderGuid(); - return UlidUtil.fromUuidToUlid(guid); + return GuidCreatorLazyHolder.INSTANCE.createUlid(); } /** - * Returns a Lexical Order GUID based on the ULID specification. + * Returns a fast ULID. * - * If you need a ULID string instead of a GUID, use - * {@link UlidCreator#getUlid()}. - * - * @return a Lexical Order GUID + * @return a ULID */ - public static UUID getLexicalOrderGuid() { - return LexicalOrderCreatorLazyHolder.INSTANCE.create(); + public static String getFastUlid() { + return FastGuidCreatorLazyHolder.INSTANCE.createUlid(); } /** - * Returns a {@link LexicalOrderGuidCreator}. + * Returns ULID as GUID object. * - * @return {@link LexicalOrderGuidCreator} + * @return a GUID */ - public static LexicalOrderGuidCreator getLexicalOrderCreator() { - return new LexicalOrderGuidCreator(); + public static UUID getGuid() { + return GuidCreatorLazyHolder.INSTANCE.create(); } - private static class LexicalOrderCreatorLazyHolder { - static final LexicalOrderGuidCreator INSTANCE = getLexicalOrderCreator().withoutOverflowException(); + /** + * Returns fast ULID as GUID object. + * + * @return a GUID + */ + public static UUID getFastGuid() { + return FastGuidCreatorLazyHolder.INSTANCE.create(); + } + + /** + * Returns ULID as byte sequence. + * + * @return a GUID + */ + public static byte[] getBytes() { + UUID guid = getGuid(); + return UlidUtil.fromUuidToBytes(guid); + } + + /** + * Returns fast ULID as byte sequence. + * + * @return a GUID + */ + public static byte[] getFastBytes() { + UUID guid = getFastGuid(); + return UlidUtil.fromUuidToBytes(guid); + } + + /** + * Return a GUID creator for direct use. + * + * This library uses the {@link GuidCreator} internally to generate ULIDs. + * + * The {@link GuidCreator} throws a {@link UlidCreatorException} when too + * many values are requested in the same millisecond. + * + * @return a {@link GuidCreator} + */ + public static GuidCreator getGuidCreator() { + return new GuidCreator(); + } + + private static class GuidCreatorLazyHolder { + static final GuidCreator INSTANCE = getGuidCreator().withoutOverflowException(); + } + + private static class FastGuidCreatorLazyHolder { + static final GuidCreator INSTANCE = getGuidCreator().withFastRandomGenerator().withoutOverflowException(); } } diff --git a/src/main/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreator.java b/src/main/java/com/github/f4b6a3/ulid/guid/GuidCreator.java similarity index 82% rename from src/main/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreator.java rename to src/main/java/com/github/f4b6a3/ulid/guid/GuidCreator.java index 7dcafdf..1d4fcd4 100644 --- a/src/main/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/guid/GuidCreator.java @@ -22,13 +22,16 @@ * SOFTWARE. */ -package com.github.f4b6a3.ulid.factory; +package com.github.f4b6a3.ulid.guid; import java.security.SecureRandom; import java.util.Random; import java.util.UUID; import com.github.f4b6a3.ulid.timestamp.TimestampStrategy; +import com.github.f4b6a3.ulid.util.FingerprintUtil; +import com.github.f4b6a3.ulid.util.UlidUtil; +import com.github.f4b6a3.ulid.exception.UlidCreatorException; import com.github.f4b6a3.ulid.random.Xorshift128PlusRandom; import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategy; @@ -38,7 +41,7 @@ import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategy; * * ULID specification: https://github.com/ulid/spec */ -public class LexicalOrderGuidCreator { +public class GuidCreator { protected static final long MAX_LOW = 0xffffffffffffffffL; // unsigned protected static final long MAX_HIGH = 0x000000000000ffffL; @@ -51,18 +54,18 @@ public class LexicalOrderGuidCreator { protected long low; protected long high; - protected static final String OVERFLOW_MESSAGE = "The system caused an overflow in the generator by requesting too many GUIDs."; + protected static final String OVERFLOW_MESSAGE = "The system caused an overflow in the generator by requesting too many IDs."; protected TimestampStrategy timestampStrategy; - public LexicalOrderGuidCreator() { + public GuidCreator() { this.reset(); this.timestampStrategy = new DefaultTimestampStrategy(); } /** * - * Return a Lexical Order GUID. + * Return a GUID based on the ULID specification. * * It has two parts: * @@ -125,6 +128,26 @@ public class LexicalOrderGuidCreator { return new UUID(msb, lsb); } + + /** + * Return a ULID. + * + * @return a ULID string + */ + public synchronized String createUlid() { + UUID guid = create(); + return UlidUtil.fromUuidToUlid(guid); + } + + /** + * Return a ULID as byte sequence. + * + * @return a byte sequence + */ + public synchronized byte[] createBytes() { + UUID guid = create(); + return UlidUtil.fromUuidToBytes(guid); + } /** * Return the current timestamp and resets or increments the random part. @@ -169,7 +192,7 @@ public class LexicalOrderGuidCreator { this.high = 0L; // Too many requests if (enableOverflowException) { - throw new LexicalOrderGuidException(OVERFLOW_MESSAGE); + throw new UlidCreatorException(OVERFLOW_MESSAGE); } } } @@ -179,11 +202,10 @@ public class LexicalOrderGuidCreator { * * @param timestampStrategy * a timestamp strategy - * @return {@link LexicalOrderGuidCreator} + * @return {@link GuidCreator} */ @SuppressWarnings("unchecked") - public synchronized T withTimestampStrategy( - TimestampStrategy timestampStrategy) { + public synchronized T withTimestampStrategy(TimestampStrategy timestampStrategy) { this.timestampStrategy = timestampStrategy; return (T) this; } @@ -201,10 +223,10 @@ public class LexicalOrderGuidCreator { * * @param random * a random generator - * @return {@link LexicalOrderGuidCreator} + * @return {@link GuidCreator} */ @SuppressWarnings("unchecked") - public synchronized T withRandomGenerator(Random random) { + public synchronized T withRandomGenerator(Random random) { this.random = random; return (T) this; } @@ -218,11 +240,12 @@ public class LexicalOrderGuidCreator { * See {@link Xorshift128PlusRandom} and * {@link FingerprintUtil#getFingerprint()} * - * @return {@link LexicalOrderGuidCreator} + * @return {@link GuidCreator} */ @SuppressWarnings("unchecked") - public synchronized T withFastRandomGenerator() { - this.random = new Xorshift128PlusRandom(); + public synchronized T withFastRandomGenerator() { + final int salt = (int) FingerprintUtil.getFingerprint(); + this.random = new Xorshift128PlusRandom(salt); return (T) this; } @@ -232,22 +255,14 @@ public class LexicalOrderGuidCreator { * An exception thrown when too many requests within the same millisecond * causes an overflow while incrementing the random bits of the GUID. * - * @return {@link LexicalOrderGuidCreator} + * @return {@link GuidCreator} */ @SuppressWarnings("unchecked") - public synchronized T withoutOverflowException() { + public synchronized T withoutOverflowException() { this.enableOverflowException = false; return (T) this; } - public static class LexicalOrderGuidException extends RuntimeException { - private static final long serialVersionUID = 1L; - - public LexicalOrderGuidException(String message) { - super(message); - } - } - private static class SecureRandomLazyHolder { static final Random INSTANCE = new SecureRandom(); } diff --git a/src/main/java/com/github/f4b6a3/ulid/random/Xorshift128PlusRandom.java b/src/main/java/com/github/f4b6a3/ulid/random/Xorshift128PlusRandom.java index a9195ce..de19962 100644 --- a/src/main/java/com/github/f4b6a3/ulid/random/Xorshift128PlusRandom.java +++ b/src/main/java/com/github/f4b6a3/ulid/random/Xorshift128PlusRandom.java @@ -43,7 +43,7 @@ public class Xorshift128PlusRandom extends Random { public Xorshift128PlusRandom() { this((int) System.nanoTime()); } - + /** * Constructor that receives an integer as 'salt'. This value is combined * with the current milliseconds and the object hash code to generate two @@ -58,7 +58,7 @@ public class Xorshift128PlusRandom extends Random { this.seed[0] = (((long) salt) << 32) | (time & 0x00000000ffffffffL); this.seed[1] = (((long) salt) << 32) | (hash & 0x00000000ffffffffL); } - + public Xorshift128PlusRandom(long[] seed) { this.seed = seed; } diff --git a/src/main/java/com/github/f4b6a3/ulid/util/FingerprintUtil.java b/src/main/java/com/github/f4b6a3/ulid/util/FingerprintUtil.java new file mode 100644 index 0000000..6de9aaf --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/FingerprintUtil.java @@ -0,0 +1,177 @@ +/* + * MIT License + * + * Copyright (c) 2020 Fabio Lima + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.f4b6a3.ulid.util; + +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +public class FingerprintUtil { + + private static MessageDigest messageDigest; + + private FingerprintUtil() { + } + + /** + * Returns long value representing a host fingerprint. + * + * The fingerprint is calculated from a list of system properties: OS + JVM + * + network details + system resources + locale + timezone. + * + * It uses these information to generate the fingerprint: operating system + * (name, version, arch), java virtual machine (vendor, version, runtime, + * VM), network settings (IP, MAC, host name, domain name), system resources + * (CPU cores, memory), locale (language, charset) and timezone. These + * information are concatenated and passed to a SHA-256 message digest. + * + * It returns the last 8 bytes of the resulting hash as long value. + * + * Read: https://en.wikipedia.org/wiki/Device_fingerprint + * + * @return a fingerprint as long value + */ + public static long getFingerprint() { + String hash = getSystemDataHash(); + return ByteUtil.toNumber(hash); + } + + /** + * Returns a SHA-256 hash string generated from all the system data: OS + + * JVM + network details + system resources. + * + * @return a string + */ + protected static String getSystemDataHash() { + + if (messageDigest == null) { + messageDigest = getMessageDigest(); + } + + String os = getOperatingSystem(); + String jvm = getJavaVirtualMachine(); + String net = getNetwork(); + String loc = getLocalization(); + String res = getResources(); + String string = String.join(" ", os, jvm, net, loc, res); + + byte[] bytes = string.getBytes(); + byte[] hash = messageDigest.digest(bytes); + + return ByteUtil.toHexadecimal(hash); + } + + /** + * Returns a string of the OS details. + * + * @return a string + */ + protected static String getOperatingSystem() { + String name = System.getProperty("os.name"); + String version = System.getProperty("os.version"); + String arch = System.getProperty("os.arch"); + return String.join(" ", name, version, arch); + } + + /** + * Returns a string of the JVM details. + * + * @return a string + */ + protected static String getJavaVirtualMachine() { + String vendor = System.getProperty("java.vendor"); + String version = System.getProperty("java.version"); + String rtName = System.getProperty("java.runtime.name"); + String rtVersion = System.getProperty("java.runtime.version"); + String vmName = System.getProperty("java.vm.name"); + String vmVersion = System.getProperty("java.vm.version"); + return String.join(" ", vendor, version, rtName, rtVersion, vmName, vmVersion); + } + + /** + * Return a string with locale, charset, encoding and timezone. + * + * @return a string + */ + protected static String getLocalization() { + String locale = Locale.getDefault().toString(); + String charset = Charset.defaultCharset().toString(); + String encoding = System.getProperty("file.encoding"); + String timezone = TimeZone.getDefault().getID(); + return String.join(" ", locale, charset, encoding, timezone); + } + + /** + * Returns a string of CPU cores and maximum memory available. + * + * @return a string + */ + protected static String getResources() { + int procs = Runtime.getRuntime().availableProcessors(); + long memory = Runtime.getRuntime().maxMemory(); + return String.join(" ", procs + " processors", memory + " bytes"); + } + + /** + * Returns a string of the network details. + * + * It's done in three two steps: + * + * 1. it tries to find the network data associated with the host name; + * + * 2. otherwise, it iterates through all interfaces to return the first one + * that is up and running. + * + * @return a string + */ + protected static String getNetwork() { + + NetworkData networkData = NetworkData.getNetworkData(); + + if (networkData == null) { + List networkDataList = NetworkData.getNetworkDataList(); + if (networkDataList != null && !networkDataList.isEmpty()) { + networkData = networkDataList.get(0); + } + } + + if (networkData == null) { + return null; + } + + return networkData.toString(); + } + + private static MessageDigest getMessageDigest() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("Message digest algorithm not supported.", e); + } + } +} diff --git a/src/main/java/com/github/f4b6a3/ulid/util/NetworkData.java b/src/main/java/com/github/f4b6a3/ulid/util/NetworkData.java new file mode 100644 index 0000000..9b74fef --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/NetworkData.java @@ -0,0 +1,198 @@ +/* + * MIT License + * + * Copyright (c) 2018-2019 Fabio Lima + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.f4b6a3.ulid.util; + +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +public class NetworkData { + + private String hostName; + private String hostCanonicalName; + private String interfaceName; + private String interfaceDisplayName; + private String interfaceHardwareAddress; + private List interfaceAddresses; + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public String getHostCanonicalName() { + return hostCanonicalName; + } + + public void setHostCanonicalName(String hostCanonicalName) { + this.hostCanonicalName = hostCanonicalName; + } + + public String getInterfaceName() { + return interfaceName; + } + + public void setInterfaceName(String interfaceName) { + this.interfaceName = interfaceName; + } + + public String getInterfaceDisplayName() { + return interfaceDisplayName; + } + + public void setInterfaceDisplayName(String interfaceDisplayName) { + this.interfaceDisplayName = interfaceDisplayName; + } + + public String getInterfaceHardwareAddress() { + return interfaceHardwareAddress; + } + + public void setInterfaceHardwareAddress(String interfaceHardwareAddress) { + this.interfaceHardwareAddress = interfaceHardwareAddress; + } + + public List getInterfaceAddresses() { + return interfaceAddresses; + } + + public void setInterfaceAddresses(List interfaceAddresses) { + this.interfaceAddresses = interfaceAddresses; + } + + @Override + public String toString() { + + String interfaceAddressesString = null; + if (this.interfaceAddresses != null) { + interfaceAddressesString = String.join(" ", this.interfaceAddresses); + } + + return String.join(" ", this.interfaceHardwareAddress, this.hostName, this.hostCanonicalName, + this.interfaceName, this.interfaceDisplayName, interfaceAddressesString); + } + + /** + * Returns a {@link NetworkData}. + * + * This method returns the network data associated to the host name. + * + * @return a {@link NetworkData} + */ + public static NetworkData getNetworkData() { + + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + NetworkInterface networkInterface = NetworkInterface.getByInetAddress(inetAddress); + return buildNetworkData(networkInterface, inetAddress); + } catch (UnknownHostException | SocketException e) { + return null; + } + } + + /** + * Returns a list of {@link NetworkData}. + * + * This method iterates over all the network interfaces to return those that + * are up and running. + * + * NOTE: it may be VERY EXPENSIVE on Windows systems, because that OS + * creates a lot of virtual network interfaces. + * + * @return a list of {@link NetworkData} + */ + public static List getNetworkDataList() { + try { + InetAddress inetAddress = InetAddress.getLocalHost(); + List networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + + HashSet networkDataHashSet = new HashSet<>(); + for (NetworkInterface networkInterface : networkInterfaces) { + NetworkData networkData = buildNetworkData(networkInterface, inetAddress); + if (networkData != null) { + networkDataHashSet.add(networkData); + } + } + return new ArrayList<>(networkDataHashSet); + } catch (SocketException | NullPointerException | UnknownHostException e) { + return Collections.emptyList(); + } + } + + private static NetworkData buildNetworkData(NetworkInterface networkInterface, InetAddress inetAddress) + throws SocketException { + if (isPhysicalNetworkInterface(networkInterface)) { + + String hostName = inetAddress != null ? inetAddress.getHostName() : null; + String hostCanonicalName = inetAddress != null ? inetAddress.getCanonicalHostName() : null; + String interfaceName = networkInterface.getName(); + String interfaceDisplayName = networkInterface.getDisplayName(); + String interfaceHardwareAddress = ByteUtil.toHexadecimal(networkInterface.getHardwareAddress()); + List interfaceAddresses = getInterfaceAddresses(networkInterface); + + NetworkData networkData = new NetworkData(); + networkData.setHostName(hostName); + networkData.setHostCanonicalName(hostCanonicalName); + networkData.setInterfaceName(interfaceName); + networkData.setInterfaceDisplayName(interfaceDisplayName); + networkData.setInterfaceHardwareAddress(interfaceHardwareAddress); + networkData.setInterfaceAddresses(interfaceAddresses); + + return networkData; + } + return null; + } + + private static boolean isPhysicalNetworkInterface(NetworkInterface networkInterface) { + try { + return networkInterface != null && networkInterface.isUp() + && !(networkInterface.isLoopback() || networkInterface.isVirtual()); + } catch (SocketException e) { + return false; + } + } + + private static List getInterfaceAddresses(NetworkInterface networkInterface) { + HashSet addresses = new HashSet<>(); + List interfaceAddresses = networkInterface.getInterfaceAddresses(); + if (interfaceAddresses != null && !interfaceAddresses.isEmpty()) { + for (InterfaceAddress addr : interfaceAddresses) { + if (addr.getAddress() != null) { + addresses.add(addr.getAddress().getHostAddress()); + } + } + } + return new ArrayList<>(addresses); + } +} diff --git a/src/test/java/com/github/f4b6a3/TestSuite.java b/src/test/java/com/github/f4b6a3/TestSuite.java index d67701f..330ed02 100644 --- a/src/test/java/com/github/f4b6a3/TestSuite.java +++ b/src/test/java/com/github/f4b6a3/TestSuite.java @@ -4,7 +4,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import com.github.f4b6a3.ulid.UlidCreatorTest; -import com.github.f4b6a3.ulid.factory.LexicalOrderGuidCreatorTest; +import com.github.f4b6a3.ulid.guid.GuidCreatorTest; import com.github.f4b6a3.ulid.random.NaiveRandomTest; import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategyTest; import com.github.f4b6a3.ulid.util.Base32UtilTest; @@ -16,11 +16,18 @@ import com.github.f4b6a3.ulid.util.UlidUtilTest; DefaultTimestampStrategyTest.class, ByteUtilTest.class, NaiveRandomTest.class, - LexicalOrderGuidCreatorTest.class, + GuidCreatorTest.class, Base32UtilTest.class, UlidUtilTest.class, UlidCreatorTest.class, }) +/** + * + * It bundles all JUnit test cases. + * + * Also see {@link UniquenesTest}. + * + */ public class TestSuite { } \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/UniquenessTest.java b/src/test/java/com/github/f4b6a3/UniquenessTest.java new file mode 100644 index 0000000..2ce9148 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/UniquenessTest.java @@ -0,0 +1,141 @@ +package com.github.f4b6a3; + +import java.util.HashSet; +import java.util.UUID; + +import com.github.f4b6a3.ulid.UlidCreator; +import com.github.f4b6a3.ulid.exception.UlidCreatorException; +import com.github.f4b6a3.ulid.guid.GuidCreator; +import com.github.f4b6a3.ulid.timestamp.FixedTimestampStretegy; + +/** + * + * This test starts many threads that keep requesting thousands of ULIDs to a + * single generator. + * + * This is is not included in the {@link TestSuite} because it takes a long time + * to finish. + */ +public class UniquenessTest { + + private int threadCount; // Number of threads to run + private int requestCount; // Number of requests for thread + + // private long[][] cacheLong; // Store values generated per thread + private HashSet hashSet; + + private boolean verbose; // Show progress or not + + // GUID creator based on ULID spec + private GuidCreator creator; + + /** + * Initialize the test. + * + * @param threadCount + * @param requestCount + * @param creator + */ + public UniquenessTest(int threadCount, int requestCount, GuidCreator creator, boolean progress) { + this.threadCount = threadCount; + this.requestCount = requestCount; + this.creator = creator; + this.verbose = progress; + this.initCache(); + } + + private void initCache() { + this.hashSet = new HashSet<>(); + } + + /** + * Initialize and start the threads. + */ + public void start() { + + Thread[] threads = new Thread[this.threadCount]; + + // Instantiate and start many threads + for (int i = 0; i < this.threadCount; i++) { + threads[i] = new Thread(new UniquenessTestThread(i, verbose)); + threads[i].start(); + } + + // Wait all the threads to finish + for (Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public class UniquenessTestThread implements Runnable { + + private int id; + private boolean verbose; + + public UniquenessTestThread(int id, boolean verbose) { + this.id = id; + this.verbose = verbose; + } + + /** + * Run the test. + */ + @Override + public void run() { + + double progress = 0; + int max = requestCount; + + for (int i = 0; i < max; i++) { + + // Request a UUID + UUID uuid = null; + try { + uuid = creator.create(); + } catch (UlidCreatorException e) { + // Ignore the overrun exception and try again + uuid = creator.create(); + } + + if (verbose) { + // Calculate and show progress + progress = (i * 1.0 / max) * 100; + if (progress % 1 == 0) { + System.out.println(String.format("[Thread %06d] %s %s %s%%", id, uuid, i, (int) progress)); + } + } + synchronized (hashSet) { + // Insert the value in cache, if it does not exist in it. + if (!hashSet.add(uuid)) { + throw new UlidCreatorException( + String.format("[DUPLICATE][Thread %s] %s %s %s%%", id, uuid, i, (int) progress)); + } + } + } + + if (verbose) { + // Finished + System.out.println(String.format("[Thread %s] Done.", id)); + } + } + } + + public static void execute(boolean verbose, int threadCount, int requestCount) { + GuidCreator creator = UlidCreator.getGuidCreator() + .withTimestampStrategy(new FixedTimestampStretegy(System.currentTimeMillis())); + + UniquenessTest test = new UniquenessTest(threadCount, requestCount, creator, verbose); + test.start(); + } + + public static void main(String[] args) { + boolean verbose = true; + int threadCount = 16; // Number of threads to run + int requestCount = 1_000_000; // Number of requests for thread + execute(verbose, threadCount, requestCount); + } +} diff --git a/src/test/java/com/github/f4b6a3/demo/DemoTest.java b/src/test/java/com/github/f4b6a3/demo/DemoTest.java new file mode 100644 index 0000000..2a5b274 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/demo/DemoTest.java @@ -0,0 +1,32 @@ +package com.github.f4b6a3.demo; + +import com.github.f4b6a3.ulid.UlidCreator; + +public class DemoTest { + + private static final String HORIZONTAL_LINE = "----------------------------------------"; + + public static void printList() { + int max = 1_000; + + System.out.println(HORIZONTAL_LINE); + System.out.println("### ULID"); + System.out.println(HORIZONTAL_LINE); + + for (int i = 0; i < max; i++) { + System.out.println(UlidCreator.getUlid()); + } + + System.out.println(HORIZONTAL_LINE); + System.out.println("### GUID"); + System.out.println(HORIZONTAL_LINE); + + for (int i = 0; i < max; i++) { + System.out.println(UlidCreator.getGuid()); + } + } + + public static void main(String[] args) { + printList(); + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java index d4ec39b..3681817 100644 --- a/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/UlidCreatorTest.java @@ -12,7 +12,7 @@ import java.util.HashSet; public class UlidCreatorTest { private static final int ULID_LENGTH = 26; - private static final int DEFAULT_LOOP_MAX = 10_000; + private static final int DEFAULT_LOOP_MAX = 100_000; @Test public void testGetUlid() { diff --git a/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorMock.java b/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorMock.java deleted file mode 100644 index 9f96741..0000000 --- a/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorMock.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.f4b6a3.ulid.factory; - -class LexicalOrderGuidCreatorMock extends LexicalOrderGuidCreator { - public LexicalOrderGuidCreatorMock(long low, long high, long previousTimestamp) { - super(); - this.low = low; - this.high = high; - this.previousTimestamp = previousTimestamp; - } -} \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorMock.java b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorMock.java new file mode 100644 index 0000000..539a653 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorMock.java @@ -0,0 +1,12 @@ +package com.github.f4b6a3.ulid.guid; + +import com.github.f4b6a3.ulid.guid.GuidCreator; + +class GuidCreatorMock extends GuidCreator { + public GuidCreatorMock(long low, long high, long previousTimestamp) { + super(); + this.low = low; + this.high = high; + this.previousTimestamp = previousTimestamp; + } +} \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorTest.java similarity index 73% rename from src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorTest.java rename to src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorTest.java index cd13e6f..10496c0 100644 --- a/src/test/java/com/github/f4b6a3/ulid/factory/LexicalOrderGuidCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorTest.java @@ -1,4 +1,4 @@ -package com.github.f4b6a3.ulid.factory; +package com.github.f4b6a3.ulid.guid; import java.util.Random; import java.util.UUID; @@ -6,19 +6,19 @@ import java.util.UUID; import org.junit.Test; import com.github.f4b6a3.ulid.exception.UlidCreatorException; -import com.github.f4b6a3.ulid.factory.LexicalOrderGuidCreator.LexicalOrderGuidException; +import com.github.f4b6a3.ulid.guid.GuidCreator; import com.github.f4b6a3.ulid.random.Xorshift128PlusRandom; import com.github.f4b6a3.ulid.timestamp.FixedTimestampStretegy; import static org.junit.Assert.*; -public class LexicalOrderGuidCreatorTest { +public class GuidCreatorTest { - private static final long DEFAULT_LOOP = 1000; + private static final long DEFAULT_LOOP_MAX = 100_000; private static final long TIMESTAMP = System.currentTimeMillis(); - private static final long MAX_LOW = LexicalOrderGuidCreator.MAX_LOW; - private static final long MAX_HIGH = LexicalOrderGuidCreator.MAX_HIGH; + private static final long MAX_LOW = GuidCreator.MAX_LOW; + private static final long MAX_HIGH = GuidCreator.MAX_HIGH; private static final Random RANDOM = new Xorshift128PlusRandom(); @@ -28,13 +28,13 @@ public class LexicalOrderGuidCreatorTest { long low = RANDOM.nextInt(); long high = RANDOM.nextInt(Short.MAX_VALUE); - LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = creator.create(); long firstMsb = (short) uuid.getMostSignificantBits(); long lastMsb = 0; - for (int i = 0; i <= DEFAULT_LOOP; i++) { + for (int i = 0; i <= DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); lastMsb = (short) uuid.getMostSignificantBits(); } @@ -50,21 +50,21 @@ public class LexicalOrderGuidCreatorTest { @Test public void testRandomLeastSignificantBits() { - LexicalOrderGuidCreator creator = new LexicalOrderGuidCreator(); + GuidCreator creator = new GuidCreator(); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = creator.create(); long firstLsb = uuid.getLeastSignificantBits(); long lastLsb = 0; - for (int i = 0; i < DEFAULT_LOOP; i++) { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); lastLsb = uuid.getLeastSignificantBits(); } - long expected = firstLsb + DEFAULT_LOOP; + long expected = firstLsb + DEFAULT_LOOP_MAX; assertEquals(String.format("The last LSB should be iqual to %s.", expected), expected, lastLsb); - long notExpected = firstLsb + DEFAULT_LOOP + 1; + long notExpected = firstLsb + DEFAULT_LOOP_MAX + 1; creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); uuid = creator.create(); lastLsb = uuid.getLeastSignificantBits(); @@ -77,15 +77,15 @@ public class LexicalOrderGuidCreatorTest { long low = RANDOM.nextInt(); long high = RANDOM.nextInt(Short.MAX_VALUE); - LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = new UUID(0, 0); - for (int i = 0; i < DEFAULT_LOOP; i++) { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); } - long expected = low + DEFAULT_LOOP; + long expected = low + DEFAULT_LOOP_MAX; long randomLsb = uuid.getLeastSignificantBits(); assertEquals(String.format("The LSB should be iqual to %s.", expected), expected, randomLsb); } @@ -96,11 +96,11 @@ public class LexicalOrderGuidCreatorTest { long low = MAX_LOW; long high = RANDOM.nextInt(Short.MAX_VALUE); - LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = new UUID(0, 0); - for (int i = 0; i < DEFAULT_LOOP; i++) { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); } @@ -109,17 +109,17 @@ public class LexicalOrderGuidCreatorTest { assertEquals(String.format("The MSB should be iqual to %s.", expected), expected, randomMsb); } - @Test(expected = LexicalOrderGuidException.class) + @Test(expected = UlidCreatorException.class) public void testShouldThrowOverflowException() { - long low = MAX_LOW - DEFAULT_LOOP; + long low = MAX_LOW - DEFAULT_LOOP_MAX; long high = MAX_HIGH; - LexicalOrderGuidCreatorMock creator = new LexicalOrderGuidCreatorMock(low, high, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = new UUID(0, 0); - for (int i = 0; i < DEFAULT_LOOP; i++) { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); } diff --git a/src/test/java/com/github/f4b6a3/ulid/random/NaiveRandomTest.java b/src/test/java/com/github/f4b6a3/ulid/random/NaiveRandomTest.java index a6ccc88..b5226d5 100644 --- a/src/test/java/com/github/f4b6a3/ulid/random/NaiveRandomTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/random/NaiveRandomTest.java @@ -6,7 +6,7 @@ import static org.junit.Assert.*; public class NaiveRandomTest { - private static final int DEFAULT_LOOP_LIMIT = 10_000; + private static final int DEFAULT_LOOP_LIMIT = 100_000; private static final String EXPECTED_BIT_COUNT_RANDOM_LONG = "The average bit count expected for random long values is 32"; @Test diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java index 3e7e49a..e50f0f3 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java @@ -7,7 +7,7 @@ import java.util.UUID; import org.junit.Test; import com.github.f4b6a3.ulid.util.UlidUtil.UlidUtilException; -import com.github.f4b6a3.ulid.util.ByteUtil; +import com.github.f4b6a3.ulid.UlidCreator; public class UlidUtilTest { @@ -18,7 +18,7 @@ public class UlidUtilTest { private static final long TIMESTAMP_MAX = 281474976710655l; // 2^48 - 1 private static final int ULID_LENGTH = 26; - private static final int DEFAULT_LOOP_MAX = 10_000; + private static final int DEFAULT_LOOP_MAX = 100_000; private static final String[] EXAMPLE_DATES = { "1970-01-01T00:00:00.000Z", "1985-10-26T01:16:00.123Z", "2001-09-09T01:46:40.456Z", "2020-01-15T14:30:33.789Z", "2038-01-19T03:14:07.321Z" }; @@ -177,28 +177,25 @@ public class UlidUtilTest { assertFalse("ULID with timestamp greater than (2^48)-1 should be invalid.", UlidUtil.isValid(ulid)); } - // TODO - // @Test - // public void testToAndFromUlid() { - // - // for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - // - // // Use random values - // UUID uuid = UuidCreator.getFastRandom(); - // String ulid = UlidUtil.fromUuidToUlid(uuid); - // - // assertTrue("ULID is null", ulid != null); - // assertTrue("ULID is empty", !ulid.isEmpty()); - // assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); - // assertTrue("ULID is not valid", UlidUtil.isValid(ulid, /* strict */ - // true)); - // - // UUID result = UlidUtil.fromUlidToUuid(ulid); - // assertEquals("Result ULID is different from original ULID", uuid, - // result); - // - // } - // } + @Test + public void testToAndFromUlid() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + UUID uuid = UlidCreator.getFastGuid(); + String ulid = UlidUtil.fromUuidToUlid(uuid); + + assertTrue("ULID is null", ulid != null); + assertTrue("ULID is empty", !ulid.isEmpty()); + assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); + assertTrue("ULID is not valid", UlidUtil.isValid(ulid, /* strict */ + true)); + + UUID result = UlidUtil.fromUlidToUuid(ulid); + assertEquals("Result ULID is different from original ULID", uuid, result); + + } + } private String leftPad(String unpadded) { return "0000000000".substring(unpadded.length()) + unpadded;