diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f4a1e34..bf9da52 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ tidelift: "maven/com.fasterxml.uuid:java-uuid-generator" +github: cowtowncoder diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2390d8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..abc6637 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,47 @@ +name: Build +on: + push: + branches: + - master + - "4.3" + - "3.0" + paths-ignore: + - "README.md" + - "release-notes/*" + pull_request: + branches: + - master +permissions: + contents: read +jobs: + build: + runs-on: 'ubuntu-latest' + strategy: + fail-fast: false + matrix: + java_version: ['8', '11', '17', '21'] + env: + JAVA_OPTS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up JDK + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: "temurin" + java-version: ${{ matrix.java_version }} + cache: 'maven' + server-id: central-snapshots + server-username: CI_DEPLOY_USERNAME + server-password: CI_DEPLOY_PASSWORD + - name: Build + run: ./mvnw -B -q -ff -ntp verify + - name: Generate code coverage + if: ${{ github.event_name != 'pull_request' && matrix.java_version == '8' }} + run: ./mvnw -B -q -ff -ntp test + - name: Publish code coverage + if: ${{ github.event_name != 'pull_request' && matrix.java_version == '8' }} + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./target/site/jacoco/jacoco.xml + flags: unittests diff --git a/.gitignore b/.gitignore index afdc2b5..1807cd8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,15 @@ -# Skip maven 'target' directory -target +syntax: glob +*.class +*~ +*.bak +*.off +*.old +*.java.orig +.DS_Store + +# building +/target +.mvn/wrapper/maven-wrapper.jar # plus eclipse crap .classpath diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..b901097 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..b9b1153 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar diff --git a/.travis.yml b/.travis.yml index 3ac4710..b99271d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,3 +3,7 @@ language: java jdk: - openjdk8 - openjdk11 + +cache: + directories: + - $HOME/.m2 diff --git a/README.md b/README.md index f9072d5..26bd017 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,44 @@ JUG is a set of Java classes for working with UUIDs: generating UUIDs using any of standard methods, outputting efficiently, sorting and so on. -It generates UUIDs according to the [UUID specification (RFC-4122)](https://tools.ietf.org/html/rfc4122) -(also see [Wikipedia UUID page](http://en.wikipedia.org/wiki/UUID) for more explanation) +It generates UUIDs according to the [UUID specification (RFC-9562)](https://tools.ietf.org/html/rfc9562) +(see [Wikipedia UUID page](http://en.wikipedia.org/wiki/UUID) for more explanation) -JUG was written by Tatu Saloranta () originally in 2002 and has been updated over years. -In addition, many other individuals have helped fix bugs and implement new features: please see `release-notes/CREDITS` -for the complete list. +JUG was written by Tatu Saloranta () originally in 2002 and has been updated over the years. +In addition, many other individuals have helped fix bugs and implement new features: please see `release-notes/CREDITS` for the complete list. JUG is licensed under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). +## Supported UUID versions (1, 3, 4, 5, 6, 7) + +JUG supports both "classic" versions defined in [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122): + +* `1`: time/location - based +* `3` and `5`: name hash - based +* `4`: random number - based + +and newly (in 2022-) proposed (see [uuid6](https://uuid6.github.io/uuid6-ietf-draft/) and [RFC-9562](https://datatracker.ietf.org/doc/html/rfc9562) variants: + +* `6`: reordered variant of version `1` (with lexicographic ordering) +* `7`: Unix-timestamp + random based variant (also with lexicographic ordering) + ## Status -[![Build Status](https://travis-ci.org/cowtowncoder/java-uuid-generator.svg)](https://travis-ci.org/cowtowncoder/java-uuid-generator) -[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/com.fasterxml.uuid/java-uuid-generator/badge.svg)](http://www.javadoc.io/doc/com.fasterxml.uuid/java-uuid-generator) -[![Tidelift](https://tidelift.com/badges/package/maven/com.fasterxml.uuid:java-uuid-generator)](https://tidelift.com/subscription/pkg/maven-com-fasterxml-uuid-java-uuid-generator?utm_source=maven-com-fasterxml-uuid-java-uuid-generator&utm_medium=referral&utm_campaign=readme) +| Type | Status | +| ---- | ------ | +| Build (CI) | [![Build (github)](https://github.com/cowtowncoder/java-uuid-generator/actions/workflows/main.yml/badge.svg)](https://github.com/cowtowncoder/java-uuid-generator/actions/workflows/main.yml) | +| Artifact | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.uuid/java-uuid-generator/badge.svg)](https://search.maven.org/artifact/com.fasterxml.uuid/java-uuid-generator) | +| OSS Sponsorship | [![Tidelift](https://tidelift.com/badges/package/maven/com.fasterxml.uuid:java-uuid-generator)](https://tidelift.com/subscription/pkg/maven-com-fasterxml-uuid-java-uuid-generator?utm_source=maven-com-fasterxml-uuid-java-uuid-generator&utm_medium=referral&utm_campaign=readme) | +| Javadocs | [![Javadoc](https://javadoc.io/badge/com.fasterxml.uuid/java-uuid-generator.svg)](http://www.javadoc.io/doc/com.fasterxml.uuid/java-uuid-generator) +| Code coverage (5.x) | [![codecov.io](https://codecov.io/github/cowtowncoder/java-uuid-generator/coverage.svg?branch=master)](https://codecov.io/github/cowtowncoder/java-uuid-generator?branch=master) | +| OpenSSF Score | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/cowtowncoder/java-uuid-generator/badge)](https://securityscorecards.dev/viewer/?uri=github.com/cowtowncoder/java-uuid-generator) | ## Usage -JUG can be used as a command-line tool (via class 'com.fasterxml.uuid.Jug`), or as a pluggable component. +JUG can be used as a command-line tool (via class `com.fasterxml.uuid.Jug`), +or as a pluggable component. -### Via Maven +### Maven Dependency Maven coordinates are: @@ -29,10 +47,24 @@ Maven coordinates are: com.fasterxml.uuid java-uuid-generator - 3.2.0 + 5.1.0 ``` + +Gradle: + +```groovy +implementation 'com.fasterxml.uuid:java-uuid-generator:5.1.0' +``` + +#### Third-party Dependencies by JUG + +The only dependency for JUG is the logging library: + +* For versions up to 3.x, `log4j` is used, optionally (runtime dependency) +* For versions 4.x and up, `slf4j` API is used: logging implementation to be provided by calling application + ### JDK9+ module info Since version `3.2.0`, JUG defines JDK9+ compatible `module-info.class`, with module name of `com.fasterxml.uuid`. @@ -41,32 +73,117 @@ Since version `3.2.0`, JUG defines JDK9+ compatible `module-info.class`, with mo For direct downloads, check out [Project Wiki](../../wiki). -### Using JUG +### Using JUG as Library + +#### Generating UUIDs -Generation itself is done by first selecting a kind of generator to use, and then calling its `generate()` method, -for example: +The original use case for JUG was generation of UUID values. This is done by first selecting a kind of generator to use, and then calling its `generate()` method. +For example: ```java -UUID uuid = Generators.randomBasedGenerator().generate(); -UUID uuid = Generators.timeBasedGenerator().generate(); +UUID uuid = Generators.timeBasedGenerator().generate(); // Version 1 +UUID uuid = Generators.randomBasedGenerator().generate(); // Version 4 +UUID uuid = Generators.nameBasedgenerator().generate("string to hash"); // Version 5 +// With JUG 4.1+: support for https://github.com/uuid6/uuid6-ietf-draft versions 6 and 7: +UUID uuid = Generators.timeBasedReorderedGenerator().generate(); // Version 6 +UUID uuid = Generators.timeBasedEpochGenerator().generate(); // Version 7 +// With JUG 5.0 added variation: +UUID uuid = Generators.timeBasedEpochRandomGenerator().generate(); // Version 7 with per-call random values ``` -If you want customize generators, you may also just want to hold on to generator instance, for example: +If you want customize generators, you may also just want to hold on to generator instance: + ```java TimeBasedGenerator gen = Generators.timeBasedGenerator(EthernetAddress.fromInterface()); UUID uuid = gen.generate(); UUID anotherUuid = gen.generate(); ``` +If your machine has a standard IP networking setup, the `Generators.defaultTimeBasedGenerator` (added in JUG 4.2) +factory method will try to determine which network interface corresponds to the default route for +all outgoing network traffic, and use that for creating a time based generator. +This is likely a good choice for common usage scenarios if you want a version 1 UUID generator. + +```java +TimeBasedGenerator gen = Generators.defaultTimeBasedGenerator(); +UUID uuid = gen.generate(); +UUID anotherUuid = gen.generate(); +``` + Generators are fully thread-safe, so a single instance may be shared among multiple threads. -JavaDocs for project can be found from [Project Wiki](../../wiki). +Javadocs for further information can be found from [Project Wiki](../../wiki). + +#### Converting `java.util.UUID` values into byte[] + +Sometimes you may want to convert from `java.util.UUID` into external serialization: +for example, as `String`s or byte arrays (`byte[]`). +Conversion to `String` is easy with `UUID.toString()` (provided by JDK), but there is no similar functionality for converting into `byte[]`. + +But `UUIDUtil` class provides methods for efficient conversions: + +``` +byte[] asBytes = UUIDUtil.asByteArray(uuid); +// or if you have longer buffer already +byte[] outputBuffer = new byte[1000]; +// append at position #100 +UUIDUtil.toByteArray(uuid, outputBuffer, 100); +``` + +#### Constructing `java.util.UUID` values from String, byte[] + +`UUID` values are often passed as java `String`s or `byte[]`s (byte arrays), +and conversion is needed to get to actual `java.util.UUID` instances. +JUG has optimized conversion functionality available via class `UUIDUtil` (package +`com.fasterxml.uuid.impl`), used as follows: + +``` +UUID uuidFromStr = UUIDUtil.uuid("ebb8e8fe-b1b1-11d7-8adb-00b0d078fa18"); +byte[] rawUuidBytes = ...; // byte array with 16 bytes +UUID uuidFromBytes = UUIDUtil.uuid(rawUuidBytes) +``` + +Note that while JDK has functionality for constructing `UUID` from `String`, like so: + +``` +UUID uuidFromStr = UUID.fromString("ebb8e8fe-b1b1-11d7-8adb-00b0d078fa18"); +``` + +it is rather slower than JUG version: for more information, read +[Measuring performance of Java UUID.fromString()](https://cowtowncoder.medium.com/measuring-performance-of-java-uuid-fromstring-or-lack-thereof-d16a910fa32a). + +### Using JUG as CLI + +JUG jar built under `target/`: + +``` +target/java-uuid-generator-5.1.0-SNAPSHOT.jar +``` + +can also be used as a simple Command-line UUID generation tool. + +To see usage you can do something like: + + java -jar target/java-uuid-generator-5.1.0-SNAPSHOT.jar + +and get full instructions, but to generate 5 Random-based UUIDs, you would use: + + java -jar target/java-uuid-generator-5.1.0-SNAPSHOT.jar -c 5 r + +(where `-c` (or `--count`) means number of UUIDs to generate, and `r` means Random-based version) + +NOTE: this functionality is included as of JUG 4.1 -- with earlier versions you would need a bit longer invocation as Jar metadata did not specify "Main-Class". +If so, you would need to use + + java -cp target/java-uuid-generator-5.1.0-SNAPSHOT.jar com.fasterxml.uuid.Jug -c 5 r ## Compatibility -JUG versions 3.1 and 3.2 require JDK 1.6 to work, mostly to be able to access local Ethernet MAC address. +JUG versions 3.1 and later require JDK 1.6 to work, mostly to be able to access local Ethernet MAC address. Earlier versions (3.0 and before) worked on 1.4 (which introduced `java.util.UUID`). +JUG versions 5.0 and later require JDK 8 to work. + ## Known Issues JDK's `java.util.UUID` has flawed implementation of `compareTo()`, which uses naive comparison @@ -109,9 +226,9 @@ There are many other publicly available UUID generators. For example: * JDK has included `java.util.UUID` since 1.4, but omits generation methods (esp. time/location based ones), has sub-standard performance for many operations and implements comparison in useless way * [ohannburkard.de UUID generator](http://johannburkard.de/software/uuid/) -Note that although some packages claim to be faster than others, it is not clear whether: +Note that although some packages claim to be faster than others, it is not clear: -1. Claims have been properly verified (or, if they have, can be independently verified), AND -2. It is not likely that performance differences truly matter: JUG, for example, can generate a millions of UUID per second per core (sometimes hitting the theoretical limit of 10 million per second), and it seems unlikely that generation will be bottleneck for about any use case +1. whether claims have been properly verified (or, if they have, can be independently verified), OR +2. whether performance differences truly matter: JUG, for example, can generate millions of UUID per second per core (sometimes hitting the theoretical limit of 10 million per second) -- and it seems unlikely that generation will be bottleneck for any actual use case so it is often best to choose based on stability of packages and API. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..969ff7e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +Last Updated: 2020-03-21 + +## Reporting a Vulnerability + +The recommended mechanism for reporting possible security vulnerabilities follows +so-called "Coordinated Disclosure Plan" (see [definition of DCP](https://vuls.cert.org/confluence/display/Wiki/Coordinated+Vulnerability+Disclosure+Guidance) +for general idea). The first step is to file a [Tidelift security contact](https://tidelift.com/security): +Tidelift will route all reports via their system to maintainers of relevant package(s), and start the +process that will evaluate concern and issue possible fixes, send update notices and so on. +Note that you do not need to be a Tidelift subscriber to file a security contact. + +Alternatively you may also report possible vulnerabilities to `info` at fasterxml dot com +mailing address. Note that filing an issue to go with report is fine, but if you do that please +DO NOT include details of security problem in the issue but only in email contact. +This is important to give us time to provide a patch, if necessary, for the problem. diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..5643201 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..23b7079 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index 59001ba..7d2af65 100644 --- a/pom.xml +++ b/pom.xml @@ -6,26 +6,29 @@ com.fasterxml oss-parent - 38 + 68 com.fasterxml.uuid java-uuid-generator + bundle Java UUID Generator - 3.3.1-SNAPSHOT + 5.1.1-SNAPSHOT Java UUID Generator (JUG) is a Java library for generating Universally Unique IDentifiers, UUIDs (see http://en.wikipedia.org/wiki/UUID). It can be used either as a component in a bigger application, or as a standalone command line tool. JUG generates UUIDs according to the IETF UUID draft specification. -JUG supports all 3 official UUID generation methods. +JUG supports 3 original official UUID generation methods as well as later additions (v6, v7) https://github.com/cowtowncoder/java-uuid-generator scm:git:git://github.com/cowtowncoder/java-uuid-generator.git https://github.com/cowtowncoder/java-uuid-generator scm:git:git@github.com:cowtowncoder/java-uuid-generator.git - HEAD + java-uuid-generator-5.1.0 @@ -38,12 +41,11 @@ JUG supports all 3 official UUID generation methods. http://github.com/cowtowncoder/java-uuid-generator/issues - - 2.2.1 - UTF-8 - 1.7.29 + 1.7.36 + + 2024-06-02T23:59:30Z @@ -89,29 +91,22 @@ JUG supports all 3 official UUID generation methods. maven-compiler-plugin ${version.plugin.compiler} - 1.6 - 1.6 + 1.8 + 1.8 - - org.apache.maven.plugins - maven-source-plugin - ${version.plugin.source} - - - attach-sources - - jar - - - - org.apache.maven.plugins maven-javadoc-plugin - ${version.plugin.javadoc} + + + src/main/java + attach-javadocs @@ -137,15 +132,18 @@ JUG supports all 3 official UUID generation methods. com.fasterxml.uuid;version="${project.version}", com.fasterxml.uuid.ext;version="${project.version}", - com.fasterxml.uuid.impl;version="${project.version}" + com.fasterxml.uuid.impl;version="${project.version}", + com.fasterxml.uuid.jug;version="${project.version}" com.fasterxml.uuid;version="[${project.version},${project.version}]", com.fasterxml.uuid.ext;version="[${project.version},${project.version}]", com.fasterxml.uuid.impl;version="[${project.version},${project.version}]", - org.slf4j;version="[${slf4j.version},)" + com.fasterxml.uuid.jug;version="[${project.version},${project.version}]", + org.slf4j;version="[${slf4j.version},2)" + com.fasterxml.uuid.Jug @@ -182,6 +180,30 @@ JUG supports all 3 official UUID generation methods. + + + org.jacoco + jacoco-maven-plugin + + + + prepare-agent + + + + report + test + + report + + + + + + + org.sonatype.central + central-publishing-maven-plugin + diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 19bcd91..a4819df 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -106,4 +106,48 @@ Ed Anuff: Felix W. Dekker (FWDekker@github) Contributed #36: Add customisable clock to UUIDTimer [3.3.0] - + +Andre Brait (andrebrait@github) + Contributed #32: Use SLF4J instead of Log4J directly + [4.0] + +Pascal Schumacher (PascalSchumacher@github) + * Reported #37: Problematic OSGI version range for slf4j dependency + [4.0.1] + +Hal Hildebrand (Hellblazer@github) + * Contributed #41: Add support for Proposed type v6 (reordered timestamp) + [4.1.0] + * Contributed #46: Add support for Proposed type v7 (epoch-based time uuid) + [4.1.0] + * Contributed fix fox #69: UUID version 7 implementation sorting incorrect? + [4.1.1] + +Dirk-Jan Rutten (excitement-engineer@github) + * Reported #69: UUID version 7 implementation sorting incorrect? + [4.1.1] + +Paul Galbraith (pgalbraith@github) + * Contributed #73: Add `Generators.defaultTimeBasedGenerator()` to use "default" + interface address for time/based UUIDs + [4.2.0] + +Pavel Raev (magdel@github) + * Contributed #81: Add UUIDUtil.extractTimestamp() for extracting 64-bit + timestamp for all timestamp-based versions + [5.0.0] + * Contributed #94 Add alternate version to UUIDv7 generator that uses random + values on every call (not just for different timestamp) + [5.0.0] + +Maia Everett (Maia-Everett@github) + * Contributed #85: Fix `LazyRandom` for native code generation tools + [5.0.0] + +Daniel Albuquerque (worldtiki@github) + * Contributed #99: New factory method to create TimeBasedEpochRandomGenerator + [5.1.0] + +Alexander Ilinykh (divinenickname@github) + * Contributed improvements to README.md, pom.xml (OSGi inclusion) + [5.1.1] diff --git a/release-notes/FAQ b/release-notes/FAQ index 9e3d881..c28058f 100644 --- a/release-notes/FAQ +++ b/release-notes/FAQ @@ -55,7 +55,7 @@ Name-based: 1 million/second (depends on length, namespace etc; this with MD5) So with default settings, time-based algorithm is by far the fastest; usually followed by name/hash based alternative (for short/medium -names at least), and random-based variant being slowest. +names at least), and random-based version being slowest. Finally, if performance _really_ is very important for you, there is a further complication when using time-based algorithm; Java's @@ -95,7 +95,7 @@ native uuidgen), using random-based method may be the best option; although there is a file-locking base synchronizer available for time-based generation. This works with multiple JVMs, but may not be applicable to synchronize with non-Java generators. -Random-number based variant should be safe to use, as long as the +Random-number based version should be safe to use, as long as the underlying random number generator is good (which SecureRandom by JDK should be). diff --git a/release-notes/USAGE b/release-notes/USAGE index d0d628a..a5dc014 100644 --- a/release-notes/USAGE +++ b/release-notes/USAGE @@ -49,7 +49,7 @@ the way a new JVM is usually instantiated between calls: * Generating the first UUID can be remarkably slow. This is because a new secure random number generator is initialized at that time (if - using random number based variant) + using random number based version) Subsequent calls are faster, but this has to be done using --count command-line argument, to create multiple UUIDs with same invocation. * Generating time-based UUIDs is not as secure due to JVM being re-initialized diff --git a/release-notes/VERSION b/release-notes/VERSION index cc68fc0..14a5670 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -4,6 +4,79 @@ Project: java-uuid-generator Releases ============================================================================ +(not yet released) + +#122: RFC-4122 Obsoleted by RFC-9562 (document change) + (pointed out by @akefirad) +- Update to `oss-parent` v68 to switch to Central Portal publishing +- Branch "master" renamed as "main" + +5.1.0 (02-Jun-2024) + +#99: New factory method to create TimeBasedEpochRandomGenerator + (contributed by Daniel A) +#105: `UUIDUtil.extractTimestamp()` is broken for versions 1 and 6 + (contributed by @magdel) + +5.0.0 (23-Feb-2024) + +#53: Increase JDK baseline to JDK 8 +#81: Add `UUIDUtil.extractTimestamp()` for extracting 64-bit timestamp for + all timestamp-based versions + (requested by @gabrielbalan) + (contributed by @magdel) +#85: Fix `LazyRandom` for native code generation tools + (contributed by @Maia-Everett) +#94: Add alternate version to UUIDv7 generator that uses random values on every + call (not just for different timestamp) + (contributed by @magdel) + +4.3.0 (12-Sep-2023) + +#78: TimeBasedEpochGenerator (UUIDv7) can't be provided a `UUIDClock` + (reported by @Frozenlock) +#84: Add `construct()` methods to specify the milliseconds being used + for time-based UUID generation + (contributed by @BranchPredictor) + +4.2.0 (14-May-2023) + +#73: Add `Generators.defaultTimeBasedGenerator()` to use "default" interface + address for time/based UUIDs + (contributed by Paul G) + +4.1.1 (01-May-2023) + +#67: Ensure correct distinction between variant and version in documentation + (requested by @mindloaf) + (contributed by @mukham12) +#69: UUID version 7 implementation sorting incorrect? + (reported by Dirk-Jan R) + (fix contributed by Hal H) + +4.1.0 (07-Jan-2023) + +#41: Add support for Proposed type v6 (reordered timestamp) + (contributed by Hal H) +#46: Add support for Proposed type v7 (epoch-based time uuid) + (contributed by Hal H) +#55: Add `Main-Class` manifest to make jar invoke `Jug` class +#57: Add constants for "Nil UUID", "Max UUID" (from draft "new UUID" spec) in `UUIDUtil` +#65: Enable "Reproducible Build" +- Fix a minor issue with argument validation for `Jug` tool class +- Update junit dependency (via oss-parent:41) +- Update slf4j-api to 1.7.36 + +4.0.1 (03-Mar-2020) + +#37: Problematic OSGI version range for slf4j dependency + (reported by Pascal S) + +4.0 (22-Feb-2020) + +#32: Use SLF4J instead of Log4J directly + (implemented by Andre B) + 3.3.0 (07-Feb-2020) #36: Add customisable clock to UUIDTimer diff --git a/src/main/java/com/fasterxml/uuid/EgressInterfaceFinder.java b/src/main/java/com/fasterxml/uuid/EgressInterfaceFinder.java new file mode 100644 index 0000000..3346822 --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/EgressInterfaceFinder.java @@ -0,0 +1,416 @@ +package com.fasterxml.uuid; + +import java.io.IOException; +import java.net.*; +import java.util.*; + +import static java.lang.String.format; + +/** + * A utility to attempt to find the default egress interface on the current + * system. The egress interface is the interface which is assigned the default + * network route, such that outbound network traffic is routed out through that + * interface. + * + * @since 4.2 + */ +public class EgressInterfaceFinder { + + public static final int DEFAULT_TIMEOUT_MILLIS = 5000; + + /** + * Attempt to find the default egress interface on the current system. + * + *

This is done on a best efforts basis, as Java does not provide the + * necessary level of OS integration that is required to do this robustly. + * However, this utility should do a decent job on Windows, Linux and macOS + * so long as the local system has a working network connection at the time + * of execution. If the current system is multihomed with multiple egress + * interfaces, one such interface will be chosen indeterminately. + * + *

Accurately determining the egress interface necessitates us attempting + * to make outbound network connections. This will be done + * synchronously and can be a very slow process. You can tune the amount of + * time allowed to establish the outbound connections by + * increasing/decreasing the timeout value. + * + * @return the egress interface + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface egressInterface() throws EgressResolutionException { + return fromDefaultMechanisms(DEFAULT_TIMEOUT_MILLIS); + } + + /** + * Attempt to find the default egress interface on the current system, + * using the specified connection timeout duration. + * + *

This will attempt to connect to one of the root DNS nameservers + * (chosen randomly), and failing that, simply to IPv4 address 1.1.1.1 + * and finally IPv6 address 1::1. + * + * @param timeoutMillis the amount of time (milliseconds) allowed to + * establish an outbound connection + * @return the egress interface + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface fromDefaultMechanisms(final int timeoutMillis) + throws EgressResolutionException { + + Finder[] finders = new Finder[] { + rootNameServerFinder(timeoutMillis), + remoteConnectionFinder(timeoutMillis, + new InetSocketAddress("1.1.1.1", 0)), + remoteConnectionFinder(timeoutMillis, + new InetSocketAddress("1::1", 0)) + }; + + return fromAggregate(finders); + } + + /** + * Attempt to find the default egress interface on the current system, + * by trying each of the specified discovery mechanisms, in order, until + * one of them succeeds. + * + * @return the egress interface + * @param finders array of finder callbacks to be executed + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface fromAggregate(Finder[] finders) + throws EgressResolutionException { + + Collection exceptions = + new ArrayList(); + + for (Finder finder : finders) { + try { + return finder.egressInterface(); + } catch (EgressResolutionException e) { + exceptions.add(e); + } + } + + throw new EgressResolutionException(exceptions.toArray( + new EgressResolutionException[0])); + } + + private Finder rootNameServerFinder(final int timeoutMillis) { + return new Finder() { + @Override + public NetworkInterface egressInterface() + throws EgressResolutionException { + return fromRootNameserverConnection(timeoutMillis); + } + }; + } + + /** + * Attempt to find the default egress interface on the current system, + * by connecting to one of the root name servers (chosen at random). + * + * @param timeoutMillis the amount of time (milliseconds) allowed to + * establish an outbound connection + * @return the egress interface + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface fromRootNameserverConnection(int timeoutMillis) + throws EgressResolutionException { + String domainName = randomRootServerName(); + InetSocketAddress address = new InetSocketAddress(domainName, 53); + return fromRemoteConnection(timeoutMillis, address); + } + + static String randomRootServerName() { + String roots = "abcdefghijklm"; + int index = new Random().nextInt(roots.length()); + return roots.charAt(index) + ".root-servers.net"; + } + + private Finder remoteConnectionFinder(final int timeoutMillis, + final InetSocketAddress address) { + return new Finder() { + @Override + public NetworkInterface egressInterface() + throws EgressResolutionException { + return fromRemoteConnection(timeoutMillis, address); + } + }; + } + + /** + * Attempt to find the default egress interface on the current system, + * by connection to the specified address. This will try two different + * methods: + *

    + *
  • using a {@link DatagramSocket}, which seems to work well for Windows + * & Linux, and is faster to uses than {@link Socket} as opening one does + * not actually require negotiate a handshake connection, but this does + * not appear to work on MacOS + *
  • using a {@link Socket}, which seems to work better for MacOS, but + * needs to actually negotiate a connection handshake from a remote host + *
+ * + * @param timeoutMillis the amount of time (milliseconds) allowed to + * establish an outbound connection + * @param remoteAddress the address to which a connection will be attempted + * in order to determine which interface is used to + * connect + * @return the egress interface + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface fromRemoteConnection( + int timeoutMillis, InetSocketAddress remoteAddress) + throws EgressResolutionException { + + if (remoteAddress.isUnresolved()) { + throw new EgressResolutionException( + format("remote address [%s] is unresolved", remoteAddress)); + } + + Finder socketFinder = + remoteSocketConnectionFinder(timeoutMillis, remoteAddress); + + Finder datagramSocketFinder = + remoteDatagramSocketConnectionFinder(remoteAddress); + + // try DatagramSocket first, by default + Finder[] finders = new Finder[] { datagramSocketFinder, socketFinder }; + + String osName = System.getProperty("os.name"); + if (osName != null && osName.startsWith("Mac")) { + // instead try Socket first, for macOS + finders = new Finder[] { socketFinder, datagramSocketFinder }; + } + + return fromAggregate(finders); + } + + /** + * Returns a finder that tries to determine egress interface by connecting + * to the specified remote address. + * + * @param timeoutMillis give up after this length of time + * @param address the remote address to connect to + * @return finder callback + */ + private Finder remoteSocketConnectionFinder( + final int timeoutMillis, final InetSocketAddress address) { + return new Finder() { + @Override + public NetworkInterface egressInterface() + throws EgressResolutionException { + return fromRemoteSocketConnection(timeoutMillis, address); + } + }; + } + + /** + * Attempt to find the default egress interface on the current system, + * using the specified connection timeout duration and connecting with + * a {@link Socket}. + * + * @param timeoutMillis the amount of time (milliseconds) allowed to + * establish an outbound connection + * @param remoteAddress the address to which a connection will be attempted + * in order to determine which interface is used to + * connect + * @return the egress interface + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface fromRemoteSocketConnection( + int timeoutMillis, InetSocketAddress remoteAddress) + throws EgressResolutionException { + + Socket socket = new Socket(); + + try { + socket.connect(remoteAddress, timeoutMillis); + return fromLocalAddress(socket.getLocalAddress()); + } catch (IOException e) { + throw new EgressResolutionException( + format("Socket connection to [%s]", remoteAddress), e); + } finally { + try { + socket.close(); + } catch (IOException e) { + // ignore; + } + } + } + + private Finder remoteDatagramSocketConnectionFinder( + final InetSocketAddress address) { + return new Finder() { + @Override + public NetworkInterface egressInterface() + throws EgressResolutionException { + return fromRemoteDatagramSocketConnection(address); + } + }; + } + + /** + * Attempt to find the default egress interface on the current system, + * using the specified connection timeout duration and connecting with + * a {@link DatagramSocket}. + * + * @param remoteAddress the address to which a connection will be attempted + * in order to determine which interface is used to + * connect + * @return the egress interface + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface fromRemoteDatagramSocketConnection( + InetSocketAddress remoteAddress) + throws EgressResolutionException { + + DatagramSocket socket = null; + + try { + socket = new DatagramSocket(); + socket.connect(remoteAddress); + return fromLocalAddress(socket.getLocalAddress()); + } catch (IOException e) { + throw new EgressResolutionException( + format("DatagramSocket connection to [%s]", remoteAddress), + e); + } finally { + if (socket != null) { + socket.close(); + } + } + } + + /** + * Attempt to find the default egress interface on the current system, by + * finding a {@link NetworkInterface} that has the specified network + * address. If more than one interface has the specified address, then + * one of them will be selected indeterminately. + * + * @param localAddress the local address which is assigned to an interface + * @return the egress interface + * @throws EgressResolutionException if an egress interface could not be + * determined + * @since 4.2 + */ + public NetworkInterface fromLocalAddress(InetAddress localAddress) + throws EgressResolutionException { + try { + InetAddress unspecifiedIPv4 = InetAddress.getByName("0.0.0.0"); + InetAddress unspecifiedIPv6 = InetAddress.getByName("::"); + + if (localAddress.equals(unspecifiedIPv4) || + localAddress.equals(unspecifiedIPv6)) { + throw new EgressResolutionException( + format("local address [%s] is unspecified", + localAddress)); + } + + NetworkInterface ni = + NetworkInterface.getByInetAddress(localAddress); + + if (ni == null) { + throw new EgressResolutionException(format( + "no interface found with local address [%s]", + localAddress)); + } + + return ni; + } catch (IOException e) { + throw new EgressResolutionException( + format("local address [%s]", localAddress), e); + } + } + + /** + * An exception representing a failure to determine a default egress + * network interface. Please help improve this functionality by + * providing feedback from the {@link #report()} method, if this is not + * working for you. + * + * @since 4.2 + */ + public static class EgressResolutionException extends Exception { + private final List messages = new ArrayList(); + + public EgressResolutionException(String message) { + super(message); + messages.add(message); + } + + public EgressResolutionException(String message, Throwable cause) { + super(message, cause); + messages.add(message); + messages.add(cause.toString()); + } + + public EgressResolutionException(EgressResolutionException[] priors) { + super(Arrays.toString(priors)); + for (EgressResolutionException e : priors) { + messages.add("----------------------------------------------------------------------------"); + messages.addAll(e.messages); + } + } + + public void report() { + reportLine(""); + reportLine("===================================="); + reportLine("| Egress Resolution Failure Report |"); + reportLine("===================================="); + reportLine(""); + reportLine("Please share this report in order to help improve the egress resolution"); + reportLine("mechanism. Also please indicate if you believe that you have a currently"); + reportLine("working network connection."); + reportLine(""); + showProperty("java.version"); + showProperty("java.version.date"); + showProperty("java.runtime.name"); + showProperty("java.runtime.version"); + showProperty("java.vendor"); + showProperty("java.vendor.url"); + showProperty("java.vendor.url.bug"); + showProperty("java.vendor.version"); + showProperty("java.vm.name"); + showProperty("java.vm.vendor"); + showProperty("java.vm.version"); + showProperty("os.arch"); + showProperty("os.name"); + showProperty("os.version"); + + for (String message : messages) { + reportLine(message); + } + } + + protected void reportLine(String line) { + System.out.println(line); + } + + private void showProperty(String key) { + reportLine(key + ": " + System.getProperty(key)); + } + + public Collection getMessages() { + return messages; + } + } + + interface Finder { + NetworkInterface egressInterface() throws EgressResolutionException; + } +} diff --git a/src/main/java/com/fasterxml/uuid/EthernetAddress.java b/src/main/java/com/fasterxml/uuid/EthernetAddress.java index eca40f2..f3222d5 100644 --- a/src/main/java/com/fasterxml/uuid/EthernetAddress.java +++ b/src/main/java/com/fasterxml/uuid/EthernetAddress.java @@ -15,8 +15,11 @@ package com.fasterxml.uuid; +import com.fasterxml.uuid.EgressInterfaceFinder.EgressResolutionException; + +import java.io.IOException; import java.io.Serializable; -import java.net.NetworkInterface; +import java.net.*; import java.security.SecureRandom; import java.util.Enumeration; import java.util.Random; @@ -30,7 +33,7 @@ public class EthernetAddress { private static final long serialVersionUID = 1L; - private final static char[] HEX_CHARS = "0123456789abcdefABCDEF".toCharArray(); + private static final char[] HEX_CHARS = "0123456789abcdefABCDEF".toCharArray(); /** * We may need a random number generator, for creating dummy ethernet @@ -271,9 +274,9 @@ public static EthernetAddress fromInterface() while (en.hasMoreElements()) { NetworkInterface nint = en.nextElement(); if (!nint.isLoopback()) { - byte[] data = nint.getHardwareAddress(); - if (data != null && data.length == 6) { - return new EthernetAddress(data); + EthernetAddress addr = fromInterface(nint); + if (addr != null) { + return addr; } } } @@ -282,11 +285,79 @@ public static EthernetAddress fromInterface() } return null; } - + + /** + * A factory method to return the ethernet address of a specified network interface. + * + * @since 4.2 + */ + public static EthernetAddress fromInterface(NetworkInterface nint) + { + if (nint != null) { + try { + byte[] data = nint.getHardwareAddress(); + if (data != null && data.length == 6) { + return new EthernetAddress(data); + } + } catch (SocketException e) { + // could not get address + } + } + return null; + } + + /** + * Factory method that locates a network interface that has + * a suitable mac address (ethernet cards, and things that + * emulate one), and return that address. It will first try to + * identify an egress interface, and failing that, it will select + * indeterminately from all non-loopback interfaces found. + * Method is meant for accessing an address needed to construct + * generator for time+location based UUID generation method. + * + * @return Ethernet address of one of interfaces system has; + * not including local or loopback addresses; if one exists, + * null if no such interfaces are found. + * + * @since 4.2 + */ + public static EthernetAddress fromPreferredInterface() + { + EthernetAddress egressIfAddress = fromEgressInterface(); + if (egressIfAddress == null) { + return fromInterface(); + } else { + return egressIfAddress; + } + } + + /** + * A factory method that will try to determine the ethernet address of + * the network interface that connects to the default network gateway. + * To do this it will try to open a connection to one of the root DNS + * servers, or barring that, to address 1.1.1.1, or finally if that also + * fails then to IPv6 address "1::1". If a connection can be opened then + * the interface through which that connection is routed is determined + * to be the default egress interface, and the corresponding address of + * that interface will be returned. If all attempts are unsuccessful, + * null will be returned. + * + * @since 4.2 + */ + public static EthernetAddress fromEgressInterface() + { + try { + EgressInterfaceFinder finder = new EgressInterfaceFinder(); + return fromInterface(finder.egressInterface()); + } catch (EgressResolutionException e) { + return null; + } + } + /** * Factory method that can be used to construct a random multicast * address; to be used in cases where there is no "real" ethernet - * address to use. Address to generate should be a multicase address + * address to use. Address to generate should be a multicast address * to avoid accidental collision with real manufacturer-assigned * MAC addresses. *

@@ -301,7 +372,7 @@ public static EthernetAddress constructMulticastAddress() /** * Factory method that can be used to construct a random multicast * address; to be used in cases where there is no "real" ethernet - * address to use. Address to generate should be a multicase address + * address to use. Address to generate should be a multicast address * to avoid accidental collision with real manufacturer-assigned * MAC addresses. *

@@ -420,6 +491,11 @@ public boolean equals(Object o) return ((EthernetAddress) o)._address == _address; } + @Override + public int hashCode() { + return (int) _address ^ (int) (_address >>> 32); + } + /** * Method that compares this EthernetAddress to one passed in as * argument. Comparison is done simply by comparing individual diff --git a/src/main/java/com/fasterxml/uuid/Generators.java b/src/main/java/com/fasterxml/uuid/Generators.java index afffd47..9ab587a 100644 --- a/src/main/java/com/fasterxml/uuid/Generators.java +++ b/src/main/java/com/fasterxml/uuid/Generators.java @@ -22,13 +22,16 @@ import com.fasterxml.uuid.impl.NameBasedGenerator; import com.fasterxml.uuid.impl.RandomBasedGenerator; +import com.fasterxml.uuid.impl.TimeBasedEpochGenerator; +import com.fasterxml.uuid.impl.TimeBasedEpochRandomGenerator; +import com.fasterxml.uuid.impl.TimeBasedReorderedGenerator; import com.fasterxml.uuid.impl.TimeBasedGenerator; /** * Root factory class for constructing UUID generators. - * + * * @author tatu - * + * * @since 3.0 */ public class Generators @@ -39,13 +42,18 @@ public class Generators * synchronization but no external file-based syncing. */ protected static UUIDTimer _sharedTimer; + + /** + * The hardware address of the egress network interface. + */ + protected static EthernetAddress _preferredIfAddr = null; // // Random-based generation /** * Factory method for constructing UUID generator that uses default (shared) * random number generator for constructing UUIDs according to standard - * method number 4. + * version 4. */ public static RandomBasedGenerator randomBasedGenerator() { return randomBasedGenerator(null); @@ -54,7 +62,7 @@ public static RandomBasedGenerator randomBasedGenerator() { /** * Factory method for constructing UUID generator that uses specified * random number generator for constructing UUIDs according to standard - * method number 4. + * version 4. */ public static RandomBasedGenerator randomBasedGenerator(Random rnd) { return new RandomBasedGenerator(rnd); @@ -65,8 +73,8 @@ public static RandomBasedGenerator randomBasedGenerator(Random rnd) { /** * Factory method for constructing UUID generator that uses specified * random number generator for constructing UUIDs according to standard - * method number 5, but without using a namespace. - * Digester to use will be SHA-1 as recommened by UUID spec. + * version 5, but without using a namespace. + * Digester to use will be SHA-1 as recommended by UUID spec. */ public static NameBasedGenerator nameBasedGenerator() { return nameBasedGenerator(null); @@ -75,7 +83,7 @@ public static NameBasedGenerator nameBasedGenerator() { /** * Factory method for constructing UUID generator that uses specified * random number generator for constructing UUIDs according to standard - * method number 5, with specified namespace (or without one if null + * version 5, with specified namespace (or without one if null * is specified). * Digester to use will be SHA-1 as recommened by UUID spec. * @@ -90,7 +98,7 @@ public static NameBasedGenerator nameBasedGenerator(UUID namespace) { /** * Factory method for constructing UUID generator that uses specified * random number generator for constructing UUIDs according to standard - * method number 3 or 5, with specified namespace (or without one if null + * version 3 or 5, with specified namespace (or without one if null * is specified), using specified digester. * If digester is passed as null, a SHA-1 digester will be constructed. * @@ -113,17 +121,142 @@ public static NameBasedGenerator nameBasedGenerator(UUID namespace, MessageDiges return new NameBasedGenerator(namespace, digester, type); } + // // Epoch Time+random generation + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 7 (Unix Epoch time+random based). + *

+ * NOTE: calls within same millisecond produce very similar values; this may be + * unsafe in some environments. + *

+ * No additional external synchronization is used. + */ + public static TimeBasedEpochGenerator timeBasedEpochGenerator() + { + return timeBasedEpochGenerator(null); + } + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 7 (Unix Epoch time+random based), using specified {@link Random} + * number generator. + *

+ * NOTE: calls within same millisecond produce very similar values; this may be + * unsafe in some environments. + *

+ * No additional external synchronization is used. + */ + public static TimeBasedEpochGenerator timeBasedEpochGenerator(Random random) + { + return new TimeBasedEpochGenerator(random); + } + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 7 (Unix Epoch time+random based), using specified {@link Random} + * number generator. + * Timestamp to use is accessed using specified {@link UUIDClock}. + *

+ * NOTE: calls within same millisecond produce very similar values; this may be + * unsafe in some environments. + *

+ * No additional external synchronization is used. + * + * @since 4.3 + */ + public static TimeBasedEpochGenerator timeBasedEpochGenerator(Random random, + UUIDClock clock) + { + return new TimeBasedEpochGenerator(random, clock); + } + + // // Epoch Time+random generation + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 7 (Unix Epoch time+random based). + *

+ * Calls within same millisecond use additional per-call randomness to try to create + * more distinct values, compared to {@link #timeBasedEpochGenerator(Random)} + *

+ * No additional external synchronization is used. + * + * @since 5.1 + */ + public static TimeBasedEpochRandomGenerator timeBasedEpochRandomGenerator() + { + return timeBasedEpochRandomGenerator(null); + } + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 7 (Unix Epoch time+random based), using specified {@link Random} + * number generator. + *

+ * Calls within same millisecond use additional per-call randomness to try to create + * more distinct values, compared to {@link #timeBasedEpochGenerator(Random)} + *

+ * No additional external synchronization is used. + * + * @since 5.0 + */ + public static TimeBasedEpochRandomGenerator timeBasedEpochRandomGenerator(Random random) + { + return new TimeBasedEpochRandomGenerator(random); + } + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 7 (Unix Epoch time+random based), using specified {@link Random} + * number generator. + * Timestamp to use is accessed using specified {@link UUIDClock} + *

+ * Calls within same millisecond use additional per-call randomness to try to create + * more distinct values, compared to {@link #timeBasedEpochGenerator(Random)} + *

+ * No additional external synchronization is used. + * + * @since 5.0 + */ + public static TimeBasedEpochRandomGenerator timeBasedEpochRandomGenerator(Random random, + UUIDClock clock) + { + return new TimeBasedEpochRandomGenerator(random, clock); + } + // // Time+location-based generation + /** + * Factory method for constructing UUID generator that generates UUID using version 1 + * (time+location based). This method will use the ethernet address of the interface + * that routes to the default gateway, or if that cannot be found, then the address of + * an indeterminately selected non-loopback interface. For most simple and common + * networking configurations this will be the most appropriate address to use. The + * default interface is determined by the calling {@link + * EthernetAddress#fromPreferredInterface()} method. Note that this will only + * identify the preferred interface once: if you have a complex network setup where + * your outbound routes/interfaces may change dynamically. If you want your UUIDs to + * accurately reflect a deterministic selection of network interface, you should + * instead use a generator implementation that uses an explicitly specified address, + * such as {@link #timeBasedGenerator(EthernetAddress)}. + * + * @since 4.2 + */ + public static TimeBasedGenerator defaultTimeBasedGenerator() + { + return timeBasedGenerator(preferredInterfaceAddress()); + } + /** * Factory method for constructing UUID generator that generates UUID using - * variant 1 (time+location based). + * version 1 (time+location based). * Since no Ethernet address is passed, a bogus broadcast address will be * constructed for purpose of UUID generation; usually it is better to * instead access one of host's NIC addresses using * {@link EthernetAddress#fromInterface} which will use one of available * MAC (Ethernet) addresses available. - */ + */ public static TimeBasedGenerator timeBasedGenerator() { return timeBasedGenerator(null); @@ -131,7 +264,7 @@ public static TimeBasedGenerator timeBasedGenerator() /** * Factory method for constructing UUID generator that generates UUID using - * variant 1 (time+location based), using specified Ethernet address + * version 1 (time+location based), using specified Ethernet address * as the location part of UUID. * No additional external synchronization is used. */ @@ -142,7 +275,7 @@ public static TimeBasedGenerator timeBasedGenerator(EthernetAddress ethernetAddr /** * Factory method for constructing UUID generator that generates UUID using - * variant 1 (time+location based), using specified Ethernet address + * version 1 (time+location based), using specified Ethernet address * as the location part of UUID, and specified synchronizer (which may add * additional restrictions to guarantee system-wide uniqueness). * @@ -165,7 +298,7 @@ public static TimeBasedGenerator timeBasedGenerator(EthernetAddress ethernetAddr /** * Factory method for constructing UUID generator that generates UUID using - * variant 1 (time+location based), using specified Ethernet address + * version 1 (time+location based), using specified Ethernet address * as the location part of UUID, and specified {@link UUIDTimer} instance * (which includes embedded synchronizer that defines synchronization behavior). */ @@ -178,6 +311,48 @@ public static TimeBasedGenerator timeBasedGenerator(EthernetAddress ethernetAddr return new TimeBasedGenerator(ethernetAddress, timer); } + // // DB Locality Time+location-based generation + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 6 (time+location based, reordered for DB locality). Since no Ethernet + * address is passed, a bogus broadcast address will be constructed for purpose + * of UUID generation; usually it is better to instead access one of host's NIC + * addresses using {@link EthernetAddress#fromInterface} which will use one of + * available MAC (Ethernet) addresses available. + */ + public static TimeBasedReorderedGenerator timeBasedReorderedGenerator() + { + return timeBasedReorderedGenerator(null); + } + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 6 (time+location based, reordered for DB locality), using specified + * Ethernet address as the location part of UUID. No additional external + * synchronization is used. + */ + public static TimeBasedReorderedGenerator timeBasedReorderedGenerator(EthernetAddress ethernetAddress) + { + return timeBasedReorderedGenerator(ethernetAddress, (UUIDTimer) null); + } + + /** + * Factory method for constructing UUID generator that generates UUID using + * version 6 (time+location based, reordered for DB locality), using specified + * Ethernet address as the location part of UUID, and specified + * {@link UUIDTimer} instance (which includes embedded synchronizer that defines + * synchronization behavior). + */ + public static TimeBasedReorderedGenerator timeBasedReorderedGenerator(EthernetAddress ethernetAddress, + UUIDTimer timer) + { + if (timer == null) { + timer = sharedTimer(); + } + return new TimeBasedReorderedGenerator(ethernetAddress, timer); + } + /* /********************************************************************** /* Internal methods @@ -195,4 +370,12 @@ private static synchronized UUIDTimer sharedTimer() } return _sharedTimer; } + + private static synchronized EthernetAddress preferredInterfaceAddress() + { + if (_preferredIfAddr == null) { + _preferredIfAddr = EthernetAddress.fromPreferredInterface(); + } + return _preferredIfAddr; + } } diff --git a/src/main/java/com/fasterxml/uuid/Jug.java b/src/main/java/com/fasterxml/uuid/Jug.java index 1e26ab5..b019447 100644 --- a/src/main/java/com/fasterxml/uuid/Jug.java +++ b/src/main/java/com/fasterxml/uuid/Jug.java @@ -31,6 +31,9 @@ public class Jug TYPES.put("time-based", "t"); TYPES.put("random-based", "r"); TYPES.put("name-based", "n"); + TYPES.put("reordered-time-based", "o"); // Version 6 + TYPES.put("epoch-time-based", "e"); // Version 7 + TYPES.put("random-epoch-time-based", "m"); // Version 7 but more random } protected final static HashMap OPTIONS = new HashMap(); @@ -44,7 +47,7 @@ public class Jug OPTIONS.put("verbose", "v"); } - protected static void printUsage() + protected void printUsage() { String clsName = Jug.class.getName(); System.err.println("Usage: java "+clsName+" [options] type"); @@ -67,10 +70,12 @@ protected static void printUsage() System.err.println("And type is one of:"); System.err.println(" time-based / t: generate UUID based on current time and optional\n location information (defined with -e option)"); System.err.println(" random-based / r: generate UUID based on the default secure random number generator"); - System.err.println(" name-based / n: generate UUID based on the na the default secure random number generator"); + System.err.println(" name-based / n: generate UUID based on MD5 hash of given String ('name')"); + System.err.println(" reordered-time-based / o: generate UUID based on current time and optional\n location information (defined with -e option)"); + System.err.println(" epoch-based / e: generate UUID based on current time (as 'epoch') and random number"); } - private static void printMap(Map m, PrintStream out, boolean option) + private void printMap(Map m, PrintStream out, boolean option) { int i = 0; int len = m.size(); @@ -97,6 +102,10 @@ private static void printMap(Map m, PrintStream out, boolean opti public static void main(String[] args) { + new Jug().run(args); + } + + public void run(String[] args) { if (args.length == 0) { printUsage(); return; @@ -118,7 +127,7 @@ public static void main(String[] args) if (tmp == null) { if (!TYPES.containsValue(type)) { System.err.println("Unrecognized UUID generation type '"+ - type+"'; currently available ones are:"); + type+"'; currently available ones are:"); printMap(TYPES, System.err, false); System.err.println(); System.exit(1); @@ -131,7 +140,7 @@ public static void main(String[] args) NoArgGenerator noArgGenerator = null; // random- or time-based StringArgGenerator nameArgGenerator = null; // name-based - + for (int i = 0; i < count; ++i) { String opt = args[i]; @@ -165,46 +174,46 @@ public static void main(String[] args) try { String next; switch (option) { - case 'c': - // Need a number now: - next = args[++i]; - try { - genCount = Integer.parseInt(next); - } catch (NumberFormatException nex) { - System.err.println("Invalid number argument for option '"+opt+"', exiting."); - System.exit(1); - } - if (genCount < 1) { - System.err.println("Invalid number argument for option '"+opt+"'; negative numbers not allowed, ignoring (defaults to 1)."); - } - break; - case 'e': - // Need the ethernet address: - next = args[++i]; - try { - addr = EthernetAddress.valueOf(next); - } catch (NumberFormatException nex) { - System.err.println("Invalid ethernet address for option '"+opt+"', error: "+nex.toString()); - System.exit(1); - } - break; - case 'h': - printUsage(); - return; - case 'n': - // Need the name - name = args[++i]; - break; - case 'p': // performance: - performance = true; - break; - case 's': - // Need the namespace id - nameSpace = args[++i]; - break; - case 'v': - verbose = true; - break; + case 'c': + // Need a number now: + next = args[++i]; + try { + genCount = Integer.parseInt(next); + } catch (NumberFormatException nex) { + System.err.println("Invalid number argument for option '"+opt+"', exiting."); + System.exit(1); + } + if (genCount < 1) { + System.err.println("Invalid number argument for option '"+opt+"'; negative numbers not allowed, ignoring (defaults to 1)."); + } + break; + case 'e': + // Need the ethernet address: + next = args[++i]; + try { + addr = EthernetAddress.valueOf(next); + } catch (NumberFormatException nex) { + System.err.println("Invalid ethernet address for option '"+opt+"', error: "+nex.toString()); + System.exit(1); + } + break; + case 'h': + printUsage(); + return; + case 'n': + // Need the name + name = args[++i]; + break; + case 'p': // performance: + performance = true; + break; + case 's': + // Need the namespace id + nameSpace = args[++i]; + break; + case 'v': + verbose = true; + break; } } catch (IndexOutOfBoundsException ie) { // We get here when an arg is missing... @@ -222,55 +231,80 @@ public static void main(String[] args) boolean usesRnd = false; switch (typeC) { - case 't': // time-based - usesRnd = true; - // No address specified? Need a dummy one... - if (addr == null) { - if (verbose) { - System.out.print("(no address specified, generating dummy address: "); + case 't': // time-based + case 'o': // reordered-time-based (Version 6) + // 30-Jun-2022, tatu: Is this true? My former self must have had his + // reasons so leaving as is but... odd. + usesRnd = true; + // No address specified? Need a dummy one... + if (addr == null) { + if (verbose) { + System.out.print("(no address specified, generating dummy address: "); + } + addr = EthernetAddress.constructMulticastAddress(new java.util.Random(System.currentTimeMillis())); + if (verbose) { + System.out.print(addr.toString()); + System.out.println(")"); + } } - addr = EthernetAddress.constructMulticastAddress(new java.util.Random(System.currentTimeMillis())); - if (verbose) { - System.out.print(addr.toString()); - System.out.println(")"); + noArgGenerator = (typeC == 't') + ? Generators.timeBasedGenerator(addr) + : Generators.timeBasedReorderedGenerator(addr); + break; + case 'r': // random-based + usesRnd = true; + { + SecureRandom r = new SecureRandom(); + if (verbose) { + System.out.print("(using secure random generator, info = '"+r.getProvider().getInfo()+"')"); + } + noArgGenerator = Generators.randomBasedGenerator(r); } - } - noArgGenerator = Generators.timeBasedGenerator(addr); - break; - case 'r': // random-based - usesRnd = true; - { - SecureRandom r = new SecureRandom(); - if (verbose) { - System.out.print("(using secure random generator, info = '"+r.getProvider().getInfo()+"')"); + break; + case 'e': // epoch-time-based + usesRnd = true; + { + SecureRandom r = new SecureRandom(); + if (verbose) { + System.out.print("(using secure random generator, info = '"+r.getProvider().getInfo()+"')"); + } + noArgGenerator = Generators.timeBasedEpochGenerator(r); } - noArgGenerator = Generators.randomBasedGenerator(r); - } - break; - case 'n': // name-based - if (name == null) { - System.err.println("--name-space (-s) - argument missing when using method that requires it, exiting."); - System.exit(1); - } - if (name == null) { - System.err.println("--name (-n) - argument missing when using method that requires it, exiting."); - System.exit(1); - } - if (typeC == 'n') { - String orig = nameSpace; - nameSpace = nameSpace.toLowerCase(); - if (nameSpace.equals("url")) { - nsUUID = NameBasedGenerator.NAMESPACE_URL; - } else if (nameSpace.equals("dns")) { - nsUUID = NameBasedGenerator.NAMESPACE_DNS; - } else { - System.err.println("Unrecognized namespace '"+orig - +"'; only DNS and URL allowed for name-based generation."); + break; + case 'm': // random-epoch-time-based + usesRnd = true; + { + SecureRandom r = new SecureRandom(); + if (verbose) { + System.out.print("(using secure random generator, info = '"+r.getProvider().getInfo()+"')"); + } + noArgGenerator = Generators.timeBasedEpochRandomGenerator(r); + } + break; + case 'n': // name-based + if (nameSpace == null) { + System.err.println("--name-space (-s) - argument missing when using method that requires it, exiting."); System.exit(1); } - } - nameArgGenerator = Generators.nameBasedGenerator(nsUUID); - break; + if (name == null) { + System.err.println("--name (-n) - argument missing when using method that requires it, exiting."); + System.exit(1); + } + if (typeC == 'n') { + String orig = nameSpace; + nameSpace = nameSpace.toLowerCase(); + if (nameSpace.equals("url")) { + nsUUID = NameBasedGenerator.NAMESPACE_URL; + } else if (nameSpace.equals("dns")) { + nsUUID = NameBasedGenerator.NAMESPACE_DNS; + } else { + System.err.println("Unrecognized namespace '"+orig + +"'; only DNS and URL allowed for name-based generation."); + System.exit(1); + } + } + nameArgGenerator = Generators.nameBasedGenerator(nsUUID); + break; } // And then let's rock: @@ -278,9 +312,8 @@ public static void main(String[] args) System.out.println(); } - /* When measuring performance, make sure that the random number - * generator is initialized prior to measurements... - */ + // When measuring performance, make sure that the random number + // generator is initialized prior to measurements... long now = 0L; if (performance) { @@ -300,7 +333,7 @@ public static void main(String[] args) for (int i = 0; i < genCount; ++i) { UUID uuid = (nameArgGenerator == null) ? - noArgGenerator.generate() : nameArgGenerator.generate(name); + noArgGenerator.generate() : nameArgGenerator.generate(name); // lgtm [java/dereferenced-value-may-be-null] if (verbose) { System.out.print("UUID: "); } diff --git a/src/main/java/com/fasterxml/uuid/NoArgGenerator.java b/src/main/java/com/fasterxml/uuid/NoArgGenerator.java index 986bb45..c3547a2 100644 --- a/src/main/java/com/fasterxml/uuid/NoArgGenerator.java +++ b/src/main/java/com/fasterxml/uuid/NoArgGenerator.java @@ -4,11 +4,16 @@ /** * Intermediate base class for UUID generators that do not take arguments for individual - * calls. This includes random and time-based variants, but not name-based ones. + * calls. This includes random and time-based versions, but not name-based ones. * * @since 3.0 */ public abstract class NoArgGenerator extends UUIDGenerator { + /** + * Method for generating a {@link UUID}. + * + * @return Newly generated {@link UUID} + */ public abstract UUID generate(); } diff --git a/src/main/java/com/fasterxml/uuid/UUIDClock.java b/src/main/java/com/fasterxml/uuid/UUIDClock.java index b655d37..28493c6 100644 --- a/src/main/java/com/fasterxml/uuid/UUIDClock.java +++ b/src/main/java/com/fasterxml/uuid/UUIDClock.java @@ -26,6 +26,15 @@ */ public class UUIDClock { + private static final UUIDClock DEFAULT = new UUIDClock(); + + /** + * @since 4.3 + */ + public static UUIDClock systemTimeClock() { + return DEFAULT; + } + /** * Returns the current time in milliseconds. */ diff --git a/src/main/java/com/fasterxml/uuid/UUIDComparator.java b/src/main/java/com/fasterxml/uuid/UUIDComparator.java index 1452887..7995ac9 100644 --- a/src/main/java/com/fasterxml/uuid/UUIDComparator.java +++ b/src/main/java/com/fasterxml/uuid/UUIDComparator.java @@ -6,13 +6,13 @@ /** * Default {@link java.util.UUID} comparator is not very useful, since * it just does blind byte-by-byte comparison which does not work well - * for time+location - based UUIDs. Additionally it also uses signed + * for time+location - based UUIDs. Additionally, it also uses signed * comparisons for longs which can lead to unexpected behavior * This comparator does implement proper lexical ordering: starting with * type (different types are collated * separately), followed by time and location (for time/location based), * and simple lexical (byte-by-byte) ordering for name/hash and random - * variants. + * versions. * * @author tatu */ @@ -36,7 +36,7 @@ public static int staticCompare(UUID u1, UUID u2) if (diff != 0) { return diff; } - // Second: for time-based variant, order by time stamp: + // Second: for time-based version, order by time stamp: if (type == UUIDType.TIME_BASED.raw()) { diff = compareULongs(u1.timestamp(), u2.timestamp()); if (diff == 0) { diff --git a/src/main/java/com/fasterxml/uuid/UUIDGenerator.java b/src/main/java/com/fasterxml/uuid/UUIDGenerator.java index 3db74f8..ebd1ec3 100644 --- a/src/main/java/com/fasterxml/uuid/UUIDGenerator.java +++ b/src/main/java/com/fasterxml/uuid/UUIDGenerator.java @@ -42,8 +42,36 @@ protected UUIDGenerator() { } */ /** - * Accessor for determining type of UUIDs (variant) that this + * Accessor for determining type of UUIDs (version) that this * generator instance will produce. */ public abstract UUIDType getType(); + + /* + /********************************************************** + /* Helper methods for implementations + /********************************************************** + */ + + protected final static long _toLong(byte[] buffer, int offset) + { + long l1 = _toInt(buffer, offset); + long l2 = _toInt(buffer, offset+4); + long l = (l1 << 32) + ((l2 << 32) >>> 32); + return l; + } + + protected final static long _toInt(byte[] buffer, int offset) + { + return (buffer[offset] << 24) + + ((buffer[++offset] & 0xFF) << 16) + + ((buffer[++offset] & 0xFF) << 8) + + (buffer[++offset] & 0xFF); + } + + protected final static long _toShort(byte[] buffer, int offset) + { + return ((buffer[offset] & 0xFF) << 8) + + (buffer[++offset] & 0xFF); + } } diff --git a/src/main/java/com/fasterxml/uuid/UUIDTimer.java b/src/main/java/com/fasterxml/uuid/UUIDTimer.java index e332c2f..203f934 100644 --- a/src/main/java/com/fasterxml/uuid/UUIDTimer.java +++ b/src/main/java/com/fasterxml/uuid/UUIDTimer.java @@ -18,11 +18,9 @@ import java.io.*; import java.util.*; +import com.fasterxml.uuid.impl.LoggerFacade; import com.fasterxml.uuid.impl.UUIDUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * UUIDTimer produces the time stamps required for time-based UUIDs. * It works as outlined in the UUID specification, with following @@ -75,9 +73,8 @@ */ public class UUIDTimer { + private final LoggerFacade _logger = LoggerFacade.getLogger(getClass()); - private static final Logger logger = LoggerFactory.getLogger(UUIDTimer.class); - // // // Constants /** @@ -172,7 +169,7 @@ public class UUIDTimer public UUIDTimer(Random rnd, TimestampSynchronizer sync) throws IOException { - this(rnd, sync, new UUIDClock()); + this(rnd, sync, UUIDClock.systemTimeClock()); } /** @@ -236,7 +233,9 @@ public int getClockSequence() { /** * Method that constructs unique timestamp suitable for use for * constructing UUIDs. Default implementation is fully synchronized; - * sub-classes may choose to implemented alternate strategies + * sub-classes may choose to implement alternate strategies but + * due to existing usage and expectations should also be synchronized + * unless usage is specifically known not to require it. * * @return 64-bit timestamp to use for constructing UUID */ @@ -247,7 +246,8 @@ public synchronized long getTimestamp() * independent of whether we can use it: */ if (systime < _lastSystemTimestamp) { - logger.warn("System time going backwards! (got value {}, last {}", systime, _lastSystemTimestamp); + _logger.warn("System time going backwards! (got value %d, last %d)", + systime, _lastSystemTimestamp); // Let's write it down, still _lastSystemTimestamp = systime; } @@ -267,7 +267,7 @@ public synchronized long getTimestamp() long origTime = systime; systime = _lastUsedTimestamp + 1L; - logger.warn("Timestamp over-run: need to reinitialize random sequence"); + _logger.warn("Timestamp over-run: need to reinitialize random sequence"); /* Clock counter is now at exactly the multiplier; no use * just anding its value. So, we better get some random @@ -320,6 +320,20 @@ public synchronized long getTimestamp() return systime; } + /** + * Converts a UUID v1 or v6 timestamp (where unit is 100 nanoseconds), + * to Unix epoch timestamp (milliseconds since 01-Jan-1970 UTC) + * + * @param timestamp Timestamp used to create UUID versions 1 and 6 + * + * @return Unix epoch timestamp + * + * @since 5.1 + */ + public static long timestampToEpoch(long timestamp) { + return (timestamp - kClockOffset) / kClockMultiplierL; + } + /* /********************************************************************** /* Test-support methods @@ -358,7 +372,7 @@ protected final void getAndSetTimestamp(byte[] uuidBytes) /********************************************************************** */ - private final static int MAX_WAIT_COUNT = 50; + private static final int MAX_WAIT_COUNT = 50; /** * Simple utility method to use to wait for couple of milliseconds, @@ -371,7 +385,7 @@ protected final void getAndSetTimestamp(byte[] uuidBytes) * @param actDiff Number of milliseconds to wait for from current * time point, to catch up */ - protected static void slowDown(long startTime, long actDiff) + protected void slowDown(long startTime, long actDiff) { /* First, let's determine how long we'd like to wait. * This is based on how far ahead are we as of now. @@ -388,7 +402,7 @@ protected static void slowDown(long startTime, long actDiff) } else { delay = 5L; } - logger.warn("Need to wait for {} milliseconds; virtual clock advanced too far in the future", delay); + _logger.warn("Need to wait for %d milliseconds; virtual clock advanced too far in the future", delay); long waitUntil = startTime + delay; int counter = 0; do { diff --git a/src/main/java/com/fasterxml/uuid/UUIDType.java b/src/main/java/com/fasterxml/uuid/UUIDType.java index 25a5333..1794b86 100644 --- a/src/main/java/com/fasterxml/uuid/UUIDType.java +++ b/src/main/java/com/fasterxml/uuid/UUIDType.java @@ -2,7 +2,7 @@ /** * Enumeration of different flavors of UUIDs: 5 specified by specs - * (RFC-4122) + * (RFC-9562) * and one * virtual entry ("UNKNOWN") to represent invalid one that consists of * all zero bites @@ -13,8 +13,10 @@ public enum UUIDType { NAME_BASED_MD5(3), RANDOM_BASED(4), NAME_BASED_SHA1(5), - UNKNOWN(0) - ; + TIME_BASED_REORDERED(6), + TIME_BASED_EPOCH(7), + FREE_FORM(8), + UNKNOWN(0); private final int _raw; diff --git a/src/main/java/com/fasterxml/uuid/ext/package-info.java b/src/main/java/com/fasterxml/uuid/ext/package-info.java new file mode 100644 index 0000000..d794d93 --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/ext/package-info.java @@ -0,0 +1,5 @@ +/** +Package that contains optional Java UUID Generator classes, +ones that depend on optional external packages (like slf4j) +*/ +package com.fasterxml.uuid.ext; diff --git a/src/main/java/com/fasterxml/uuid/ext/package.html b/src/main/java/com/fasterxml/uuid/ext/package.html deleted file mode 100644 index c8eac25..0000000 --- a/src/main/java/com/fasterxml/uuid/ext/package.html +++ /dev/null @@ -1,11 +0,0 @@ - -Package that contains optional Java UUID Generator classes; classes that: -

    -
  • Depend on optional external packages; like log4j or java.util.logging - -based Logger adapters (java.util.logging itself was added in JDK 1.4) -
  • -
-

-Otherwise base JDK version requirement for these classes is 1.4. -

- diff --git a/src/main/java/com/fasterxml/uuid/impl/GeneratorImplBase.java b/src/main/java/com/fasterxml/uuid/impl/GeneratorImplBase.java deleted file mode 100644 index 6a2feb7..0000000 --- a/src/main/java/com/fasterxml/uuid/impl/GeneratorImplBase.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.fasterxml.uuid.impl; - -/** - * Shared base class for various UUID generator implementations. - */ -public class GeneratorImplBase -{ -} diff --git a/src/main/java/com/fasterxml/uuid/impl/LazyRandom.java b/src/main/java/com/fasterxml/uuid/impl/LazyRandom.java new file mode 100644 index 0000000..40a8ba2 --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/impl/LazyRandom.java @@ -0,0 +1,31 @@ +package com.fasterxml.uuid.impl; + +import java.security.SecureRandom; + +/** + * Trivial helper class that uses class loading as synchronization + * mechanism for lazy instantiation of the shared secure random + * instance. + *

+ * Since 5.0 has been lazily created to avoid issues with native-generation + * tools like Graal. + */ +public final class LazyRandom +{ + private static final Object lock = new Object(); + private static volatile SecureRandom shared; + + public static SecureRandom sharedSecureRandom() { + if (shared != null) { + return shared; + } + synchronized (lock) { + SecureRandom result = shared; + if (result == null) { + shared = result = new SecureRandom(); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/fasterxml/uuid/impl/LoggerFacade.java b/src/main/java/com/fasterxml/uuid/impl/LoggerFacade.java new file mode 100644 index 0000000..3aaf077 --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/impl/LoggerFacade.java @@ -0,0 +1,72 @@ +package com.fasterxml.uuid.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wrapper we (only) need to support CLI usage (see {@link com.fasterxml.uuid.Jug} + * wherein we do not actually have logger package included; in which case we + * will print warning(s) out to {@code System.err}. + * For normal embedded usage no benefits, except if someone forgot their SLF4j API + * package. :) + * + * @since 4.1 + */ +public class LoggerFacade { + private final Class _forClass; + + private WrappedLogger _logger; + + private LoggerFacade(Class forClass) { + _forClass = forClass; + } + + public static LoggerFacade getLogger(Class forClass) { + return new LoggerFacade(forClass); + } + + public void warn(String msg) { + _warn(msg); + } + + public void warn(String msg, Object arg) { + _warn(String.format(msg, arg)); + } + + public void warn(String msg, Object arg, Object arg2) { + _warn(String.format(msg, arg, arg2)); + } + + private synchronized void _warn(String message) { + if (_logger == null) { + _logger = WrappedLogger.logger(_forClass); + } + _logger.warn(message); + } + + private static class WrappedLogger { + private final Logger _logger; + + private WrappedLogger(Logger l) { + _logger = l; + } + + public static WrappedLogger logger(Class forClass) { + // Why all these contortions? To support case where Slf4j API missing + // (or, if it ever fails for not having impl) to just print to STDERR + try { + return new WrappedLogger(LoggerFactory.getLogger(forClass)); + } catch (Throwable t) { + return new WrappedLogger(null); + } + } + + public void warn(String message) { + if (_logger != null) { + _logger.warn(message); + } else { + System.err.println("WARN: "+message); + } + } + } +} diff --git a/src/main/java/com/fasterxml/uuid/impl/NameBasedGenerator.java b/src/main/java/com/fasterxml/uuid/impl/NameBasedGenerator.java index a7bfb7c..096d32f 100644 --- a/src/main/java/com/fasterxml/uuid/impl/NameBasedGenerator.java +++ b/src/main/java/com/fasterxml/uuid/impl/NameBasedGenerator.java @@ -1,18 +1,16 @@ package com.fasterxml.uuid.impl; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.UUID; import com.fasterxml.uuid.StringArgGenerator; import com.fasterxml.uuid.UUIDType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Implementation of UUID generator that uses one of name-based generation methods - * (variants 3 (MD5) and 5 (SHA1)). + * (versions 3 (MD5) and 5 (SHA1)). *

* As all JUG provided implementations, this generator is fully thread-safe; access * to digester is synchronized as necessary. @@ -21,14 +19,10 @@ */ public class NameBasedGenerator extends StringArgGenerator { + public final static Charset _utf8 = StandardCharsets.UTF_8; + + private final LoggerFacade _logger = LoggerFacade.getLogger(getClass()); - private static final Logger logger = LoggerFactory.getLogger(NameBasedGenerator.class); - - public final static Charset _utf8; - static { - _utf8 = Charset.forName("UTF-8"); - } - /** * Namespace used when name is a DNS name. */ @@ -78,14 +72,13 @@ public class NameBasedGenerator extends StringArgGenerator * Note that this argument is optional; if no namespace is needed * (for example when name includes namespace prefix), null may be passed. * @param digester Hashing algorithm to use. - - */ + */ public NameBasedGenerator(UUID namespace, MessageDigest digester, UUIDType type) { _namespace = namespace; // And default digester SHA-1 if (digester == null) { - + throw new IllegalArgumentException("Digester not optional: cannot pass `null`"); } if (type == null) { String typeStr = digester.getAlgorithm(); @@ -96,7 +89,7 @@ public NameBasedGenerator(UUID namespace, MessageDigest digester, UUIDType type) } else { // Hmmh... error out? Let's default to SHA-1, but log a warning type = UUIDType.NAME_BASED_SHA1; - logger.warn("Could not determine type of Digester from '{}'; assuming 'SHA-1' type", typeStr); + _logger.warn("Could not determine type of Digester from '%s'; assuming 'SHA-1' type", typeStr); } } _digester = digester; @@ -123,7 +116,7 @@ public NameBasedGenerator(UUID namespace, MessageDigest digester, UUIDType type) @Override public UUID generate(String name) { - // !!! TODO: 14-Oct-2010, tatu: can repurpose faster UTF-8 encoding from Jackson + // !!! TODO: 14-Oct-2010, tatu: could re-purpose faster UTF-8 encoding from Jackson return generate(name.getBytes(_utf8)); } diff --git a/src/main/java/com/fasterxml/uuid/impl/RandomBasedGenerator.java b/src/main/java/com/fasterxml/uuid/impl/RandomBasedGenerator.java index 2b91fe9..241b2ed 100644 --- a/src/main/java/com/fasterxml/uuid/impl/RandomBasedGenerator.java +++ b/src/main/java/com/fasterxml/uuid/impl/RandomBasedGenerator.java @@ -23,12 +23,6 @@ */ public class RandomBasedGenerator extends NoArgGenerator { - /** - * Default shared random number generator, used if no random number generator - * is explicitly specified for instance - */ - protected static Random _sharedRandom = null; - /** * Random number generator that this generator uses. */ @@ -40,7 +34,7 @@ public class RandomBasedGenerator extends NoArgGenerator * so let's use that knowledge to our benefit. */ protected final boolean _secureRandom; - + /** * @param rnd Random number generator to use for generating UUIDs; if null, * shared default generator is used. Note that it is strongly recommend to @@ -51,10 +45,8 @@ public RandomBasedGenerator(Random rnd) { if (rnd == null) { rnd = LazyRandom.sharedSecureRandom(); - _secureRandom = true; - } else { - _secureRandom = (rnd instanceof SecureRandom); } + _secureRandom = (rnd instanceof SecureRandom); _random = rnd; } @@ -76,9 +68,9 @@ public RandomBasedGenerator(Random rnd) @Override public UUID generate() { - /* 14-Oct-2010, tatu: Surprisingly, variant for reading byte array is - * tad faster for SecureRandom... so let's use that then - */ + // 14-Oct-2010, tatu: Surprisingly, variant for reading byte array is + // tad faster for SecureRandom... so let's use that then + long r1, r2; if (_secureRandom) { @@ -92,46 +84,4 @@ public UUID generate() } return UUIDUtil.constructUUID(UUIDType.RANDOM_BASED, r1, r2); } - - /* - /********************************************************************** - /* Internal methods - /********************************************************************** - */ - - private final static long _toLong(byte[] buffer, int offset) - { - long l1 = _toInt(buffer, offset); - long l2 = _toInt(buffer, offset+4); - long l = (l1 << 32) + ((l2 << 32) >>> 32); - return l; - } - - private final static long _toInt(byte[] buffer, int offset) - { - return (buffer[offset] << 24) - + ((buffer[++offset] & 0xFF) << 16) - + ((buffer[++offset] & 0xFF) << 8) - + (buffer[++offset] & 0xFF); - } - - /* - /********************************************************************** - /* Helper classes - /********************************************************************** - */ - - /** - * Trivial helper class that uses class loading as synchronization - * mechanism for lazy instantation of the shared secure random - * instance. - */ - private final static class LazyRandom - { - private final static SecureRandom shared = new SecureRandom(); - - public static SecureRandom sharedSecureRandom() { - return shared; - } - } } diff --git a/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java b/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java new file mode 100644 index 0000000..42157ce --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java @@ -0,0 +1,147 @@ +package com.fasterxml.uuid.impl; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.fasterxml.uuid.NoArgGenerator; +import com.fasterxml.uuid.UUIDClock; +import com.fasterxml.uuid.UUIDType; + +/** + * Implementation of UUID generator that uses time/location based generation + * method field from the Unix Epoch timestamp source - the number of + * milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. + * This is usually referred to as "Version 7". + *

+ * As all JUG provided implementations, this generator is fully thread-safe. + * Additionally it can also be made externally synchronized with other instances + * (even ones running on other JVMs); to do this, use + * {@link com.fasterxml.uuid.ext.FileBasedTimestampSynchronizer} (or + * equivalent). + * + * @since 4.1 + */ +public class TimeBasedEpochGenerator extends NoArgGenerator +{ + private static final int ENTROPY_BYTE_LENGTH = 10; + + /* + /********************************************************************** + /* Configuration + /********************************************************************** + */ + + /** + * Random number generator that this generator uses. + */ + protected final Random _random; + + /** + * Underlying {@link UUIDClock} used for accessing current time, to use for + * generation. + * + * @since 4.3 + */ + protected final UUIDClock _clock; + + private long _lastTimestamp = -1; + private final byte[] _lastEntropy = new byte[ENTROPY_BYTE_LENGTH]; + private final Lock lock = new ReentrantLock(); + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + /** + * @param rnd Random number generator to use for generating UUIDs; if null, + * shared default generator is used. Note that it is strongly recommend to + * use a good (pseudo) random number generator; for example, JDK's + * {@link SecureRandom}. + */ + public TimeBasedEpochGenerator(Random rnd) { + this(rnd, UUIDClock.systemTimeClock()); + } + + /** + * @param rnd Random number generator to use for generating UUIDs; if null, + * shared default generator is used. Note that it is strongly recommend to + * use a good (pseudo) random number generator; for example, JDK's + * {@link SecureRandom}. + * @param clock clock Object used for accessing current time to use for generation + */ + public TimeBasedEpochGenerator(Random rnd, UUIDClock clock) + { + if (rnd == null) { + rnd = LazyRandom.sharedSecureRandom(); + } + _random = rnd; + _clock = clock; + } + + /* + /********************************************************************** + /* Access to config + /********************************************************************** + */ + + @Override + public UUIDType getType() { return UUIDType.TIME_BASED_EPOCH; } + + /* + /********************************************************************** + /* UUID generation + /********************************************************************** + */ + + @Override + public UUID generate() + { + return construct(_clock.currentTimeMillis()); + } + + /** + * Method that will construct actual {@link UUID} instance for given + * unix epoch timestamp: called by {@link #generate()} but may alternatively be + * called directly to construct an instance with known timestamp. + * NOTE: calling this method directly produces somewhat distinct UUIDs as + * "entropy" value is still generated as necessary to avoid producing same + * {@link UUID} even if same timestamp is being passed. + * + * @param rawTimestamp unix epoch millis + * + * @return unix epoch time based UUID + * + * @since 4.3 + */ + public UUID construct(long rawTimestamp) + { + lock.lock(); + try { + if (rawTimestamp == _lastTimestamp) { + boolean c = true; + for (int i = ENTROPY_BYTE_LENGTH - 1; i >= 0; i--) { + if (c) { + byte temp = _lastEntropy[i]; + temp = (byte) (temp + 0x01); + c = _lastEntropy[i] == (byte) 0xff; + _lastEntropy[i] = temp; + } + } + if (c) { + throw new IllegalStateException("overflow on same millisecond"); + } + } else { + _lastTimestamp = rawTimestamp; + _random.nextBytes(_lastEntropy); + } + return UUIDUtil.constructUUID(UUIDType.TIME_BASED_EPOCH, (rawTimestamp << 16) | _toShort(_lastEntropy, 0), _toLong(_lastEntropy, 2)); + } finally { + lock.unlock(); + } + } +} diff --git a/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochRandomGenerator.java b/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochRandomGenerator.java new file mode 100644 index 0000000..53b9cbc --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochRandomGenerator.java @@ -0,0 +1,129 @@ +package com.fasterxml.uuid.impl; + +import com.fasterxml.uuid.NoArgGenerator; +import com.fasterxml.uuid.UUIDClock; +import com.fasterxml.uuid.UUIDType; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Implementation of UUID generator that uses time/location based generation + * method field from the Unix Epoch timestamp source - the number of + * milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. + * This is usually referred to as "Version 7". + * In addition to that random part is regenerated for every new UUID. + * This removes possibilities to have almost similar UUID, when calls + * to generate are made within same millisecond. + *

+ * As all JUG provided implementations, this generator is fully thread-safe. + * Additionally it can also be made externally synchronized with other instances + * (even ones running on other JVMs); to do this, use + * {@link com.fasterxml.uuid.ext.FileBasedTimestampSynchronizer} (or + * equivalent). + * + * @since 5.0 + */ +public class TimeBasedEpochRandomGenerator extends NoArgGenerator +{ + private static final int ENTROPY_BYTE_LENGTH = 10; + + /* + /********************************************************************** + /* Configuration + /********************************************************************** + */ + + /** + * Random number generator that this generator uses. + */ + protected final Random _random; + + /** + * Underlying {@link UUIDClock} used for accessing current time, to use for + * generation. + */ + protected final UUIDClock _clock; + + private final byte[] _lastEntropy = new byte[ENTROPY_BYTE_LENGTH]; + private final Lock lock = new ReentrantLock(); + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + /** + * @param rnd Random number generator to use for generating UUIDs; if null, + * shared default generator is used. Note that it is strongly recommend to + * use a good (pseudo) random number generator; for example, JDK's + * {@link SecureRandom}. + */ + public TimeBasedEpochRandomGenerator(Random rnd) { + this(rnd, UUIDClock.systemTimeClock()); + } + + /** + * @param rnd Random number generator to use for generating UUIDs; if null, + * shared default generator is used. Note that it is strongly recommend to + * use a good (pseudo) random number generator; for example, JDK's + * {@link SecureRandom}. + * @param clock clock Object used for accessing current time to use for generation + */ + public TimeBasedEpochRandomGenerator(Random rnd, UUIDClock clock) + { + if (rnd == null) { + rnd = LazyRandom.sharedSecureRandom(); + } + _random = rnd; + _clock = clock; + } + + /* + /********************************************************************** + /* Access to config + /********************************************************************** + */ + + @Override + public UUIDType getType() { return UUIDType.TIME_BASED_EPOCH; } + + /* + /********************************************************************** + /* UUID generation + /********************************************************************** + */ + + @Override + public UUID generate() + { + return construct(_clock.currentTimeMillis()); + } + + /** + * Method that will construct actual {@link UUID} instance for given + * unix epoch timestamp: called by {@link #generate()} but may alternatively be + * called directly to construct an instance with known timestamp. + * NOTE: calling this method directly produces somewhat distinct UUIDs as + * "entropy" value is still generated as necessary to avoid producing same + * {@link UUID} even if same timestamp is being passed. + * + * @param rawTimestamp unix epoch millis + * + * @return unix epoch time based UUID + */ + public UUID construct(long rawTimestamp) + { + lock.lock(); + try { + _random.nextBytes(_lastEntropy); + return UUIDUtil.constructUUID(UUIDType.TIME_BASED_EPOCH, (rawTimestamp << 16) | _toShort(_lastEntropy, 0), _toLong(_lastEntropy, 2)); + } finally { + lock.unlock(); + } + } +} diff --git a/src/main/java/com/fasterxml/uuid/impl/TimeBasedGenerator.java b/src/main/java/com/fasterxml/uuid/impl/TimeBasedGenerator.java index 2008d6d..4a8fada 100644 --- a/src/main/java/com/fasterxml/uuid/impl/TimeBasedGenerator.java +++ b/src/main/java/com/fasterxml/uuid/impl/TimeBasedGenerator.java @@ -6,10 +6,10 @@ /** * Implementation of UUID generator that uses time/location based generation - * method (variant 1). + * method (version 1). *

* As all JUG provided implementations, this generator is fully thread-safe. - * Additionally it can also be made externally synchronized with other + * Additionally, it can also be made externally synchronized with other * instances (even ones running on other JVMs); to do this, * use {@link com.fasterxml.uuid.ext.FileBasedTimestampSynchronizer} * (or equivalent). @@ -85,15 +85,29 @@ public TimeBasedGenerator(EthernetAddress ethAddr, UUIDTimer timer) /* UUID generation /********************************************************************** */ - - /* As timer is not synchronized (nor _uuidBytes), need to sync; but most - * importantly, synchronize on timer which may also be shared between - * multiple instances - */ + @Override public UUID generate() { - final long rawTimestamp = _timer.getTimestamp(); + return construct(_timer.getTimestamp()); + } + + /** + * Method that will construct actual {@link UUID} instance for given + * timestamp: called by {@link #generate()} but may alternatively be + * called directly to construct an instance with known timestamp. + * NOTE: calling this method directly does NOT guarantee uniqueness of resulting + * {@link UUID} (caller has to guarantee uniqueness) + * + * @param rawTimestamp Timestamp usually obtained from {@link UUIDTimer#getTimestamp()}, + * used for constructing UUID instance + * + * @return unix Time-based UUID constructed for given timestamp + * + * @since 4.3 + */ + public UUID construct(long rawTimestamp) + { // Time field components are kind of shuffled, need to slice: int clockHi = (int) (rawTimestamp >>> 32); int clockLo = (int) rawTimestamp; diff --git a/src/main/java/com/fasterxml/uuid/impl/TimeBasedReorderedGenerator.java b/src/main/java/com/fasterxml/uuid/impl/TimeBasedReorderedGenerator.java new file mode 100644 index 0000000..5e0959b --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/impl/TimeBasedReorderedGenerator.java @@ -0,0 +1,133 @@ +package com.fasterxml.uuid.impl; + +import java.util.UUID; + +import com.fasterxml.uuid.*; + +/** + * Implementation of UUID generator that uses time/location based generation + * method field compatible with UUIDv1, reorderd for improved DB locality. + * This is usually referred to as "Version 6". + *

+ * As all JUG provided implementations, this generator is fully thread-safe. + * Additionally it can also be made externally synchronized with other instances + * (even ones running on other JVMs); to do this, use + * {@link com.fasterxml.uuid.ext.FileBasedTimestampSynchronizer} (or + * equivalent). + * + * @since 4.1 + */ +public class TimeBasedReorderedGenerator extends NoArgGenerator +{ + public static int BYTE_OFFSET_TIME_HIGH = 0; + public static int BYTE_OFFSET_TIME_MID = 4; + public static int BYTE_OFFSET_TIME_LOW = 7; + + /* + /********************************************************************** + /* Configuration + /********************************************************************** + */ + + protected final EthernetAddress _ethernetAddress; + + /** + * Object used for synchronizing access to timestamps, to guarantee + * that timestamps produced by this generator are unique and monotonically increasings. + * Some implementations offer even stronger guarantees, for example that + * same guarantee holds between instances running on different JVMs (or + * with native code). + */ + protected final UUIDTimer _timer; + + /** + * Base values for the second long (last 8 bytes) of UUID to construct + */ + protected final long _uuidL2; + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + /** + * @param ethAddr Hardware address (802.1) to use for generating + * spatially unique part of UUID. If system has more than one NIC, + */ + + public TimeBasedReorderedGenerator(EthernetAddress ethAddr, UUIDTimer timer) + { + byte[] uuidBytes = new byte[16]; + if (ethAddr == null) { + ethAddr = EthernetAddress.constructMulticastAddress(); + } + // initialize baseline with MAC address info + _ethernetAddress = ethAddr; + _ethernetAddress.toByteArray(uuidBytes, 10); + // and add clock sequence + int clockSeq = timer.getClockSequence(); + uuidBytes[UUIDUtil.BYTE_OFFSET_CLOCK_SEQUENCE] = (byte) (clockSeq >> 8); + uuidBytes[UUIDUtil.BYTE_OFFSET_CLOCK_SEQUENCE+1] = (byte) clockSeq; + long l2 = UUIDUtil.gatherLong(uuidBytes, 8); + _uuidL2 = UUIDUtil.initUUIDSecondLong(l2); + _timer = timer; + } + + /* + /********************************************************************** + /* Access to config + /********************************************************************** + */ + + @Override + public UUIDType getType() { return UUIDType.TIME_BASED_REORDERED; } + + public EthernetAddress getEthernetAddress() { return _ethernetAddress; } + + /* + /********************************************************************** + /* UUID generation + /********************************************************************** + */ + + @Override + public UUID generate() + { + // Ok, get 60-bit timestamp (4 MSB are ignored) + return construct(_timer.getTimestamp()); + } + + /** + * Method that will construct actual {@link UUID} instance for given + * timestamp: called by {@link #generate()} but may alternatively be + * called directly to construct an instance with known timestamp. + * NOTE: calling this method directly does NOT guarantee uniqueness of resulting + * {@link UUID} (caller has to guarantee uniqueness) + * + * @param rawTimestamp Timestamp usually obtained from {@link UUIDTimer#getTimestamp()}, + * used for constructing UUID instance + * + * @return unix Time-based, Reordered UUID constructed for given timestamp + * + * @since 4.3 + */ + public UUID construct(long rawTimestamp) + { + // First: discard 4 MSB, next 32 bits (top of 60-bit timestamp) form the + // highest 32-bit segments + final long timestampHigh = (rawTimestamp >>> 28) << 32; + // and then bottom 28 bits split into mid (16 bits), low (12 bits) + final int timestampLow = (int) rawTimestamp; + // and then low part gets mixed with variant identifier + final int timeBottom = (((timestampLow >> 12) & 0xFFFF) << 16) + // and final 12 bits mixed with variant identifier + | 0x6000 | (timestampLow & 0xFFF); + long timeBottomL = (long) timeBottom; + timeBottomL = ((timeBottomL << 32) >>> 32); // to get rid of sign extension + + // and reconstruct + long l1 = timestampHigh | timeBottomL; + return new UUID(l1, _uuidL2); + } +} diff --git a/src/main/java/com/fasterxml/uuid/impl/UUIDUtil.java b/src/main/java/com/fasterxml/uuid/impl/UUIDUtil.java index 65be0ba..e66041f 100644 --- a/src/main/java/com/fasterxml/uuid/impl/UUIDUtil.java +++ b/src/main/java/com/fasterxml/uuid/impl/UUIDUtil.java @@ -2,6 +2,7 @@ import java.util.UUID; +import com.fasterxml.uuid.UUIDTimer; import com.fasterxml.uuid.UUIDType; public class UUIDUtil @@ -16,7 +17,17 @@ public class UUIDUtil // similarly, clock sequence and variant are multiplexed public final static int BYTE_OFFSET_CLOCK_SEQUENCE = 8; public final static int BYTE_OFFSET_VARIATION = 8; - + + /** + * @since 4.1 + */ + private final static UUID NIL_UUID = new UUID(0L, 0L); + + /** + * @since 4.1 + */ + private final static UUID MAX_UUID = new UUID(-1L, -1L); + /* /********************************************************************** /* Construction (can instantiate, although usually not necessary) @@ -27,6 +38,38 @@ public class UUIDUtil // via static methods public UUIDUtil() { } + /* + /********************************************************************** + /* Static UUID instances + /********************************************************************** + */ + + /** + * Accessor for so-call "Nil UUID" (see + * RFC 9562, #5.9; + * one that is all zeroes. + * + * @since 4.1 + * + * @return "Nil" UUID instance + */ + public static UUID nilUUID() { + return NIL_UUID; + } + + /** + * Accessor for so-call "Max UUID" (see + * RFC-9562, #5.10); + * one that is all one bits + * + * @since 4.1 + * + * @return "Nil" UUID instance + */ + public static UUID maxUUID() { + return MAX_UUID; + } + /* /********************************************************************** /* Factory methods @@ -219,6 +262,12 @@ public static UUIDType typeOf(UUID uuid) return UUIDType.RANDOM_BASED; case 5: return UUIDType.NAME_BASED_SHA1; + case 6: + return UUIDType.TIME_BASED_REORDERED; + case 7: + return UUIDType.TIME_BASED_EPOCH; + case 8: + return UUIDType.FREE_FORM; } // not recognized: return null return null; @@ -305,4 +354,82 @@ private final static void _checkUUIDByteArray(byte[] bytes, int offset) throw new IllegalArgumentException("Invalid offset ("+offset+") passed: not enough room in byte array (need 16 bytes)"); } } + + /** + * Extract 64-bit timestamp from time-based UUIDs (if time-based type); + * returns 0 for other types. + * + * @param uuid uuid timestamp to extract from + * + * @return Unix timestamp in milliseconds (since Epoch), or 0 if type does not support timestamps + * + * @since 5.0 + */ + public static long extractTimestamp(UUID uuid) + { + UUIDType type = typeOf(uuid); + if (type == null) { + // Likely null UUID: + return 0L; + } + switch (type) { + case NAME_BASED_SHA1: + case UNKNOWN: + case DCE: + case RANDOM_BASED: + case FREE_FORM: + case NAME_BASED_MD5: + return 0L; + case TIME_BASED: + return UUIDTimer.timestampToEpoch(_getRawTimestampFromUuidV1(uuid)); + case TIME_BASED_REORDERED: + return UUIDTimer.timestampToEpoch(_getRawTimestampFromUuidV6(uuid)); + case TIME_BASED_EPOCH: + return _getRawTimestampFromUuidV7(uuid); + default: + throw new IllegalArgumentException("Invalid `UUID`: unexpected type " + type); + } + } + + /** + * Get raw timestamp, used to create the UUID v1 + *

+ * NOTE: no verification is done to ensure UUID given is of version 1. + * + * @param uuid uuid, to extract timestamp from + * @return timestamp, used to create uuid v1 + */ + static long _getRawTimestampFromUuidV1(UUID uuid) { + long mostSignificantBits = uuid.getMostSignificantBits(); + mostSignificantBits = mostSignificantBits & 0b1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1110_1111_1111_1111L; + long low = mostSignificantBits >>> 32; + long lowOfHigher = mostSignificantBits & 0xFFFF0000L; + lowOfHigher = lowOfHigher >>> 16; + long highOfHigher = mostSignificantBits & 0xFFFFL; + return highOfHigher << 48 | lowOfHigher << 32 | low; + } + + /** + * Get raw timestamp, used to create the UUID v6. + *

+ * NOTE: no verification is done to ensure UUID given is of version 6. + * + * @param uuid uuid, to extract timestamp from + * @return timestamp, used to create uuid v6 + */ + static long _getRawTimestampFromUuidV6(UUID uuid) { + long mostSignificantBits = uuid.getMostSignificantBits(); + mostSignificantBits = mostSignificantBits & 0b1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1001_1111_1111_1111L; + long lowL = mostSignificantBits & 0xFFFL; + long lowH = mostSignificantBits & 0xFFFF0000L; + lowH = lowH >>> 16; + long high = mostSignificantBits & 0xFFFFFFFF00000000L; + return high >>> 4 | lowH << 12 | lowL; + } + + static long _getRawTimestampFromUuidV7(UUID uuid) { + long mostSignificantBits = uuid.getMostSignificantBits(); + mostSignificantBits = mostSignificantBits & 0b1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1001_1111_1111_1111L; + return mostSignificantBits >>> 16; + } } diff --git a/src/main/java/com/fasterxml/uuid/impl/package-info.java b/src/main/java/com/fasterxml/uuid/impl/package-info.java new file mode 100644 index 0000000..aa868e4 --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/impl/package-info.java @@ -0,0 +1,5 @@ +/** +Package that contains actual Java UUID Generator implementation classes, +including generators for different UUID types. +*/ +package com.fasterxml.uuid.impl; diff --git a/src/main/java/com/fasterxml/uuid/package-info.java b/src/main/java/com/fasterxml/uuid/package-info.java new file mode 100644 index 0000000..2b17540 --- /dev/null +++ b/src/main/java/com/fasterxml/uuid/package-info.java @@ -0,0 +1,9 @@ +/** +Package that contains classes that define Java UUID Generator API. +Implementation classes can be found from under {@link com.fasterxml.uuid.impl}. +

+The primary API entrypoint is {@link com.fasterxml.uuid.Generators}, +used to construct actual generators to use for UUID generation. +*/ + +package com.fasterxml.uuid; diff --git a/src/main/java/com/fasterxml/uuid/package.html b/src/main/java/com/fasterxml/uuid/package.html deleted file mode 100644 index ae055d7..0000000 --- a/src/main/java/com/fasterxml/uuid/package.html +++ /dev/null @@ -1,14 +0,0 @@ - -Package that contains core (non-optional) Java UUID Generator API classes. -Implementation classes can be found from under {@link com.fasterxml.uuid.impl}. -These classes should be usable on JDK 1.4 and up, and have no external dependencies; -except that any functionality that uses Ethernet-address discovery requires JDK 1.6. -

-The primary point is {@link com.fasterxml.uuid.Generators}, used to construct actual -generators, based on method to use and some optional arguments. -

-

-Note: earlier JUG versions (up to 2.0) supported older JDKs (1.1); 1.4 is now needed -since {@link java.util.UUID} is used as a core abstraction. -

- diff --git a/src/main/java/perf/MeasurePerformance.java b/src/main/java/perf/MeasurePerformance.java index bc9f067..71d1928 100644 --- a/src/main/java/perf/MeasurePerformance.java +++ b/src/main/java/perf/MeasurePerformance.java @@ -1,5 +1,6 @@ package perf; +import java.nio.charset.StandardCharsets; import java.util.UUID; import com.fasterxml.uuid.*; @@ -8,31 +9,37 @@ /** * Simple micro-benchmark for evaluating performance of various UUID generation - * techniques, including JDK's method as well as JUG's variants. + * techniques, including JDK's method as well as JUG's versions. *

- * Notes: for name-based variant we will pass plain Strings, assuming this is the + * Notes: for name-based version we will pass plain Strings, assuming this is the * most common use case; even though it is possible to also pass raw byte arrays. * JDK and Jug implementations have similar performance so this only changes - * relative speeds of name- vs time-based variants. + * relative speeds of name- vs time-based versions. * * @since 3.1 */ public class MeasurePerformance { - // Let's generate quarter million UUIDs per test - - private static final int ROUNDS = 250; - private static final int COUNT = 1000; - + // also: let's just use a single name for name-based, to avoid extra overhead: - final String NAME = "http://www.cowtowncoder.com/blog/blog.html"; - final byte[] NAME_BYTES; + private final static String NAME_STRING = "http://www.cowtowncoder.com/blog/blog.html"; - public MeasurePerformance() throws java.io.IOException - { - NAME_BYTES = NAME.getBytes("UTF-8"); + private final static byte[] NAME_BYTES = NAME_STRING.getBytes(StandardCharsets.UTF_8); + + // Let's generate 50k UUIDs per test round + private static final int COUNT = 1000; + private static final int DEFAULT_ROUNDS = 50; + + private final int rounds; + private final boolean runForever; + + public MeasurePerformance() { this(DEFAULT_ROUNDS, true); } + + public MeasurePerformance(int rounds, boolean runForever) { + this.rounds = rounds; + this.runForever = runForever; } - + public void test() throws Exception { int i = 0; @@ -53,8 +60,11 @@ public void test() throws Exception new com.fasterxml.uuid.ext.FileBasedTimestampSynchronizer()); final StringArgGenerator nameGen = Generators.nameBasedGenerator(namespaceForNamed); - while (true) { - try { Thread.sleep(100L); } catch (InterruptedException ie) { } + boolean running = true; + final long sleepTime = runForever ? 350L : 1L; + + while (running) { + Thread.sleep(sleepTime); int round = (i++ % 7); long curr = System.currentTimeMillis(); @@ -65,44 +75,49 @@ public void test() throws Exception case 0: msg = "JDK, random"; - testJDK(uuids, ROUNDS); + testJDK(uuids, rounds); break; case 1: msg = "JDK, name"; - testJDKNames(uuids, ROUNDS); + testJDKNames(uuids, rounds); break; case 2: msg = "Jug, time-based (non-sync)"; - testTimeBased(uuids, ROUNDS, timeGenPlain); + testTimeBased(uuids, rounds, timeGenPlain); break; case 3: msg = "Jug, time-based (SYNC)"; - testTimeBased(uuids, ROUNDS, timeGenSynced); + testTimeBased(uuids, rounds, timeGenSynced); break; case 4: msg = "Jug, SecureRandom"; - testRandom(uuids, ROUNDS, secureRandomGen); + testRandom(uuids, rounds, secureRandomGen); break; case 5: msg = "Jug, java.util.Random"; - testRandom(uuids, ROUNDS, utilRandomGen); + testRandom(uuids, rounds, utilRandomGen); break; case 6: msg = "Jug, name-based"; - testNameBased(uuids, ROUNDS, nameGen); + testNameBased(uuids, rounds, nameGen); + + // Last one, quit unless running forever + if (!runForever) { + running = false; + } break; /* case 7: msg = "http://johannburkard.de/software/uuid/"; - testUUID32(uuids, ROUNDS); + testUUID32(uuids, rounds); break; */ @@ -143,7 +158,7 @@ private final void testJDKNames(Object[] uuids, int rounds) throws java.io.IOExc { while (--rounds >= 0) { for (int i = 0, len = uuids.length; i < len; ++i) { - final byte[] nameBytes = NAME.getBytes("UTF-8"); + final byte[] nameBytes = NAME_BYTES; uuids[i] = UUID.nameUUIDFromBytes(nameBytes); } } @@ -171,13 +186,13 @@ private final void testNameBased(Object[] uuids, int rounds, StringArgGenerator { while (--rounds >= 0) { for (int i = 0, len = uuids.length; i < len; ++i) { - uuids[i] = uuidGen.generate(NAME); + uuids[i] = uuidGen.generate(NAME_STRING); } } } public static void main(String[] args) throws Exception { - new MeasurePerformance().test(); + new MeasurePerformance(DEFAULT_ROUNDS, true).test(); } } diff --git a/src/test/java/com/fasterxml/uuid/EgressInterfaceFinderTest.java b/src/test/java/com/fasterxml/uuid/EgressInterfaceFinderTest.java new file mode 100644 index 0000000..4b6eb77 --- /dev/null +++ b/src/test/java/com/fasterxml/uuid/EgressInterfaceFinderTest.java @@ -0,0 +1,127 @@ +package com.fasterxml.uuid; + +import com.fasterxml.uuid.EgressInterfaceFinder.EgressResolutionException; +import com.fasterxml.uuid.EgressInterfaceFinder.Finder; +import junit.framework.TestCase; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.UnknownHostException; + +import static com.fasterxml.uuid.EgressInterfaceFinder.DEFAULT_TIMEOUT_MILLIS; + +public class EgressInterfaceFinderTest extends TestCase { + + private final EgressInterfaceFinder finder = new EgressInterfaceFinder(); + + public void testUnspecifiedIPv4LocalAddress() throws UnknownHostException { + EgressResolutionException ex = null; + try { + finder.fromLocalAddress(InetAddress.getByName("0.0.0.0")); + } catch (EgressResolutionException e) { + ex = e; + } + assertNotNull("EgressResolutionException was not thrown", ex); + String message = ex.getMessage(); + assertTrue(String.format( + "message [%s] does not begin with \"local address\"", + message), + message.startsWith("local address")); + assertEquals(1, ex.getMessages().size()); + } + + public void testUnspecifiedIPv6LocalAddress() throws Exception { + EgressResolutionException ex = null; + try { + finder.fromLocalAddress(InetAddress.getByName("::")); + } catch (EgressResolutionException e) { + ex = e; + } + assertNotNull("EgressResolutionException was not thrown", ex); + String message = ex.getMessage(); + assertTrue(String.format( + "message [%s] does not begin with \"local address\"", + message), + message.startsWith("local address")); + assertEquals(1, ex.getMessages().size()); + } + + public void testFromLocalAddress() throws Exception { + NetworkInterface anInterface = + NetworkInterface.getNetworkInterfaces().nextElement(); + InetAddress anAddress = anInterface.getInetAddresses().nextElement(); + assertEquals(anInterface, finder.fromLocalAddress(anAddress)); + } + + public void testFromIncorrectLocalAddress() throws Exception { + EgressResolutionException ex = null; + try { + String name = EgressInterfaceFinder.randomRootServerName(); + finder.fromLocalAddress(InetAddress.getByName(name)); + } catch (EgressResolutionException e) { + ex = e; + } + assertNotNull("EgressResolutionException was not thrown", ex); + String message = ex.getMessage(); + assertTrue(String.format( + "message [%s] does not begin with \"no interface found\"", + message), + message.startsWith("no interface found")); + assertEquals(1, ex.getMessages().size()); + } + + public void testFromRemoteDatagramSocketConnection() throws Exception { + if (!System.getProperty("os.name").startsWith("Mac")) { + String name = EgressInterfaceFinder.randomRootServerName(); + InetSocketAddress address = new InetSocketAddress(name, 53); + finder.fromRemoteDatagramSocketConnection(address); + } + } + + public void testFromRemoteSocketConnection() throws Exception { + String name = EgressInterfaceFinder.randomRootServerName(); + InetSocketAddress address = new InetSocketAddress(name, 53); + finder.fromRemoteSocketConnection(DEFAULT_TIMEOUT_MILLIS, address); + } + + public void testFromRemoteConnection() throws Exception { + String name = EgressInterfaceFinder.randomRootServerName(); + InetSocketAddress address = new InetSocketAddress(name, 53); + finder.fromRemoteConnection(DEFAULT_TIMEOUT_MILLIS, address); + } + + public void testFromRootNameServerConnection() throws Exception { + finder.fromRootNameserverConnection(DEFAULT_TIMEOUT_MILLIS); + } + + public void testAggregateExceptions() { + EgressResolutionException ex = null; + final int[] counter = {0}; + Finder aFinder = new Finder() { + @Override + public NetworkInterface egressInterface() + throws EgressResolutionException { + throw new EgressResolutionException( + String.format("exception %d", ++counter[0]), + new Exception("test exception")); + } + }; + try { + finder.fromAggregate(new Finder[] { aFinder, aFinder, aFinder}); + } catch (EgressResolutionException e) { + ex = e; + } + assertNotNull("EgressResolutionException was not thrown", ex); + assertEquals(9, ex.getMessages().size()); + } + + public void testDefaultMechanisms() throws Exception { + try { + finder.egressInterface(); + } catch (EgressResolutionException e) { + e.report(); + throw e; + } + } +} diff --git a/src/test/java/com/fasterxml/uuid/EthernetAddressTest.java b/src/test/java/com/fasterxml/uuid/EthernetAddressTest.java index b235c7d..1695b32 100644 --- a/src/test/java/com/fasterxml/uuid/EthernetAddressTest.java +++ b/src/test/java/com/fasterxml/uuid/EthernetAddressTest.java @@ -17,6 +17,8 @@ package com.fasterxml.uuid; +import com.fasterxml.uuid.impl.TimeBasedGenerator; +import java.net.InetSocketAddress; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; @@ -25,8 +27,6 @@ import java.util.Arrays; import java.util.Random; -import com.fasterxml.uuid.EthernetAddress; - /** * JUnit Test class for the com.fasterxml.uuid.EthernetAddress class. * @@ -1309,6 +1309,21 @@ public void testFromInterface() throws Exception assertNotNull(addr.toString()); } + public void testFromEgressInterface() { + EthernetAddress ifAddr = EthernetAddress.fromEgressInterface(); + assertNotNull(ifAddr); + assertNotNull(ifAddr.toString()); + } + + public void testDefaultTimeBasedGenerator() + { + TimeBasedGenerator generator = Generators.defaultTimeBasedGenerator(); + assertNotNull(generator); + EthernetAddress ifAddr = generator.getEthernetAddress(); + assertNotNull(ifAddr); + assertNotNull(ifAddr.toString()); + } + public void testBogus() throws Exception { // First, two using pseudo-random; verify they are different diff --git a/src/test/java/com/fasterxml/uuid/JugNamedTest.java b/src/test/java/com/fasterxml/uuid/JugNamedTest.java new file mode 100644 index 0000000..2352a94 --- /dev/null +++ b/src/test/java/com/fasterxml/uuid/JugNamedTest.java @@ -0,0 +1,178 @@ +package com.fasterxml.uuid; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +@RunWith(Parameterized.class) +public class JugNamedTest { + @Parameterized.Parameter + public UseCase useCase; + + private PrintStream oldStrOut; + private PrintStream oldStrErr; + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private Jug jug_underTest; + + @Before + public void setup() { + jug_underTest = new Jug();oldStrOut = System.out; + oldStrErr = System.err; + PrintStream stubbedStream = new PrintStream(outContent); + System.setOut(stubbedStream); + PrintStream stubbedErrStream = new PrintStream(errContent); + System.setErr(stubbedErrStream); + } + + @After + public void cleanup() { + System.setOut(oldStrOut); + System.setErr(oldStrErr); + } + + @Test + public void run_shouldProduceUUID() { + // given + + // when + List arguments = useCase.getArgs(); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String actualUuid = outContent.toString(); + assertEquals('\n', actualUuid.charAt(actualUuid.length() - 1)); + + assertEquals(UUID.class, + UUID.fromString(actualUuid.substring(0, actualUuid.length() - 1)).getClass()); + } + + @Test + public void run_givenCount3_shouldProduceUUID() { + // given + + // when + List arguments = useCase.getArgs(); + arguments.add(0, "-c"); + arguments.add(1, "3"); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String[] actualUuids = outContent.toString().split("\n"); + for(String actualUuid: actualUuids) { + assertEquals(UUID.class, + UUID.fromString(actualUuid).getClass()); + } + } + + @Test + public void run_givenPerformance_shouldProducePerformanceInfo() { + // given + + // when + List arguments = useCase.getArgs(); + arguments.add(0, "-p"); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then + String actualOutput = outContent.toString(); + + assertThat(actualOutput, containsString("Performance: took")); + } + @Test + public void run_givenHelp_shouldProduceHelpInfo() { + // given + + // when + List arguments = useCase.getArgs(); + arguments.add(0, "-h"); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then + String actualOutput = errContent.toString(); + + assertThat(actualOutput, containsString("Usage: java")); + } + + @Test + public void run_givenVerbose_shouldProduceExtraInfo() { + // given + + // when + List arguments = useCase.getArgs(); + arguments.add(0, "-v"); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then + String actualOutput = outContent.toString(); + + assertThat(actualOutput, containsString("Done.")); + } + + @Test + public void run_givenVerboseAndPerformance_shouldProduceExtraInfo() { + // given + + // when + List arguments = useCase.getArgs(); + arguments.add(0, "-v"); + arguments.add(1, "-p"); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then + String actualOutput = outContent.toString(); + + assertThat(actualOutput, containsString("Done.")); + assertThat(actualOutput, containsString("Performance: took")); + } + + @Parameterized.Parameters(name = "{index} -> {0}") + public static List useCases() { + return Arrays.asList( + new UseCase("n", "-n", "world", "-s", "url"), + new UseCase("n", "-n", "world", "-s", "dns") + ); + } + + private static class UseCase { + private final String type; + private String[] options = new String[]{}; + + public UseCase(String type, String...options) { + this.type = type; + if (options != null) { + this.options = options; + } + } + + public List getArgs() { + List arguments = new ArrayList<>(Arrays.asList(options)); + arguments.add(type); + return arguments; + } + + @Override + public String toString() { + if (options.length == 0) { + return String.format("type: %s, options: no options", type); + } else { + return String.format("type: %s, options: %s", type, String.join(", ", options)); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/fasterxml/uuid/JugNoArgsTest.java b/src/test/java/com/fasterxml/uuid/JugNoArgsTest.java new file mode 100644 index 0000000..d000105 --- /dev/null +++ b/src/test/java/com/fasterxml/uuid/JugNoArgsTest.java @@ -0,0 +1,217 @@ +package com.fasterxml.uuid; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Array; +import java.util.*; + +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.*; + +@RunWith(Parameterized.class) +public class JugNoArgsTest { + @Parameterized.Parameter + public String useCase; + + private PrintStream oldStrOut; + private PrintStream oldStrErr; + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private Jug jug_underTest; + + @Before + public void setup() { + jug_underTest = new Jug(); + oldStrOut = System.out; + oldStrErr = System.err; + PrintStream stubbedStream = new PrintStream(outContent); + System.setOut(stubbedStream); + PrintStream stubbedErrStream = new PrintStream(errContent); + System.setErr(stubbedErrStream); + } + + @After + public void cleanup() { + System.setOut(oldStrOut); + System.setErr(oldStrErr); + } + + @Test + public void run_givenNoOptions_shouldProduceUUID() { + // given + + // when + jug_underTest.run(new String[]{useCase}); + + // then - if it is a UUID then we should be able to parse it back out + String actualUuid = outContent.toString(); + assertEquals('\n', actualUuid.charAt(actualUuid.length() - 1)); + + assertEquals(UUID.class, + UUID.fromString(actualUuid.substring(0, actualUuid.length() - 1)).getClass()); + } + + @Test + public void run_givenCount1_shouldProduceUUID() { + // given + + // when + List arguments = new ArrayList<>(Arrays.asList("-c", "1")); + arguments.add(useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String actualUuid = outContent.toString(); + assertEquals('\n', actualUuid.charAt(actualUuid.length() - 1)); + + assertEquals(UUID.class, + UUID.fromString(actualUuid.substring(0, actualUuid.length() - 1)).getClass()); + } + + @Test + public void run_givenCount2_shouldProduce2UUIDs() { + // given + + // when + List arguments = new ArrayList<>(Arrays.asList("-c", "2")); + arguments.add(useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String[] actualUuids = outContent.toString().split("\n"); + assertEquals(2, actualUuids.length); + + for(String actualUuid: actualUuids) { + assertEquals(UUID.class, + UUID.fromString(actualUuid).getClass()); + } + } + + @Test + public void run_givenEthernet_shouldProduceUUID() { + // given + + // when + List arguments = new ArrayList<>(Arrays.asList("-e", ":::::")); + arguments.add(useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String actualUuid = outContent.toString(); + assertEquals('\n', actualUuid.charAt(actualUuid.length() - 1)); + + assertEquals(UUID.class, + UUID.fromString(actualUuid.substring(0, actualUuid.length() - 1)).getClass()); + } + + @Test + public void run_givenName_shouldProduceUUID() { + // given + + // when + List arguments = new ArrayList<>(Arrays.asList("-n", "hello")); + arguments.add(useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String actualUuid = outContent.toString(); + assertEquals('\n', actualUuid.charAt(actualUuid.length() - 1)); + + assertEquals(UUID.class, + UUID.fromString(actualUuid.substring(0, actualUuid.length() - 1)).getClass()); + } + + @Test + public void run_givenDnsNameSpace_shouldProduceUUID() { + // given + + // when + List arguments = new ArrayList<>(Arrays.asList("-s", "dns")); + arguments.add(useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String actualUuid = outContent.toString(); + assertEquals('\n', actualUuid.charAt(actualUuid.length() - 1)); + + assertEquals(UUID.class, + UUID.fromString(actualUuid.substring(0, actualUuid.length() - 1)).getClass()); + } + + @Test + public void run_givenUrlNameSpace_shouldProduceUUID() { + // given + + // when + List arguments = new ArrayList<>(Arrays.asList("-s", "url")); + arguments.add(useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then - if it is a UUID then we should be able to parse it back out + String actualUuid = outContent.toString(); + assertEquals('\n', actualUuid.charAt(actualUuid.length() - 1)); + + assertEquals(UUID.class, + UUID.fromString(actualUuid.substring(0, actualUuid.length() - 1)).getClass()); + } + + @Test + public void run_givenPerformance_shouldProducePerformanceInfo() { + // given + + // when + List arguments = Arrays.asList("-p", useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then + String actualOutput = outContent.toString(); + + assertThat(actualOutput, containsString("Performance: took")); + } + + @Test + public void run_givenHelp_shouldProduceHelpInfo() { + // given + + // when + List arguments = Arrays.asList("-h", useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then + String actualOutput = errContent.toString(); + + assertThat(actualOutput, containsString("Usage: java")); + } + + @Test + public void run_givenVerbose_shouldProduceExtraInfo() { + // given + + // when + List arguments = Arrays.asList("-v", useCase); + jug_underTest.run(arguments.toArray((String[]) Array.newInstance(String.class, 0))); + + // then + String actualOutput = outContent.toString(); + + assertThat(actualOutput, containsString("Done.")); + } + + @Parameterized.Parameters(name = "{index} -> type: {0}") + public static List useCases() { + return Arrays.asList( + "t", + "o", + "r", + "e", + "m" + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/fasterxml/uuid/UUIDComparatorTest.java b/src/test/java/com/fasterxml/uuid/UUIDComparatorTest.java index 099af19..3d79ec9 100644 --- a/src/test/java/com/fasterxml/uuid/UUIDComparatorTest.java +++ b/src/test/java/com/fasterxml/uuid/UUIDComparatorTest.java @@ -14,8 +14,15 @@ */ package com.fasterxml.uuid; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; import java.util.UUID; +import com.fasterxml.uuid.impl.TimeBasedEpochGenerator; + +import com.fasterxml.uuid.impl.TimeBasedEpochRandomGenerator; import junit.framework.TestCase; public class UUIDComparatorTest @@ -100,4 +107,24 @@ public void testSorting() } } } + + public void testSortingMV7() throws Exception { + final int count = 10000000; + Random entropy = new Random(0x666); + final TimeBasedEpochGenerator generator = Generators.timeBasedEpochGenerator(entropy); + List created = new ArrayList(count); + for (int i = 0; i < count; i++) { + created.add(generator.generate()); + } + List sortedUUID = new ArrayList(created); + sortedUUID.sort(new UUIDComparator()); + HashSet unique = new HashSet(count); + + for (int i = 0; i < created.size(); i++) { + assertEquals("Error at: " + i, created.get(i), sortedUUID.get(i)); + if (!unique.add(created.get(i))) { + System.out.println("Duplicate at: " + i); + } + } + } } diff --git a/src/test/java/com/fasterxml/uuid/UUIDGeneratorTest.java b/src/test/java/com/fasterxml/uuid/UUIDGeneratorTest.java index c7e22f7..1b86416 100644 --- a/src/test/java/com/fasterxml/uuid/UUIDGeneratorTest.java +++ b/src/test/java/com/fasterxml/uuid/UUIDGeneratorTest.java @@ -17,6 +17,9 @@ package com.fasterxml.uuid; +import static org.junit.Assert.assertNotEquals; + +import java.nio.ByteBuffer; import java.security.MessageDigest; import java.util.*; @@ -25,24 +28,27 @@ import junit.framework.TestSuite; import junit.textui.TestRunner; - -import com.fasterxml.uuid.EthernetAddress; -import com.fasterxml.uuid.Generators; -import com.fasterxml.uuid.UUIDType; import com.fasterxml.uuid.impl.UUIDUtil; import com.fasterxml.uuid.impl.NameBasedGenerator; import com.fasterxml.uuid.impl.RandomBasedGenerator; +import com.fasterxml.uuid.impl.TimeBasedEpochGenerator; +import com.fasterxml.uuid.impl.TimeBasedEpochRandomGenerator; +import com.fasterxml.uuid.impl.TimeBasedReorderedGenerator; import com.fasterxml.uuid.impl.TimeBasedGenerator; /** * JUnit Test class for the com.fasterxml.uuid.UUIDGenerator class. * * @author Eric Bie + * @author Tatu Saloranta */ public class UUIDGeneratorTest extends TestCase { // size of the arrays to create for tests using arrays of values - private static final int SIZE_OF_TEST_ARRAY = 10000; + // 19-Jun-2022, tatu: Reduce from 10000 since that seems to sometimes + // trigger timing overflow wrt sanity checks (sanity checks being + // simplistic; not exposing an actual issue) + private static final int SIZE_OF_TEST_ARRAY = 9000; public UUIDGeneratorTest(java.lang.String testName) { @@ -114,7 +120,8 @@ public void testGenerateRandomBasedUUID() // we need a instance to use RandomBasedGenerator uuid_gen = Generators.randomBasedGenerator(); - + assertEquals(uuid_gen.getType(), UUIDType.RANDOM_BASED); + // for the random UUID generator, we will generate a bunch of // random UUIDs UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; @@ -148,7 +155,8 @@ public void testGenerateTimeBasedUUID() // we need a instance to use TimeBasedGenerator uuid_gen = Generators.timeBasedGenerator(); - + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED); + // first check that given a number of calls to generateTimeBasedUUID, // all returned UUIDs order after the last returned UUID // we'll check this by generating the UUIDs into one array and sorting @@ -197,7 +205,8 @@ public void testGenerateTimeBasedUUIDWithEthernetAddress() // we need a instance to use TimeBasedGenerator uuid_gen = Generators.timeBasedGenerator(ethernet_address); - + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED); + // check that given a number of calls to generateTimeBasedUUID, // all returned UUIDs order after the last returned UUID // we'll check this by generating the UUIDs into one array and sorting @@ -235,6 +244,253 @@ public void testGenerateTimeBasedUUIDWithEthernetAddress() // check that all UUIDs have the correct ethernet address in the UUID checkUUIDArrayForCorrectEthernetAddress(uuid_array, ethernet_address); } + + public void testV7value() + { + // Test vector from spec + UUID testValue = UUID.fromString("017F22E2-79B0-7CC3-98C4-DC0C0C07398F"); + checkUUIDArrayForCorrectCreationTimeEpoch(new UUID[] { testValue }, 1645557742000L, 1645557742010L); + } + + /** + * Test of generateTimeBasedEpochUUID() method, + * of class com.fasterxml.uuid.UUIDGenerator. + */ + public void testGenerateTimeBasedEpochUUID() throws Exception + { + // this test will attempt to check for reasonable behavior of the + // generateTimeBasedUUID method + + Random entropy = new Random(0x666); + + // we need a instance to use + TimeBasedEpochGenerator uuid_gen = Generators.timeBasedEpochGenerator(entropy); + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED_EPOCH); + + // first check that given a number of calls to generateTimeBasedEpochUUID, + // all returned UUIDs order after the last returned UUID + // we'll check this by generating the UUIDs into one array and sorting + // then in another and checking the order of the two match + // change the number in the array statement if you want more or less + // UUIDs to be generated and tested + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // before we generate all the uuids, lets get the start time + long start_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean start time + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) { + uuid_array[i] = uuid_gen.generate(); + } + + // now capture the end time + long end_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean end time + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version (type-1) + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.TIME_BASED_EPOCH); + + // check that all the uuids were generated with correct order + checkUUIDArrayForCorrectOrdering(uuid_array); + + // check that all uuids were unique + checkUUIDArrayForUniqueness(uuid_array); + + // check that all uuids have timestamps between the start and end time + checkUUIDArrayForCorrectCreationTimeEpoch(uuid_array, start_time, end_time); + } + + /** + * Test of generateTimeBasedEpochUUID() method with UUIDClock instance, + * of class com.fasterxml.uuid.UUIDGenerator. + */ + public void testGenerateTimeBasedEpochUUIDWithUUIDClock() throws Exception + { + // this test will attempt to check for reasonable behavior of the + // generateTimeBasedUUID method + + Random entropy = new Random(0x666); + + // we need a instance to use + TimeBasedEpochGenerator uuid_gen = Generators.timeBasedEpochGenerator(null, UUIDClock.systemTimeClock()); + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED_EPOCH); + + // first check that given a number of calls to generateTimeBasedEpochUUID, + // all returned UUIDs order after the last returned UUID + // we'll check this by generating the UUIDs into one array and sorting + // then in another and checking the order of the two match + // change the number in the array statement if you want more or less + // UUIDs to be generated and tested + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // before we generate all the uuids, lets get the start time + long start_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean start time + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) { + uuid_array[i] = uuid_gen.generate(); + } + + // now capture the end time + long end_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean end time + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version (type-1) + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.TIME_BASED_EPOCH); + + // check that all the uuids were generated with correct order + checkUUIDArrayForCorrectOrdering(uuid_array); + + // check that all uuids were unique + checkUUIDArrayForUniqueness(uuid_array); + + // check that all uuids have timestamps between the start and end time + checkUUIDArrayForCorrectCreationTimeEpoch(uuid_array, start_time, end_time); + } + + /** + * Test of generateTimeBasedEpochRandomUUID() method, + * of class com.fasterxml.uuid.UUIDGenerator. + */ + public void testGenerateTimeBasedEpochRandomUUID() throws Exception + { + // this test will attempt to check for reasonable behavior of the + // generateTimeBasedRandomUUID method + + Random entropy = new Random(0x666); + + // we need a instance to use + TimeBasedEpochRandomGenerator uuid_gen = Generators.timeBasedEpochRandomGenerator(entropy); + + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED_EPOCH); + // first check that given a number of calls to generateTimeBasedEpochUUID, + // all returned UUIDs order after the last returned UUID + // we'll check this by generating the UUIDs into one array and sorting + // then in another and checking the order of the two match + // change the number in the array statement if you want more or less + // UUIDs to be generated and tested + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // before we generate all the uuids, lets get the start time + long start_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean start time + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) { + uuid_array[i] = uuid_gen.generate(); + } + + // now capture the end time + long end_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean end time + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version (type-1) + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.TIME_BASED_EPOCH); + + // check that all uuids were unique + // NOTE: technically, this test 'could' fail, but statistically + // speaking it should be extremely unlikely unless the implementation + // of (Secure)Random is bad + checkUUIDArrayForUniqueness(uuid_array); + + // check that all uuids have timestamps between the start and end time + checkUUIDArrayForCorrectCreationTimeEpoch(uuid_array, start_time, end_time); + } + + /** + * Test of generateTimeBasedEpochRandomUUID() method with UUIDClock instance, + * of class com.fasterxml.uuid.UUIDGenerator. + */ + public void testGenerateTimeBasedEpochRandomUUIDWithUUIDClock() throws Exception + { + // this test will attempt to check for reasonable behavior of the + // generateTimeBasedRandomUUID method + + Random entropy = new Random(0x666); + + // we need a instance to use + TimeBasedEpochRandomGenerator uuid_gen = Generators.timeBasedEpochRandomGenerator(null, UUIDClock.systemTimeClock()); + + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED_EPOCH); + // first check that given a number of calls to generateTimeBasedEpochUUID, + // all returned UUIDs order after the last returned UUID + // we'll check this by generating the UUIDs into one array and sorting + // then in another and checking the order of the two match + // change the number in the array statement if you want more or less + // UUIDs to be generated and tested + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // before we generate all the uuids, lets get the start time + long start_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean start time + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) { + uuid_array[i] = uuid_gen.generate(); + } + + // now capture the end time + long end_time = System.currentTimeMillis(); + Thread.sleep(2); // Clean end time + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version (type-1) + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.TIME_BASED_EPOCH); + + // check that all uuids were unique + // NOTE: technically, this test 'could' fail, but statistically + // speaking it should be extremely unlikely unless the implementation + // of (Secure)Random is bad + checkUUIDArrayForUniqueness(uuid_array); + + // check that all uuids have timestamps between the start and end time + checkUUIDArrayForCorrectCreationTimeEpoch(uuid_array, start_time, end_time); + } + + // [#70]: allow use of custom UUIDClock + public void testGenerateTimeBasedEpochUUIDWithFixedClock() throws Exception + { + final UUIDClock fixedClock = new UUIDClock() { + @Override + public long currentTimeMillis() { + return 123L; + } + }; + // we need a instance to use + TimeBasedEpochGenerator gen = Generators.timeBasedEpochGenerator(new Random(123), + fixedClock); + + UUID uuid1 = gen.generate(); + UUID uuid2 = gen.generate(); + UUID uuid3 = gen.generate(); + + // Alas! Was thinking of comparing fixed value, but even Epoch-based generator + // forces uniqueness by default. So instead will only test that generation + // works and produces unique instances + + // First: should be unique (diff contents) + assertNotEquals(uuid1, uuid2); + assertNotEquals(uuid2, uuid3); + assertNotEquals(uuid3, uuid1); + + // Second: should not be same instances either: + assertNotSame(uuid1, uuid2); + assertNotSame(uuid2, uuid3); + assertNotSame(uuid3, uuid1); + } /** * Test of generateNameBasedUUID(UUID, String) @@ -247,7 +503,8 @@ public void testGenerateNameBasedUUIDNameSpaceAndName() // we need a instance to use NameBasedGenerator uuid_gen = Generators.nameBasedGenerator(NameBasedGenerator.NAMESPACE_URL); - + assertEquals(uuid_gen.getType(), UUIDType.NAME_BASED_SHA1); + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; // now create the array of uuids @@ -331,6 +588,7 @@ public void testGenerateNameBasedUUIDNameSpaceNameAndMessageDigest() // generateNameBasedUUID method NameBasedGenerator uuid_gen = Generators.nameBasedGenerator(NameBasedGenerator.NAMESPACE_URL, MESSAGE_DIGEST); + assertEquals(uuid_gen.getType(), UUIDType.NAME_BASED_MD5); UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; // now create the array of uuids @@ -396,10 +654,188 @@ public void testGenerateNameBasedUUIDNameSpaceNameAndMessageDigest() Arrays.equals(uuid_array, uuid_array2)); } + /** + * Test of generateNameBasedUUID() + * method, of class com.fasterxml.uuid.UUIDGenerator. + */ + public void testGenerateNameBasedUUIDWithDefaults() + { + // this test will attempt to check for reasonable behavior of the + // generateNameBasedUUID method + + NameBasedGenerator uuid_gen = Generators.nameBasedGenerator(); + assertEquals(uuid_gen.getType(), UUIDType.NAME_BASED_SHA1); + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) + { + uuid_array[i] = uuid_gen.generate("test name"+i); + } + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.NAME_BASED_SHA1); + + // check that all uuids were unique + checkUUIDArrayForUniqueness(uuid_array); + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) + { + uuid_array[i] = uuid_gen.generate("test name" + i); + } + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.NAME_BASED_SHA1); + + // check that all uuids were unique + checkUUIDArrayForUniqueness(uuid_array); + + // now, lets make sure generating two sets of name based uuid with the + // same args always gives the same result + uuid_array = new UUID[SIZE_OF_TEST_ARRAY]; + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) { + uuid_array[i] = uuid_gen.generate("test name" + i); + } + + UUID uuid_array2[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // now create the array of uuids + for (int i = 0; i < uuid_array2.length; i++) { + uuid_array2[i] = uuid_gen.generate("test name" + i); + } + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + checkUUIDArrayForNonNullUUIDs(uuid_array2); + + // check that all the uuids were correct variant and version + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.NAME_BASED_SHA1); + checkUUIDArrayForCorrectVariantAndVersion(uuid_array2, UUIDType.NAME_BASED_SHA1); + + // check that all uuids were unique + checkUUIDArrayForUniqueness(uuid_array); + checkUUIDArrayForUniqueness(uuid_array2); + + // check that both arrays are equal to one another + assertTrue("expected both arrays to be equal, they were not!", + Arrays.equals(uuid_array, uuid_array2)); + } + + /** + * Test of generateTimeBasedReorderedUUID() method, + * of class com.fasterxml.uuid.UUIDGenerator. + */ + public void testGenerateTimeBasedReorderedUUID() + { + // this test will attempt to check for reasonable behavior of the + // generateTimeBasedUUID method + + // we need a instance to use + TimeBasedReorderedGenerator uuid_gen = Generators.timeBasedReorderedGenerator(); + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED_REORDERED); + + // first check that given a number of calls to generateTimeBasedReorderedUUID, + // all returned UUIDs order after the last returned UUID + // we'll check this by generating the UUIDs into one array and sorting + // then in another and checking the order of the two match + // change the number in the array statement if you want more or less + // UUIDs to be generated and tested + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // before we generate all the uuids, lets get the start time + long start_time = System.currentTimeMillis(); + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) { + uuid_array[i] = uuid_gen.generate(); + } + + // now capture the end time + long end_time = System.currentTimeMillis(); + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version (type-1) + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.TIME_BASED_REORDERED); + + // check that all the uuids were generated with correct order + checkUUIDArrayForCorrectOrdering(uuid_array); + + // check that all uuids were unique + checkUUIDArrayForUniqueness(uuid_array); + + // check that all uuids have timestamps between the start and end time + checkUUIDArrayForCorrectCreationTimeReorder(uuid_array, start_time, end_time); + } + + /** + * Test of generateTimeBasedReorderedUUID(EthernetAddress) method, + * of class com.fasterxml.uuid.UUIDGenerator. + */ + public void testGenerateTimeBasedReorderedUUIDWithEthernetAddress() + { + // this test will attempt to check for reasonable behavior of the + // generateTimeBasedUUID(EthernetAddress) method + EthernetAddress ethernet_address = + new EthernetAddress("87:F5:93:06:D3:0C"); + + // we need a instance to use + TimeBasedReorderedGenerator uuid_gen = Generators.timeBasedReorderedGenerator(ethernet_address); + assertEquals(uuid_gen.getType(), UUIDType.TIME_BASED_REORDERED); + + // check that given a number of calls to generateTimeBasedReorderedUUID, + // all returned UUIDs order after the last returned UUID + // we'll check this by generating the UUIDs into one array and sorting + // then in another and checking the order of the two match + // change the number in the array statement if you want more or less + // UUIDs to be generated and tested + UUID uuid_array[] = new UUID[SIZE_OF_TEST_ARRAY]; + + // before we generate all the uuids, lets get the start time + long start_time = System.currentTimeMillis(); + + // now create the array of uuids + for (int i = 0; i < uuid_array.length; i++) { + uuid_array[i] = uuid_gen.generate(); + } + + // now capture the end time + long end_time = System.currentTimeMillis(); + + // check that none of the UUIDs are null + checkUUIDArrayForNonNullUUIDs(uuid_array); + + // check that all the uuids were correct variant and version (type-1) + checkUUIDArrayForCorrectVariantAndVersion(uuid_array, UUIDType.TIME_BASED_REORDERED); + + // check that all the uuids were generated with correct order + checkUUIDArrayForCorrectOrdering(uuid_array); + + // check that all uuids were unique + checkUUIDArrayForUniqueness(uuid_array); + + // check that all uuids have timestamps between the start and end time + checkUUIDArrayForCorrectCreationTimeReorder(uuid_array, start_time, end_time); + + // check that all UUIDs have the correct ethernet address in the UUID + checkUUIDArrayForCorrectEthernetAddress(uuid_array, ethernet_address); + } + /************************************************************************** * Begin Private Helper Methods for use in tests *************************************************************************/ - private class ReverseOrderUUIDComparator implements Comparator + + class ReverseOrderUUIDComparator implements Comparator { // this Comparator class has a compare which orders reverse of the // compareTo methond in UUID (so we can be sure our arrays below are @@ -467,7 +903,7 @@ private void checkUUIDArrayForUniqueness(UUID[] uuidArray) } private void checkUUIDArrayForCorrectVariantAndVersion(UUID[] uuidArray, - UUIDType expectedType) + UUIDType expectedType) { // let's check that all the UUIDs are valid type-X UUIDs with the // correct variant according to the specification. @@ -508,15 +944,16 @@ private void checkUUIDArrayForCorrectCreationTime(UUID[] uuidArray, long startTi // System.currenTimeMillis()... assertTrue("Start time: " + startTime +" was after the end time: " + endTime, startTime <= endTime); - + // let's check that all uuids in the array have a timestamp which lands // between the start and end time for (int i = 0; i < uuidArray.length; i++){ byte[] temp_uuid = UUIDUtil.asByteArray(uuidArray[i]); - + // first we'll collect the UUID time stamp which is // the number of 100-nanosecond intervals since // 00:00:00.00 15 October 1582 + long uuid_time = 0L; uuid_time |= ((temp_uuid[3] & 0xF0L) << 0); uuid_time |= ((temp_uuid[2] & 0xFFL) << 8); @@ -545,6 +982,80 @@ private void checkUUIDArrayForCorrectCreationTime(UUID[] uuidArray, long startTi } } + // Modified version for Version 6 (reordered timestamps) + private void checkUUIDArrayForCorrectCreationTimeReorder(UUID[] uuidArray, + long startTime, long endTime) + { + // we need to convert from 100-nanosecond units (as used in UUIDs) + // to millisecond units as used in UTC based time + final long MILLI_CONVERSION_FACTOR = 10000L; + // Since System.currentTimeMillis() returns time epoc time + // (from 1-Jan-1970), and UUIDs use time from the beginning of + // Gregorian calendar (15-Oct-1582) we have a offset for correction + final long GREGORIAN_CALENDAR_START_TO_UTC_START_OFFSET = + 122192928000000000L; + + // 21-Feb-2020, tatu: Not sure why this would be checked, as timestamps come from + // System.currenTimeMillis()... + assertTrue("Start time: " + startTime +" was after the end time: " + endTime, + startTime <= endTime); + + // let's check that all uuids in the array have a timestamp which lands + // between the start and end time + for (int i = 0; i < uuidArray.length; i++){ + byte[] temp_uuid = UUIDUtil.asByteArray(uuidArray[i]); + + // first we'll collect the UUID time stamp which is + // the number of 100-nanosecond intervals since + // 00:00:00.00 15 October 1582 + long uuid_time = 0L; + uuid_time |= ((temp_uuid[0] & 0xFFL) << 52); + uuid_time |= ((temp_uuid[1] & 0xFFL) << 44); + uuid_time |= ((temp_uuid[2] & 0xFFL) << 36); + uuid_time |= ((temp_uuid[3] & 0xFFL) << 28); + uuid_time |= ((temp_uuid[4] & 0xFFL) << 20); + uuid_time |= ((temp_uuid[5] & 0xFFL) << 12); + uuid_time |= ((temp_uuid[6] & 0x0FL) << 8); + uuid_time |= ((temp_uuid[7] & 0xFFL)); + + // first we'll remove the gregorian offset + uuid_time -= GREGORIAN_CALENDAR_START_TO_UTC_START_OFFSET; + + // and convert to milliseconds as the system clock is in millis + uuid_time /= MILLI_CONVERSION_FACTOR; + + // now check that the times are correct + assertTrue( + "Start time: " + startTime + + " was not before UUID timestamp: " + uuid_time, + startTime <= uuid_time); + assertTrue( + "UUID timestamp: " + uuid_time + + " was not before the end time: " + endTime, + uuid_time <= endTime); + } + } + + // Modified version for Variant 7 (Unix Epoch monotonic timestamps) + private void checkUUIDArrayForCorrectCreationTimeEpoch(UUID[] uuidArray, + long startTime, long endTime) + { + assertTrue("Start time: " + startTime + " was after the end time: " + endTime, startTime <= endTime); + + // let's check that all uuids in the array have a timestamp which lands + // between the start and end time + for (int i = 0; i < uuidArray.length; i++) { + byte[] temp_uuid = UUIDUtil.asByteArray(uuidArray[i]); + ByteBuffer buff = ByteBuffer.wrap(temp_uuid); + final long uuid_time = buff.getLong() >>> 16; + // now check that the times are correct + assertTrue("Start time: " + startTime + " was not before UUID timestamp: " + uuid_time, + startTime <= uuid_time); + assertTrue("UUID: " + i + " timestamp: " + uuid_time + " was not before the end time: " + endTime, + uuid_time <= endTime); + } + } + private void checkUUIDArrayForCorrectEthernetAddress(UUID[] uuidArray, EthernetAddress ethernetAddress) { diff --git a/src/test/java/com/fasterxml/uuid/UUIDTest.java b/src/test/java/com/fasterxml/uuid/UUIDTest.java index 74c2921..d688718 100644 --- a/src/test/java/com/fasterxml/uuid/UUIDTest.java +++ b/src/test/java/com/fasterxml/uuid/UUIDTest.java @@ -25,7 +25,6 @@ import java.util.Arrays; import java.util.UUID; -import com.fasterxml.uuid.UUIDType; import com.fasterxml.uuid.impl.UUIDUtil; /** @@ -35,7 +34,7 @@ */ public class UUIDTest extends TestCase { - final static UUID nullUUID = new UUID(0L, 0L); + final static UUID nullUUID = UUIDUtil.nilUUID(); public UUIDTest(java.lang.String testName) { @@ -52,10 +51,11 @@ public static void main(String[] args) { TestRunner.run(suite()); } - + /************************************************************************** * Begin constructor tests *************************************************************************/ + /** * Test of UUID() constructor, of class com.fasterxml.uuid.UUID. */ diff --git a/src/test/java/com/fasterxml/uuid/UUIDTimerTest.java b/src/test/java/com/fasterxml/uuid/UUIDTimerTest.java index c3991fc..bb699b0 100644 --- a/src/test/java/com/fasterxml/uuid/UUIDTimerTest.java +++ b/src/test/java/com/fasterxml/uuid/UUIDTimerTest.java @@ -30,8 +30,6 @@ import junit.framework.TestSuite; import junit.textui.TestRunner; -import com.fasterxml.uuid.UUIDTimer; - /** * JUnit Test class for the com.fasterxml.uuid.UUIDTimer class. * @@ -235,7 +233,7 @@ private Long[] convertArrayOfByteArraysToArrayOfLongs( return array_of_longs; } - private class ReverseOrderUUIDTimerLongComparator implements Comparator + class ReverseOrderUUIDTimerLongComparator implements Comparator { // this Comparator class has a compare which orders reverse of the // compare method in UUIDTimerArrayComparator (so we can be sure our diff --git a/src/test/java/com/fasterxml/uuid/ext/LockedFileTest.java b/src/test/java/com/fasterxml/uuid/ext/LockedFileTest.java new file mode 100644 index 0000000..cb9db2e --- /dev/null +++ b/src/test/java/com/fasterxml/uuid/ext/LockedFileTest.java @@ -0,0 +1,261 @@ +package com.fasterxml.uuid.ext; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import static com.fasterxml.uuid.ext.LockedFile.READ_ERROR; +import static org.junit.Assert.*; + +public class LockedFileTest +{ + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void constructor_givenNull_shouldThrowNullPointerException() throws IOException { + try { + new LockedFile(null); + fail("This should have thrown a null pointer exception"); + } catch (NullPointerException nullPointerException) { + ; // good + } + } + + @Test + public void constructor_givenEmptyFile_shouldLeaveFileAsIs() throws IOException { + // given + File emptyFile = temporaryFolder.newFile(); + + // when + new LockedFile(emptyFile); + + // then + assertTrue(emptyFile.exists()); + assertTrue(emptyFile.canRead()); + assertTrue(emptyFile.canWrite()); + } + + @Test + public void constructor_givenNonExistentFile_shouldCreateANewFile() throws IOException { + // given + File blankFile = temporaryFolder.newFile(); + File nonExistentFile = new File(blankFile + ".nonexistent"); + + if (Files.exists(nonExistentFile.toPath())) { + fail("temp file should not exist"); + } + + // when + new LockedFile(nonExistentFile); + + // then - the nonexistent file now exists? + assertTrue(Files.exists(nonExistentFile.toPath())); + assertTrue(nonExistentFile.canRead()); + assertTrue(nonExistentFile.canWrite()); + } + + @Test + public void constructor_canOnlyTakeAFile_shouldThrowFileNotFoundException() throws IOException { + // given + File blankFolder = temporaryFolder.newFolder(); + + // when + try { + new LockedFile(blankFolder); + fail("This should not succeed"); + } catch (FileNotFoundException fileNotFoundException) { + // then + assertEquals( + String.format("%s (Is a directory)", blankFolder.getPath()), + fileNotFoundException.getMessage() + ); + } + } + + @Test + public void readStamp_givenEmptyFile_shouldReturnREADERROR() throws IOException { + // given + File emptyFile = temporaryFolder.newFile(); + + // when + LockedFile lockedFile = new LockedFile(emptyFile); + long stamp = lockedFile.readStamp(); + + // then + assertEquals(READ_ERROR, stamp); + } + + @Test + public void readStamp_givenGibberishFile_shouldReturnREADERROR() throws IOException { + // given + File gibberishFile = temporaryFolder.newFile(); + try(FileWriter fileWriter = new FileWriter(gibberishFile)) { + fileWriter.write(UUID.randomUUID().toString().substring(0, 22)); + fileWriter.flush(); + } + + assertEquals(22, Files.size(gibberishFile.toPath())); + + // when + LockedFile lockedFile = new LockedFile(gibberishFile); + long stamp = lockedFile.readStamp(); + + // then + assertEquals(READ_ERROR, stamp); + } + + @Test + public void readStamp_givenTimestampedFile_shouldReturnValueInside() throws IOException { + // given + File timeStampedFile = temporaryFolder.newFile(); + try(FileWriter fileWriter = new FileWriter(timeStampedFile)) { + // we are faking the timestamp format + fileWriter.write("[0x0000000000000001]"); + fileWriter.flush(); + } + + // when + LockedFile lockedFile = new LockedFile(timeStampedFile); + long stamp = lockedFile.readStamp(); + + // then + long expectedTimestamp = 1; + assertEquals(expectedTimestamp, stamp); + } + + // test for overflows + @Test + public void readStamp_givenOverflowedDigitFile_shouldReturnREADERROR() throws IOException { + // given + File timeStampedFile = temporaryFolder.newFile(); + try(FileWriter fileWriter = new FileWriter(timeStampedFile)) { + // we are faking an overflowed timestamp + fileWriter.write("[0x10000000000000000]"); + fileWriter.flush(); + } + + // when + LockedFile lockedFile = new LockedFile(timeStampedFile); + long stamp = lockedFile.readStamp(); + + // then + assertEquals(READ_ERROR, stamp); + } + + @Test + public void readStamp_givenMaxLongFile_shouldReturnLargeTimestamp() throws IOException { + // given + File timeStampedFile = temporaryFolder.newFile(); + try(FileWriter fileWriter = new FileWriter(timeStampedFile)) { + // we are faking an overflowed timestamp + fileWriter.write("[0x7fffffffffffffff]"); + fileWriter.flush(); + } + + // when + LockedFile lockedFile = new LockedFile(timeStampedFile); + long stamp = lockedFile.readStamp(); + + // then + assertEquals(Long.MAX_VALUE, stamp); + } + + @Test + public void writeStamp_givenNegativeTimestamps_shouldThrowIOException() throws IOException { + // given + File timeStampedFile = temporaryFolder.newFile(); + + // when + LockedFile lockedFile = new LockedFile(timeStampedFile); + try { + lockedFile.writeStamp(Long.MIN_VALUE); + fail("This should throw an exception"); + } catch (IOException ioException) { + // then + assertTrue(ioException.getMessage().contains("trying to overwrite existing value")); + assertTrue(ioException.getMessage().contains("with an earlier timestamp")); + } + } + + @Test + public void writeStamp_givenTimestampedFile_withLowerValue_shouldOverrideValue() throws IOException { + // given + String inputValue = "[0x0000000000000000]"; + long numericInputValue = 0L; + long newTimestamp = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + + File timeStampedFile = temporaryFolder.newFile(); + try(FileWriter fileWriter = new FileWriter(timeStampedFile)) { + fileWriter.write(inputValue); + fileWriter.flush(); + } + + // when + LockedFile lockedFile = new LockedFile(timeStampedFile); + + lockedFile.writeStamp(newTimestamp); + long stamp = lockedFile.readStamp(); + + // then + assertNotEquals(numericInputValue, stamp); + assertEquals(newTimestamp, stamp); + } + + @Test + public void writeStamp_givenNewerTimestampedFile_writeNegativeTimestamp_shouldThrowException() throws IOException { + // given + String inputValue = "[0x7fffffffffffffff]"; + long newTimestamp = Long.MIN_VALUE; + + File timeStampedFile = temporaryFolder.newFile(); + try(FileWriter fileWriter = new FileWriter(timeStampedFile)) { + fileWriter.write(inputValue); + fileWriter.flush(); + } + + // when + LockedFile lockedFile = new LockedFile(timeStampedFile); + + try { + lockedFile.writeStamp(newTimestamp); + fail("This should throw an exception"); + } catch (IOException ioException) { + // then + assertTrue(ioException.getMessage().contains("trying to overwrite existing value")); + assertTrue(ioException.getMessage().contains("with an earlier timestamp")); + } + } + + @Test + public void writeStamp_givenTimestampedFile_writeSameTimestamp_shouldLeaveFileAlone() throws IOException { + // given + String inputValue = "[0x7fffffffffffffff]"; + long numericInputValue = Long.MAX_VALUE; + long newTimestamp = Long.MAX_VALUE; + + File timeStampedFile = temporaryFolder.newFile(); + try(FileWriter fileWriter = new FileWriter(timeStampedFile)) { + fileWriter.write(inputValue); + fileWriter.flush(); + } + + // when + LockedFile lockedFile = new LockedFile(timeStampedFile); + + lockedFile.writeStamp(newTimestamp); + long stamp = lockedFile.readStamp(); + + // then + assertEquals(numericInputValue, stamp); + assertEquals(newTimestamp, stamp); + } +} diff --git a/src/test/java/com/fasterxml/uuid/impl/UUIDUtilTest.java b/src/test/java/com/fasterxml/uuid/impl/UUIDUtilTest.java new file mode 100644 index 0000000..b730c00 --- /dev/null +++ b/src/test/java/com/fasterxml/uuid/impl/UUIDUtilTest.java @@ -0,0 +1,104 @@ +package com.fasterxml.uuid.impl; + +import java.util.Random; +import java.util.UUID; + +import com.fasterxml.uuid.Generators; +import com.fasterxml.uuid.NoArgGenerator; +import junit.framework.TestCase; + +/** + * Test class focusing on verifying functionality provided by + * {@link UUIDUtil}. + *

+ * NOTE: some of {@code UUIDUtil} testing is via main + * {@link com.fasterxml.uuid.UUIDTest}. + */ +public class UUIDUtilTest extends TestCase +{ + final static int TEST_REPS = 1_000_000; + + public void testNilUUID() { + UUID nil = UUIDUtil.nilUUID(); + // Should be all zeroes: + assertEquals(0L, nil.getMostSignificantBits()); + assertEquals(0L, nil.getLeastSignificantBits()); + } + + public void testMaxUUID() { + UUID max = UUIDUtil.maxUUID(); + // Should be all ones: + assertEquals(~0, max.getMostSignificantBits()); + assertEquals(~0, max.getLeastSignificantBits()); + } + + public void testExtractTimestampUUIDTimeBased() { + TimeBasedGenerator generator = Generators.timeBasedGenerator(); + final Random rnd = new Random(1); + for (int i = 0; i < TEST_REPS; i++) { + long rawTimestamp = rnd.nextLong() >>> 4; + UUID uuid = generator.construct(rawTimestamp); + assertEquals(rawTimestamp, UUIDUtil._getRawTimestampFromUuidV1(uuid)); + } + } + + public void testExtractTimestampUUIDTimeBasedCurrentTimemillis() { + TimeBasedGenerator generator = Generators.timeBasedGenerator(); + long time = System.currentTimeMillis(); + UUID uuid2 = generator.generate(); + assertEquals(time, UUIDUtil.extractTimestamp(uuid2)); + } + + + public void testExtractTimestampUUIDTimeBasedReordered() { + TimeBasedReorderedGenerator generator = Generators.timeBasedReorderedGenerator(); + final Random rnd = new Random(2); + for (int i = 0; i < TEST_REPS; i++) { + long rawTimestamp = rnd.nextLong() >>> 4; + UUID uuid = generator.construct(rawTimestamp); + assertEquals(rawTimestamp, UUIDUtil._getRawTimestampFromUuidV6(uuid)); + } + } + + public void testExtractTimestampUUIDTimeBasedReorderedCurrentTimeMillis() { + NoArgGenerator generator = Generators.timeBasedReorderedGenerator(); + long time = System.currentTimeMillis(); + UUID uuid = generator.generate(); + assertEquals(time, UUIDUtil.extractTimestamp(uuid)); + } + + public void testExtractTimestampUUIDEpochBased() { + TimeBasedEpochGenerator generator = Generators.timeBasedEpochGenerator(); + final Random rnd = new Random(3); + for (int i = 0; i < TEST_REPS; i++) { + long rawTimestamp = rnd.nextLong() >>> 16; + UUID uuid = generator.construct(rawTimestamp); + assertEquals(rawTimestamp, UUIDUtil.extractTimestamp(uuid)); + } + } + + public void testExtractTimestampUUIDEpochBasedCurrentTimeMillis() { + NoArgGenerator generator = Generators.timeBasedEpochGenerator(); + long time = System.currentTimeMillis(); + UUID uuid = generator.generate(); + assertEquals(time, UUIDUtil.extractTimestamp(uuid)); + } + + + public void testExtractTimestampUUIDEpochRandomBased() { + TimeBasedEpochRandomGenerator generator = Generators.timeBasedEpochRandomGenerator(); + final Random rnd = new Random(3); + for (int i = 0; i < TEST_REPS; i++) { + long rawTimestamp = rnd.nextLong() >>> 16; + UUID uuid = generator.construct(rawTimestamp); + assertEquals(rawTimestamp, UUIDUtil.extractTimestamp(uuid)); + } + } + + public void testExtractTimestampUUIDOnOtherValues() { + assertEquals(0L, UUIDUtil.extractTimestamp(null)); + assertEquals(0L, UUIDUtil.extractTimestamp(UUID.fromString("00000000-0000-0000-0000-000000000000"))); + assertEquals(0L, UUIDUtil.extractTimestamp(UUIDUtil.nilUUID())); + assertEquals(0L, UUIDUtil.extractTimestamp(UUIDUtil.maxUUID())); + } +} diff --git a/src/test/java/perf/MeasurePerformanceTest.java b/src/test/java/perf/MeasurePerformanceTest.java new file mode 100644 index 0000000..fdc81c7 --- /dev/null +++ b/src/test/java/perf/MeasurePerformanceTest.java @@ -0,0 +1,17 @@ +package perf; + +import org.junit.Test; + +// Things we do for Code Coverage... altough "perf/MeasurePerformance.java" +// is only to be manually run, it is included in build, so +// we get code coverage whether we want it or not. So let's have +// a silly little driver to exercise it from unit tests and avoid dinging +// overall test coverage +public class MeasurePerformanceTest +{ + @Test + public void runMinimalPerfTest() throws Exception + { + new MeasurePerformance(10, false).test(); + } +}