├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── test └── java │ └── com │ └── github │ └── shamil │ └── XidTest.java └── main └── java └── com └── github └── shamil └── Xid.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk8 5 | 6 | install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -Dgpg.skip 7 | 8 | script: "mvn cobertura:cobertura" 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License 3 | 4 | Copyright (C) 2020 Shamil 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Globally Unique ID Generator 2 | 3 | [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/0xShamil/java-xid/master/LICENSE) [![Build Status](https://travis-ci.org/0xShamil/java-xid.svg?branch=master)](https://travis-ci.org/0xShamil/java-xid) [![codecov](https://codecov.io/gh/0xShamil/java-xid/branch/master/graph/badge.svg)](https://codecov.io/gh/0xShamil/java-xid) 4 | 5 | 6 | 7 | ###### This project is a Java implementation of the Go Lang library found here: [https://github.com/rs/xid](https://github.com/rs/xid) 8 | 9 | --- 10 | 11 | ## Description 12 | 13 | `Xid` is a globally unique id generator library. They are small, fast to generate and ordered. 14 | 15 | Xid uses the *Mongo Object ID* algorithm to generate globally unique ids with a different serialization (base32) to make it shorter when transported as a string: 16 | https://docs.mongodb.org/manual/reference/object-id/ 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Xid layout
01234567891011
timerandom valueinc
27 | 28 | - a 4-byte value representing the seconds since the Unix epoch 29 | - a 5-byte random value 30 | - a 3-byte incrementing counter, initialized to a random value 31 | 32 | The binary representation of the id is compatible with Mongo 12 bytes Object IDs. 33 | The string representation is using [base32 hex (w/o padding)](https://tools.ietf.org/html/rfc4648#page-10) for better space efficiency when stored in that form (20 bytes). The hex variant of base32 is used to retain the 34 | sortable property of the id. 35 | 36 | `Xid`s simply offer uniqueness and speed, but they are not cryptographically secure. They are predictable and can be *brute forced* given enough time. 37 | 38 | ## Features 39 | - Size: 12 bytes (96 bits), smaller than UUID, larger than [Twitter Snowflake](https://blog.twitter.com/2010/announcing-snowflake) 40 | - Base32 hex encoded by default (20 chars when transported as printable string, still sortable) 41 | - Configuration free: there is no need to set a unique machine and/or data center id 42 | - K-ordered 43 | - Embedded time with 1 second precision 44 | - Unicity guaranteed for 16,777,216 (24 bits) unique ids per second and per host/process 45 | - Lock-free (unlike UUIDv1 and v2) 46 | 47 | ## Comparison 48 | 49 | | Name | Binary Size | String Size | Features 50 | |-------------|-------------|----------------|---------------- 51 | | [UUID] | 16 bytes | 36 chars | configuration free, not sortable 52 | | [shortuuid] | 16 bytes | 22 chars | configuration free, not sortable 53 | | [Snowflake] | 8 bytes | up to 20 chars | needs machine/DC configuration, needs central server, sortable 54 | | [MongoID] | 12 bytes | 24 chars | configuration free, sortable 55 | | xid | 12 bytes | 20 chars | configuration free, sortable 56 | 57 | [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier 58 | [shortuuid]: https://github.com/stochastic-technologies/shortuuid 59 | [Snowflake]: https://blog.twitter.com/2010/announcing-snowflake 60 | [MongoID]: https://docs.mongodb.org/manual/reference/object-id/ 61 | 62 | ## Installation 63 | 64 | ### Gradle 65 | 66 | ```gradle 67 | dependencies { 68 | implementation 'com.github.0xshamil:java-xid:1.0.0' 69 | } 70 | ``` 71 | 72 | ### Maven 73 | 74 | ```xml 75 | 76 | com.github.0xshamil 77 | java-xid 78 | 1.0.0 79 | 80 | ``` 81 | 82 | ## Usage 83 | Get `Xid` instance 84 | ```java 85 | final Xid xid = Xid.get(); 86 | 87 | System.out.println(xid.toString()); // 9m4e2mr0ui3e8a215n4g 88 | ``` 89 | as base32Hex `String` 90 | 91 | ```java 92 | final String xidStr = Xid.string(); // bt0j9l2s5bo37fcla7q0 93 | ``` 94 | as `byte` array: 95 | 96 | ```java 97 | final byte[] xidBytes = Xid.bytes(); 98 | ``` 99 | 100 | to create an `Xid` from a specific date 101 | 102 | ```java 103 | final String d = "10-Aug-2020 09:43:29 +0000"; 104 | final String dateFormat = "dd-MMM-yyyy HH:mm:ss Z"; 105 | Date date = new SimpleDateFormat(dateFormat).parse(d); 106 | final Xid xid = new Xid(date); 107 | 108 | System.out.println(xid.toString()); // bsohdgdl8njn9eimov6g 109 | System.out.println(xid.getDate()); // Mon Aug 10 15:13:29 IST 2020 110 | System.out.println(xid.getTimestamp()); // 1597052609 111 | ``` 112 | 113 | to construct back `Xid` from a hex string: 114 | ```java 115 | final Xid xid = new Xid("bsohdgdl8njn9eimov6g"); 116 | 117 | System.out.println(xid.getDate()); // Mon Aug 10 15:13:29 IST 2020 118 | System.out.println(xid.getTimestamp()); // 1597052609 119 | ``` 120 | 121 | ## Licenses 122 | The source code is licensed under the [MIT License](https://github.com/0xShamil/java-xid/master/LICENSE). 123 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.0xshamil 8 | java-xid 9 | 1.0.0 10 | jar 11 | java-xid 12 | xid is a globally unique id generator 13 | https://github.com/0xShamil/java-xid 14 | 15 | 16 | 17 | MIT License 18 | https://github.com/0xShamil/java-xid/blob/master/LICENSE 19 | repo 20 | 21 | 22 | 23 | 24 | 25 | 0xShamil 26 | https://github.com/0xShamil 27 | 28 | 29 | 30 | 31 | scm:git:git://github.com:0xShamil/java-xid.git 32 | scm:git:ssh://github.com:0xShamil/java-xid.git 33 | https://github.com/0xShamil/java-xid/blob/master 34 | 35 | 36 | 37 | 38 | ossrh 39 | https://oss.sonatype.org/content/repositories/snapshots 40 | 41 | 42 | ossrh 43 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.apache.maven.plugins 51 | maven-compiler-plugin 52 | 53 | 8 54 | 8 55 | 56 | 57 | 58 | org.codehaus.mojo 59 | cobertura-maven-plugin 60 | 2.7 61 | 62 | 63 | html 64 | xml 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | release 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-deploy-plugin 80 | 3.0.0-M1 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-source-plugin 85 | 3.2.1 86 | 87 | 88 | attach-sources 89 | 90 | jar-no-fork 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-javadoc-plugin 98 | 3.2.0 99 | 100 | 101 | attach-javadocs 102 | 103 | jar 104 | 105 | 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-gpg-plugin 111 | 1.6 112 | 113 | 114 | sign-artifacts 115 | verify 116 | 117 | sign 118 | 119 | 120 | 121 | 122 | 123 | org.sonatype.plugins 124 | nexus-staging-maven-plugin 125 | 1.6.8 126 | true 127 | 128 | ossrh 129 | https://oss.sonatype.org/ 130 | true 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | org.junit.jupiter 141 | junit-jupiter-api 142 | 5.6.2 143 | test 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/test/java/com/github/shamil/XidTest.java: -------------------------------------------------------------------------------- 1 | package com.github.shamil; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.text.ParseException; 7 | import java.text.SimpleDateFormat; 8 | import java.util.ArrayList; 9 | import java.util.Date; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Locale; 13 | import java.util.Map; 14 | import java.util.Random; 15 | import java.util.TimeZone; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.junit.jupiter.api.Assertions.assertTrue; 21 | 22 | 23 | class XidTest { 24 | 25 | @Test 26 | public void testToBytes() { 27 | byte[] expectedBytes = new byte[]{81, 6, -4, -102, -68, -126, 55, 85, -127, 54, -46, -119}; 28 | Xid id = new Xid(expectedBytes); 29 | 30 | assertArrayEquals(expectedBytes, id.toByteArray()); 31 | 32 | ByteBuffer buffer = ByteBuffer.allocate(12); 33 | id.putToByteBuffer(buffer); 34 | assertArrayEquals(expectedBytes, buffer.array()); 35 | } 36 | 37 | @Test 38 | public void testFromBytes() { 39 | byte[] bytes = new byte[]{81, 6, -4, -102, -68, -126, 55, 85, -127, 54, -46, -119}; 40 | 41 | Xid xid1 = new Xid(bytes); 42 | assertEquals(0x5106FC9A, xid1.getTimestamp()); 43 | 44 | Xid xid2 = new Xid(ByteBuffer.wrap(bytes)); 45 | assertEquals(0x5106FC9A, xid2.getTimestamp()); 46 | } 47 | 48 | @Test 49 | public void testLengthValidation() { 50 | Throwable whenNull = assertThrows( 51 | IllegalArgumentException.class, 52 | () -> new Xid((byte[]) null) 53 | ); 54 | assertEquals("bytes can not be null", whenNull.getMessage()); 55 | 56 | Throwable whenLengthIsLessThanExpected = assertThrows( 57 | IllegalArgumentException.class, 58 | () -> new Xid(new byte[11]) 59 | ); 60 | assertEquals("state should be: bytes has length of 12", whenLengthIsLessThanExpected.getMessage()); 61 | 62 | Throwable whenLengthIsMoreThanExpected = assertThrows( 63 | IllegalArgumentException.class, 64 | () -> new Xid(new byte[13]) 65 | ); 66 | assertEquals("state should be: bytes has length of 12", whenLengthIsMoreThanExpected.getMessage()); 67 | } 68 | 69 | @Test 70 | public void testBytesRoundTrip() { 71 | Xid expected = new Xid(); 72 | Xid actual = new Xid(expected.toByteArray()); 73 | assertEquals(expected, actual); 74 | 75 | byte[] b = new byte[12]; 76 | Random r = new Random(17); 77 | for (int i = 0; i < b.length; i++) { 78 | b[i] = (byte) (r.nextInt()); 79 | } 80 | expected = new Xid(b); 81 | assertEquals(expected, new Xid(expected.toByteArray())); 82 | } 83 | 84 | @Test 85 | public void testGetSmallestWithDate() { 86 | Date date = new Date(1588467737760L); 87 | byte[] expectedBytes = new byte[]{94, -82, 24, 25, 0, 0, 0, 0, 0, 0, 0, 0}; 88 | Xid xid = Xid.getSmallestWithDate(date); 89 | assertArrayEquals(expectedBytes, xid.toByteArray()); 90 | assertEquals(date.getTime() / 1000 * 1000, xid.getDate().getTime()); 91 | assertEquals(-1, xid.compareTo(new Xid(date))); 92 | } 93 | 94 | @Test 95 | public void testGetTimeZero() { 96 | assertEquals(0L, new Xid(0, 0).getDate().getTime()); 97 | } 98 | 99 | @Test 100 | public void testGetTimeMaxSignedInt() { 101 | assertEquals(0x7FFFFFFFL * 1000, new Xid(0x7FFFFFFF, 0).getDate().getTime()); 102 | } 103 | 104 | @Test 105 | public void testGetTimeMaxSignedIntPlusOne() { 106 | assertEquals(0x80000000L * 1000, new Xid(0x80000000, 0).getDate().getTime()); 107 | } 108 | 109 | @Test 110 | public void testGetTimeMaxInt() { 111 | assertEquals(0xFFFFFFFFL * 1000, new Xid(0xFFFFFFFF, 0).getDate().getTime()); 112 | } 113 | 114 | @Test 115 | public void testTime() { 116 | long a = System.currentTimeMillis(); 117 | long b = (new Xid()).getDate().getTime(); 118 | assertTrue(Math.abs(b - a) < 3000); 119 | } 120 | 121 | @Test 122 | public void testDateCons() { 123 | assertEquals(new Date().getTime() / 1000, new Xid(new Date()).getDate().getTime() / 1000); 124 | } 125 | 126 | @Test 127 | public void testHexStringConstructor() { 128 | Xid id = new Xid(); 129 | assertEquals(id, new Xid(id.toHexString())); 130 | } 131 | 132 | @Test 133 | public void testCompareTo() { 134 | Date dateOne = new Date(); 135 | Date dateTwo = new Date(dateOne.getTime() + 10000); 136 | Xid first = new Xid(dateOne, 0); 137 | Xid second = new Xid(dateOne, 1); 138 | Xid third = new Xid(dateTwo, 0); 139 | assertEquals(0, first.compareTo(first)); 140 | assertEquals(-1, first.compareTo(second)); 141 | assertEquals(-1, first.compareTo(third)); 142 | assertEquals(1, second.compareTo(first)); 143 | assertEquals(1, third.compareTo(first)); 144 | } 145 | 146 | @Test 147 | public void testToHexString() { 148 | assertEquals("00000000000000000000", new Xid(new byte[12]).toHexString()); 149 | assertEquals("9m4e2mr0ui3e8a215n4g", 150 | new Xid(new byte[]{(byte) 0x4d, (byte) 0x88, (byte) 0xe1, (byte) 0x5b, (byte) 0x60, (byte) 0xf4, (byte) 0x86, (byte) 0xe4, (byte) 0x28, (byte) 0x41, (byte) 0x2d, (byte) 0xc9}).toHexString()); 151 | } 152 | 153 | @Test 154 | public void testFromHexString() { 155 | byte[] actual = new Xid("9m4e2mr0ui3e8a215n4g").toByteArray(); 156 | byte[] expected = new byte[]{(byte) 0x4d, (byte) 0x88, (byte) 0xe1, (byte) 0x5b, (byte) 0x60, (byte) 0xf4, (byte) 0x86, (byte) 0xe4, (byte) 0x28, (byte) 0x41, (byte) 0x2d, (byte) 0xc9}; 157 | assertArrayEquals(expected, actual); 158 | } 159 | 160 | private Date getDate(final String s) throws ParseException { 161 | return new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z").parse(s); 162 | } 163 | 164 | @Test 165 | public void testTimeZero() throws ParseException { 166 | assertEquals(getDate("01-Jan-1970 00:00:00 -0000"), new Xid(0, 0).getDate()); 167 | } 168 | 169 | @Test 170 | public void testTimeMaxSignedInt() throws ParseException { 171 | assertEquals(getDate("19-Jan-2038 03:14:07 -0000"), new Xid(0x7FFFFFFF, 0).getDate()); 172 | } 173 | 174 | @Test 175 | public void testTimeMaxSignedIntPlusOne() throws ParseException { 176 | assertEquals(getDate("19-Jan-2038 03:14:08 -0000"), new Xid(0x80000000, 0).getDate()); 177 | } 178 | 179 | @Test 180 | public void testTimeMaxInt() throws ParseException { 181 | assertEquals(getDate("07-Feb-2106 06:28:15 -0000"), new Xid(0xFFFFFFFF, 0).getDate()); 182 | } 183 | 184 | @Test 185 | void testIntervals() { 186 | List ids = new ArrayList<>(); 187 | for (int i = 0; i < 10; i++) { 188 | ids.add(Xid.get()); 189 | } 190 | 191 | for (int i = 1; i < 10; i++) { 192 | Xid previousId = ids.get(i - 1); 193 | Xid currentId = ids.get(i); 194 | 195 | int diffSecs = currentId.getTimestamp() - previousId.getTimestamp(); 196 | // test that both ids generated within same second 197 | assertEquals(0, diffSecs); 198 | 199 | // test currentId is greater than the previous 200 | assertTrue(currentId.compareTo(previousId) > 0); 201 | } 202 | } 203 | 204 | @Test 205 | public void testNoCollisions() { 206 | assertTrue(hasNoCollisions(1000000)); 207 | } 208 | 209 | private boolean hasNoCollisions(int iterations) { 210 | Map ids = new HashMap<>(); 211 | for (int i = 0; i < iterations; i++) { 212 | String id = Xid.string(); 213 | if (ids.containsKey(id)) { 214 | return false; 215 | } else { 216 | ids.put(id, id); 217 | } 218 | } 219 | return true; 220 | } 221 | 222 | } -------------------------------------------------------------------------------- /src/main/java/com/github/shamil/Xid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008-present MongoDB, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * The MIT License 19 | * 20 | * Copyright (C) 2020 Shamil 21 | * 22 | * Permission is hereby granted, free of charge, to any person obtaining 23 | * a copy of this software and associated documentation files (the 24 | * "Software"), to deal in the Software without restriction, including 25 | * without limitation the rights to use, copy, modify, merge, publish, 26 | * distribute, sublicense, and/or sell copies of the Software, and to 27 | * permit persons to whom the Software is furnished to do so, subject to 28 | * the following conditions: 29 | * 30 | * The above copyright notice and this permission notice shall be 31 | * included in all copies or substantial portions of the Software. 32 | * 33 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 34 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 35 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 36 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 37 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 38 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 39 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | */ 41 | 42 | package com.github.shamil; 43 | 44 | import java.nio.ByteBuffer; 45 | import java.security.SecureRandom; 46 | import java.util.Date; 47 | import java.util.concurrent.atomic.AtomicInteger; 48 | 49 | /** 50 | *

A globally unique identifier for objects.

51 | * 52 | *

Consists of 12 bytes, divided as follows:

53 | * 54 | * 55 | * 56 | * 57 | * 58 | * 59 | * 60 | * 61 | *
layout
01234567891011
timerandom valueinc
62 | * 63 | *

Instances of this class are immutable.

64 | */ 65 | public final class Xid implements Comparable { 66 | private static final int ID_LENGTH = 12; 67 | private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff; 68 | 69 | // Use primitives to represent the 5-byte random value. 70 | private static final int RANDOM_VALUE1; 71 | private static final short RANDOM_VALUE2; 72 | 73 | private static final AtomicInteger NEXT_COUNTER = new AtomicInteger(new SecureRandom().nextInt()); 74 | 75 | private static final char[] BASE32_HEX_CHARS = new char[]{ 76 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 77 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 78 | 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 79 | 'u', 'v', 80 | }; 81 | private static final int[] BASE32_LOOKUP_TABLE = { 82 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // '0', '1', '2', '3', '4', '5', '6', '7' 83 | 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // '8', '9', ':', ';', '<', '=', '>', '?' 84 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G' 85 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O' 86 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W' 87 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_' 88 | 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g' 89 | 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' 90 | 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0xFF, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' 91 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' 92 | }; 93 | 94 | private final int timestamp; 95 | private final int counter; 96 | private final int randomValue1; 97 | private final short randomValue2; 98 | 99 | static { 100 | try { 101 | SecureRandom secureRandom = new SecureRandom(); 102 | RANDOM_VALUE1 = secureRandom.nextInt(0x01000000); 103 | RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000); 104 | } catch (Exception e) { 105 | throw new RuntimeException(e); 106 | } 107 | } 108 | 109 | /** 110 | * Create a new object id. 111 | */ 112 | public Xid() { 113 | this(new Date()); 114 | } 115 | 116 | /** 117 | * Constructs a new instance using the given date. 118 | * 119 | * @param date the date 120 | */ 121 | public Xid(final Date date) { 122 | this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false); 123 | } 124 | 125 | /** 126 | * Constructs a new instances using the given date and counter. 127 | * 128 | * @param date the date 129 | * @param counter the counter 130 | * @throws IllegalArgumentException if the high order byte of counter is not zero 131 | */ 132 | public Xid(final Date date, 133 | final int counter) { 134 | this(dateToTimestampSeconds(date), counter, true); 135 | } 136 | 137 | /** 138 | * Creates an Xid using the given time, machine identifier, process identifier, and counter. 139 | * 140 | * @param timestamp the time in seconds 141 | * @param counter the counter 142 | * @throws IllegalArgumentException if the high order byte of counter is not zero 143 | */ 144 | public Xid(final int timestamp, 145 | final int counter) { 146 | this(timestamp, counter, true); 147 | } 148 | 149 | private Xid(final int timestamp, 150 | final int counter, 151 | final boolean checkCounter) { 152 | this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter); 153 | } 154 | 155 | private Xid(final int timestamp, 156 | final int randomValue1, 157 | final short randomValue2, 158 | final int counter, 159 | final boolean checkCounter) { 160 | if ((randomValue1 & 0xff000000) != 0) { 161 | throw new IllegalArgumentException("The machine identifier must be between 0 and 16777215 (it must fit in three bytes)."); 162 | } 163 | if (checkCounter && ((counter & 0xff000000) != 0)) { 164 | throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes)."); 165 | } 166 | this.timestamp = timestamp; 167 | this.counter = counter & LOW_ORDER_THREE_BYTES; 168 | this.randomValue1 = randomValue1; 169 | this.randomValue2 = randomValue2; 170 | } 171 | 172 | /** 173 | * Constructs a new instance from a 24-byte hexadecimal string representation. 174 | * 175 | * @param hexString the string to convert 176 | * @throws IllegalArgumentException if the string is not a valid hex string representation of an Xid 177 | */ 178 | public Xid(final String hexString) { 179 | this(parseHexString(hexString)); 180 | } 181 | 182 | /** 183 | * Constructs a new instance from the given byte array 184 | * 185 | * @param bytes the byte array 186 | * @throws IllegalArgumentException if array is null or not of length 12 187 | */ 188 | public Xid(final byte[] bytes) { 189 | this(ByteBuffer.wrap(isTrue("bytes has length of 12", bytes, paramNotNull("bytes", bytes).length == 12))); 190 | } 191 | 192 | /** 193 | * Constructs a new instance from the given ByteBuffer 194 | * 195 | * @param buffer the ByteBuffer 196 | * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining 197 | */ 198 | public Xid(final ByteBuffer buffer) { 199 | paramNotNull("buffer", buffer); 200 | isTrue("buffer.remaining() >=12", buffer.remaining() >= ID_LENGTH); 201 | 202 | // Note: Cannot use ByteBuffer.getInt because it depends on tbe buffer's byte order 203 | // and Xid's are always in big-endian order. 204 | timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get()); 205 | randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); 206 | randomValue2 = makeShort(buffer.get(), buffer.get()); 207 | counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); 208 | } 209 | 210 | // Factory methods 211 | 212 | /** 213 | * Gets a new object id. 214 | * 215 | * @return the new id 216 | */ 217 | public static Xid get() { 218 | return new Xid(); 219 | } 220 | 221 | public static String string() { 222 | return get().toHexString(); 223 | } 224 | 225 | public static byte[] bytes() { 226 | return get().toByteArray(); 227 | } 228 | 229 | /** 230 | * Gets a new object id with the given date value and all other bits zeroed. 231 | *

232 | * The returned object id will compare as less than or equal to any other object id within the same second as the given date, and 233 | * less than any later date. 234 | *

235 | * 236 | * @param date the date 237 | * @return the Xid 238 | */ 239 | public static Xid getSmallestWithDate(final Date date) { 240 | return new Xid(dateToTimestampSeconds(date), 0, (short) 0, 0, false); 241 | } 242 | 243 | /** 244 | * Convert to a byte array. Note that the numbers are stored in big-endian order. 245 | * 246 | * @return the byte array 247 | */ 248 | public byte[] toByteArray() { 249 | ByteBuffer buffer = ByteBuffer.allocate(ID_LENGTH); 250 | putToByteBuffer(buffer); 251 | return buffer.array(); // using .allocate ensures there is a backing array that can be returned 252 | } 253 | 254 | /** 255 | * Convert to bytes and put those bytes to the provided ByteBuffer. 256 | * Note that the numbers are stored in big-endian order. 257 | * 258 | * @param buffer the ByteBuffer 259 | * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining 260 | */ 261 | public void putToByteBuffer(final ByteBuffer buffer) { 262 | paramNotNull("buffer", buffer); 263 | isTrue("buffer.remaining() >=12", buffer.remaining() >= ID_LENGTH); 264 | 265 | buffer.put(int3(timestamp)); 266 | buffer.put(int2(timestamp)); 267 | buffer.put(int1(timestamp)); 268 | buffer.put(int0(timestamp)); 269 | buffer.put(int2(randomValue1)); 270 | buffer.put(int1(randomValue1)); 271 | buffer.put(int0(randomValue1)); 272 | buffer.put(short1(randomValue2)); 273 | buffer.put(short0(randomValue2)); 274 | buffer.put(int2(counter)); 275 | buffer.put(int1(counter)); 276 | buffer.put(int0(counter)); 277 | } 278 | 279 | /** 280 | * Gets the timestamp (number of seconds since the Unix epoch). 281 | * 282 | * @return the timestamp 283 | */ 284 | public int getTimestamp() { 285 | return timestamp; 286 | } 287 | 288 | /** 289 | * Gets the timestamp as a {@code Date} instance. 290 | * 291 | * @return the Date 292 | */ 293 | public Date getDate() { 294 | return new Date((timestamp & 0xFFFFFFFFL) * 1000L); 295 | } 296 | 297 | /** 298 | * Converts this instance into a 20-byte hexadecimal string representation. 299 | * 300 | * @return a string representation of the Xid in hexadecimal format 301 | */ 302 | public String toHexString() { 303 | return base32Hex(toByteArray()); 304 | } 305 | 306 | @Override 307 | public boolean equals(final Object o) { 308 | if (this == o) { 309 | return true; 310 | } 311 | if (o == null || getClass() != o.getClass()) { 312 | return false; 313 | } 314 | 315 | Xid other = (Xid) o; 316 | 317 | if (counter != other.counter) { 318 | return false; 319 | } 320 | if (timestamp != other.timestamp) { 321 | return false; 322 | } 323 | 324 | if (randomValue1 != other.randomValue1) { 325 | return false; 326 | } 327 | 328 | return randomValue2 == other.randomValue2; 329 | } 330 | 331 | @Override 332 | public int hashCode() { 333 | int result = timestamp; 334 | result = 31 * result + counter; 335 | result = 31 * result + randomValue1; 336 | result = 31 * result + randomValue2; 337 | return result; 338 | } 339 | 340 | @Override 341 | public int compareTo(final Xid other) { 342 | if (other == null) { 343 | throw new NullPointerException(); 344 | } 345 | 346 | byte[] byteArray = toByteArray(); 347 | byte[] otherByteArray = other.toByteArray(); 348 | for (int i = 0; i < ID_LENGTH; i++) { 349 | if (byteArray[i] != otherByteArray[i]) { 350 | return ((byteArray[i] & 0xff) < (otherByteArray[i] & 0xff)) ? -1 : 1; 351 | } 352 | } 353 | return 0; 354 | } 355 | 356 | @Override 357 | public String toString() { 358 | return toHexString(); 359 | } 360 | 361 | /** 362 | * Checks if a string could be an {@code Xid}. 363 | * 364 | * @param hexString a potential Xid as a String. 365 | * @return whether the string could be an object id 366 | * @throws IllegalArgumentException if hexString is null 367 | */ 368 | public static boolean isValid(final String hexString) { 369 | if (hexString == null) { 370 | throw new IllegalArgumentException(); 371 | } 372 | 373 | int len = hexString.length(); 374 | if (len != 20) { 375 | return false; 376 | } 377 | 378 | for (int i = 0; i < len; i++) { 379 | char c = hexString.charAt(i); 380 | if (c >= '0' && c <= '9') { 381 | continue; 382 | } 383 | if (c >= 'a' && c <= 'v') { 384 | continue; 385 | } 386 | 387 | return false; 388 | } 389 | 390 | return true; 391 | } 392 | 393 | private static byte[] parseHexString(final String id) { 394 | if (!isValid(id)) { 395 | throw new IllegalArgumentException("invalid hexadecimal representation of an Xid: [" + id + "]"); 396 | } 397 | 398 | return base32Hex(id); 399 | } 400 | 401 | /** 402 | * Encodes byte array to Base32 String. 403 | * 404 | * @param bytes Bytes to encode. 405 | * @return Encoded byte array bytes as a String. 406 | */ 407 | private static String base32Hex(final byte[] bytes) { 408 | int i = 0; 409 | int index = 0; 410 | int digit = 0; 411 | int currByte; 412 | int nextByte; 413 | StringBuilder base32 = new StringBuilder((bytes.length + 7) * 8 / 5); 414 | 415 | while (i < bytes.length) { 416 | currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsigned 417 | 418 | if (index > 3) { // Current digit spanning a byte boundary? 419 | if ((i + 1) < bytes.length) { 420 | nextByte = (bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256); 421 | } else { 422 | nextByte = 0; 423 | } 424 | 425 | digit = currByte & (0xFF >> index); 426 | index = (index + 5) % 8; 427 | digit <<= index; 428 | digit |= nextByte >> (8 - index); 429 | i++; 430 | } else { 431 | digit = (currByte >> (8 - (index + 5))) & 0x1F; 432 | index = (index + 5) % 8; 433 | if (index == 0) { 434 | i++; 435 | } 436 | } 437 | base32.append(BASE32_HEX_CHARS[digit]); 438 | } 439 | 440 | return base32.toString(); 441 | } 442 | 443 | /** 444 | * Decodes the given Base32 String to a raw byte array. 445 | * 446 | * @return Decoded base32 String as a raw byte array. 447 | */ 448 | private static byte[] base32Hex(final String base32) { 449 | int i; 450 | int index; 451 | int lookup; 452 | int offset; 453 | int digit; 454 | byte[] bytes = new byte[base32.length() * 5 / 8]; 455 | 456 | for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { 457 | lookup = base32.charAt(i) - '0'; 458 | 459 | /* Skip chars outside the lookup table. */ 460 | if (lookup < 0 || lookup >= BASE32_LOOKUP_TABLE.length) { 461 | continue; 462 | } 463 | 464 | digit = BASE32_LOOKUP_TABLE[lookup]; 465 | 466 | /* If this digit is not in the table, ignore it. */ 467 | if (digit == 0xFF) { 468 | continue; 469 | } 470 | 471 | if (index <= 3) { 472 | index = (index + 5) % 8; 473 | if (index == 0) { 474 | bytes[offset] |= digit; 475 | offset++; 476 | if (offset >= bytes.length) { 477 | break; 478 | } 479 | } else { 480 | bytes[offset] |= digit << (8 - index); 481 | } 482 | } else { 483 | index = (index + 5) % 8; 484 | bytes[offset] |= (digit >>> index); 485 | offset++; 486 | 487 | if (offset >= bytes.length) { 488 | break; 489 | } 490 | bytes[offset] |= digit << (8 - index); 491 | } 492 | } 493 | return bytes; 494 | } 495 | 496 | private static int dateToTimestampSeconds(final Date time) { 497 | return (int) (time.getTime() / 1000); 498 | } 499 | 500 | // Big-Endian helpers, in this class because all other BSON numbers are little-endian 501 | 502 | private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) { 503 | return (((b3) << 24) | 504 | ((b2 & 0xff) << 16) | 505 | ((b1 & 0xff) << 8) | 506 | ((b0 & 0xff))); 507 | } 508 | 509 | private static short makeShort(final byte b1, final byte b0) { 510 | return (short) (((b1 & 0xff) << 8) | ((b0 & 0xff))); 511 | } 512 | 513 | private static byte int3(final int x) { 514 | return (byte) (x >> 24); 515 | } 516 | 517 | private static byte int2(final int x) { 518 | return (byte) (x >> 16); 519 | } 520 | 521 | private static byte int1(final int x) { 522 | return (byte) (x >> 8); 523 | } 524 | 525 | private static byte int0(final int x) { 526 | return (byte) (x); 527 | } 528 | 529 | private static byte short1(final short x) { 530 | return (byte) (x >> 8); 531 | } 532 | 533 | private static byte short0(final short x) { 534 | return (byte) (x); 535 | } 536 | 537 | public static void isTrue(final String name, 538 | final boolean condition) { 539 | if (!condition) { 540 | throw new IllegalStateException("state should be: " + name); 541 | } 542 | } 543 | 544 | static T isTrue(final String name, 545 | final T value, 546 | final boolean condition) { 547 | if (!condition) { 548 | throw new IllegalArgumentException("state should be: " + name); 549 | } 550 | return value; 551 | } 552 | 553 | static T paramNotNull(final String name, 554 | final T value) { 555 | if (value == null) { 556 | throw new IllegalArgumentException(name + " can not be null"); 557 | } 558 | return value; 559 | } 560 | } --------------------------------------------------------------------------------