Skip to content

Commit

Permalink
Merge pull request #18 from tomtom-international/dev
Browse files Browse the repository at this point in the history
Added Uid class
  • Loading branch information
rijnb authored Aug 19, 2020
2 parents e9e93e4 + 086e48f commit 55d33e1
Show file tree
Hide file tree
Showing 6 changed files with 435 additions and 8 deletions.
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,36 @@ Advanced examples of using this trace event mechanism are:
For more information, for example on how to define and register trace event consumers, please refer
to the documentation in the `Tracer` class.

## Module: Uid

Generic immutable unique ID class. Really just an abstraction of UUIDs. Used to uniquely
identify things. No 2 Uid objects shall be equal unless they are the same instance of the
Uid class or their underlying UUIDs are equal.

The class has a generic type T to allow creating typesafe Uid's, like `Uid<Message>`.
The class represents UUIDs as Strings internally, to avoid loads of UUID to String conversions
all the time. This makes the class considerably faster in use than the regular Java UUID class.

Examples:
```kotlin
var messageId = Uid<Message>() // Creates a new unique message ID.
var personId = Uid<Person>() // Creates a new unique person ID.

messageId = personId // <-- Does not compile, the IDs are type safe.

val testId = Uid.fromString("1-2-3-4-5") // Allows shorthand notation for UUIDs,
// easy for testing, or input.

val s = messageId.toString() // Serialized to string.
val id = Uid<Message>(s) // Deserialized from string. Faster than `fromString`
// if the format is known to be the serialized format.

val messageId : Uid<Message>() // If you need, you can translate IDs from one
val personId : messageId as Uid<Person> // type to another using 'as'. This is useful if
// the type information was lost, for example,
// in serialization.
```

## License

Copyright (C) 2020-2020, TomTom (http://tomtom.com).
Expand Down Expand Up @@ -440,7 +470,11 @@ Contributors: Timon Kanters, Jeroen Erik Jensen

## Release notes

### 1.0.17
### 1.1.0

* Added `Uid` class for UUID handling.

### 1.0.17-1.0.18

* Minor bug fixes.

Expand Down
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<groupId>com.tomtom.kotlin</groupId>
<artifactId>kotlin-tools</artifactId>
<version>1.0.17</version>
<version>1.1.0</version>
<packaging>pom</packaging>

<name>Kotlin Tools</name>
Expand Down Expand Up @@ -72,6 +72,7 @@

<modules>
<module>traceevents</module>
<module>uid</module>
</modules>

<properties>
Expand Down
13 changes: 7 additions & 6 deletions traceevents/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<parent>
<groupId>com.tomtom.kotlin</groupId>
<artifactId>kotlin-tools</artifactId>
<version>1.0.17</version>
<version>1.1.0</version>
</parent>

<artifactId>traceevents</artifactId>
Expand All @@ -42,11 +42,6 @@
</description>

<dependencies>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
Expand All @@ -64,6 +59,12 @@

<!-- Test scope. -->

<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
69 changes: 69 additions & 0 deletions uid/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 2020-2020, TomTom (http://tomtom.com).
~ 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.
-->

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<!-- Use the speedtools shared pom. -->
<parent>
<groupId>com.tomtom.kotlin</groupId>
<artifactId>kotlin-tools</artifactId>
<version>1.1.0</version>
</parent>

<artifactId>uid</artifactId>
<packaging>jar</packaging>

<name>Uid</name>

<!--
Don't add a description if you want the project not to be picked up by the maven-mailinglist-plugin
(if the plugin is enabled).
-->
<description>
Uids uses UUIDs for unique IDs, but are more efficient in their handling.
</description>

<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>

<!-- Test scope. -->

<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
198 changes: 198 additions & 0 deletions uid/src/main/java/com/tomtom/kotlin/uid/Uid.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Copyright (C) 2020-2020, TomTom (http://tomtom.com).
* 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.
*/
package com.tomtom.kotlin.uid

import java.util.UUID


/**
* Generic immutable unique ID class. Really just an abstraction of UUIDs. Used to uniquely
* identify things.
*
* The class has a generic type T to allow creating typesafe IDs, like Uid<Message> or Uid<Person>.
* The class represents UUIDs as Strings internally, to avoid loads of UUID to String conversions
* all the time. This makes the class considerably faster in use than the regular [UUID] class.
*
* @param T Type tag for ID, to make IDs type-safe.
*/
class Uid<T> {
private val uuid: String

/**
* Create a new, unique UUID-based ID.
*/
constructor() {
uuid = UUID.randomUUID().toString()
}

/**
* Instantiates a [Uid] with a string. Mainly used when de-serializing existing entities.
*
* The format of 'uuid' is checked to comply with a standard UUID format, which is:
* - Dashes at positions 8, 13, 18, 23 (base 0).
* - Characters 0-9 and a-f (lowercase only).
*
* If this format is used, the creation of the [Uid] is very fast. If an alternative format
* us used, as accepted by [fromString], the call is much more expensive.
*
* @param uuidAsString An existing string representation of a UUID.
* @throws IllegalArgumentException If name does not conform to the string representation
* as described in [UUID.toString]. Use [isValid] to make sure the string is valid.
*/
constructor(uuidAsString: String) {
/**
* This code has been optimized to NOT just call UUID.fromString(uuid) to convert the
* UUID-String into a String (and catch an IllegalArgumentException).
*
* If the UUID does not comply, the expensive call to UUID.fromString is made after all.
*/
val length = uuidAsString.length
require(length in UUID_MIN_LENGTH..UUID_MAX_LENGTH) {
"Length of UUID must be [" + UUID_MIN_LENGTH + ", " +
UUID_MAX_LENGTH + "], but is " + uuidAsString.length + ", uuid=" + uuidAsString
}

// Check dashes.
this.uuid = if (areDashesAtCorrectPosition(uuidAsString)) {
uuidAsString.toLowerCase().also {
require(onlyContainsValidUuidCharacters(it)) {
"Incorrect UUID format, uuid=$uuidAsString"
}
}
} else {
UUID.fromString(uuidAsString).toString().toLowerCase()
}
}

/**
* Instantiates an ID with a [UUID].
*
* @param uuidAsUuid Existing [UUID].
*/
private constructor(uuidAsUuid: UUID) {
this.uuid = uuidAsUuid.toString()
}

/**
* Returns hex string representation of this Uid. Opposite of [fromHexString].
* This representation is shorthand, it does not include dashes.
*
* @return Hex string representation of ID, exactly 32 characters long.
*/
fun toHexString(): String {
val uuid2 = UUID.fromString(uuid)
val msb = java.lang.Long.toHexString(uuid2.mostSignificantBits)
val lsb = java.lang.Long.toHexString(uuid2.leastSignificantBits)
return "${msb.padStart(16, '0')}${lsb.padStart(16, '0')}"
}

/**
* Method converts given String representation to [Uid] and compares it with this instance.
* A String value of "0-0-0-0-0" would match a [Uid] of "00000-0000-0000-000000000-00" or so.
*
* @param uid String representation od Uid.
* @return True in case String representation matches instance. False otherwise.
*/
fun matchesFromString(uid: String) = this == fromString<T>(uid)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Uid<*>) return false
if (uuid != other.uuid) return false
return true
}

override fun hashCode() = uuid.hashCode()

/**
* Returns the string representation of this Uid. Opposite of [fromString].
*
* @return String representation of ID.
*/
override fun toString() = uuid

companion object {
private const val UUID_DASH = '-'
private const val UUID_MIN_LENGTH = 9
private const val UUID_MAX_LENGTH = 36
private val UUID_DASH_POS = intArrayOf(8, 13, 18, 23)

/**
* Returns an ID if it is a valid UUID, or `null` if it's not.
*
* @param id String representation of UUID.
* @return Valid UUID, or null.
*/
fun <T> fromStringIfValid(id: String?) =
if (id == null) {
null
} else try {
Uid<T>(id)
} catch (ignored: IllegalArgumentException) {
null
}

/**
* Instantiates a [Uid] with given ID as a string. Mainly used for deserialization.
* Opposite of [toString].
*
* @param <T> Uid type.
* @param id String representation of ID.
* @return Uid.
* @throws IllegalArgumentException If name does not conform to the string
* representation as described in [UUID.toString]. Use [isValid] to make sure the
* string is valid.
</T> */
fun <T> fromString(id: String) = Uid<T>(id)

/**
* Instantiates a [Uid] with given ID as hex-formatted string. Opposite of [toHexString].
*
* @param <T> Uid type.
* @param id Hex string representation of ID, must be exactly 32 characters long.
* @return Uid.
* @throws IllegalArgumentException If name does not conform to the string
* representation as described in [UUID.toString]. Use [isValid] to make sure the
* string is valid.
</T> */
fun <T> fromHexString(id: String): Uid<T> {
require(id.length == 32)
val msb: Long =
id.substring(0, 8).toLong(16) shl 32 or id.substring(8, 16).toLong(16)
val lsb: Long =
id.substring(16, 24).toLong(16) shl 32 or id.substring(24, 32).toLong(16)
return Uid<T>(UUID(msb, lsb))
}

/**
* Return if string contains valid UUID characters only.
* Must be converted to lowercase already.
*
* @param uuid Input UUID. Should be lowercase already.
* @return True if valid characters only.
*/
private fun onlyContainsValidUuidCharacters(uuid: String) =
uuid.toCharArray().all { it in '0'..'9' || it in 'a'..'f' || it == UUID_DASH }

/**
* Checks if the dashes are at the right positions for a UUID.
*
* @param s Input string.
* @return True if the dashes are correctly placed.
*/
private fun areDashesAtCorrectPosition(s: String) =
UUID_DASH_POS.all { s.length > it && s[it] == '-' }
}
}
Loading

0 comments on commit 55d33e1

Please sign in to comment.