├── .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 | [](https://raw.githubusercontent.com/0xShamil/java-xid/master/LICENSE) [](https://travis-ci.org/0xShamil/java-xid) [](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 |
Xid layout
20 |
21 |
0
1
2
3
4
5
6
7
8
9
10
11
22 |
23 |
24 |
time
random value
inc
25 |
26 |
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 | *
layout
55 | *
56 | *
0
1
2
3
4
5
6
7
8
9
10
11
57 | *
58 | *
59 | *
time
random value
inc
60 | *
61 | *
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 | }
--------------------------------------------------------------------------------