diff --git a/bson/src/main/org/bson/types/ObjectId.java b/bson/src/main/org/bson/types/ObjectId.java
index 7c1b1d29540..927d3ab0c31 100644
--- a/bson/src/main/org/bson/types/ObjectId.java
+++ b/bson/src/main/org/bson/types/ObjectId.java
@@ -16,17 +16,18 @@
package org.bson.types;
+import static org.bson.assertions.Assertions.isTrueArgument;
+import static org.bson.assertions.Assertions.notNull;
+
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.security.SecureRandom;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
-import static org.bson.assertions.Assertions.isTrueArgument;
-import static org.bson.assertions.Assertions.notNull;
-
/**
*
A globally unique identifier for objects.
*
@@ -53,9 +54,8 @@ public final class ObjectId implements Comparable, Serializable {
private static final int OBJECT_ID_LENGTH = 12;
private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff;
- // Use primitives to represent the 5-byte random value.
- private static final int RANDOM_VALUE1;
- private static final short RANDOM_VALUE2;
+ // Use upper bytes of a long to represent the 5-byte random value.
+ private static final long RANDOM_VALUE;
private static final AtomicInteger NEXT_COUNTER;
@@ -67,18 +67,12 @@ public final class ObjectId implements Comparable, Serializable {
* The timestamp
*/
private final int timestamp;
+
/**
- * The counter.
- */
- private final int counter;
- /**
- * the first four bits of randomness.
- */
- private final int randomValue1;
- /**
- * The last two bits of randomness.
+ * The final 8 bytes of the ObjectID are 5 bytes probabilistically unique to the machine and
+ * process, followed by a 3 byte incrementing counter initialized to a random value.
*/
- private final short randomValue2;
+ private final long nonce;
/**
* Gets a new object id.
@@ -101,7 +95,7 @@ public static ObjectId get() {
* @since 4.1
*/
public static ObjectId getSmallestWithDate(final Date date) {
- return new ObjectId(dateToTimestampSeconds(date), 0, (short) 0, 0, false);
+ return new ObjectId(dateToTimestampSeconds(date), 0L);
}
/**
@@ -152,7 +146,7 @@ public ObjectId() {
* @param date the date
*/
public ObjectId(final Date date) {
- this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false);
+ this(dateToTimestampSeconds(date), RANDOM_VALUE | (NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES));
}
/**
@@ -163,7 +157,7 @@ public ObjectId(final Date date) {
* @throws IllegalArgumentException if the high order byte of counter is not zero
*/
public ObjectId(final Date date, final int counter) {
- this(dateToTimestampSeconds(date), counter, true);
+ this(dateToTimestampSeconds(date), getNonceFromUntrustedCounter(counter));
}
/**
@@ -174,25 +168,19 @@ public ObjectId(final Date date, final int counter) {
* @throws IllegalArgumentException if the high order byte of counter is not zero
*/
public ObjectId(final int timestamp, final int counter) {
- this(timestamp, counter, true);
+ this(timestamp, getNonceFromUntrustedCounter(counter));
}
- private ObjectId(final int timestamp, final int counter, final boolean checkCounter) {
- this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter);
+ private ObjectId(final int timestamp, final long nonce) {
+ this.timestamp = timestamp;
+ this.nonce = nonce;
}
- private ObjectId(final int timestamp, final int randomValue1, final short randomValue2, final int counter,
- final boolean checkCounter) {
- if ((randomValue1 & 0xff000000) != 0) {
- throw new IllegalArgumentException("The random value must be between 0 and 16777215 (it must fit in three bytes).");
- }
- if (checkCounter && ((counter & 0xff000000) != 0)) {
+ private static long getNonceFromUntrustedCounter(final int counter) {
+ if ((counter & 0xff000000) != 0) {
throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes).");
}
- this.timestamp = timestamp;
- this.counter = counter & LOW_ORDER_THREE_BYTES;
- this.randomValue1 = randomValue1;
- this.randomValue2 = randomValue2;
+ return RANDOM_VALUE | counter;
}
/**
@@ -226,12 +214,14 @@ public ObjectId(final ByteBuffer buffer) {
notNull("buffer", buffer);
isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH);
- // Note: Cannot use ByteBuffer.getInt because it depends on tbe buffer's byte order
- // and ObjectId's are always in big-endian order.
- timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get());
- randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());
- randomValue2 = makeShort(buffer.get(), buffer.get());
- counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());
+ ByteOrder originalOrder = buffer.order();
+ try {
+ buffer.order(ByteOrder.BIG_ENDIAN);
+ this.timestamp = buffer.getInt();
+ this.nonce = buffer.getLong();
+ } finally {
+ buffer.order(originalOrder);
+ }
}
/**
@@ -240,9 +230,11 @@ public ObjectId(final ByteBuffer buffer) {
* @return the byte array
*/
public byte[] toByteArray() {
- ByteBuffer buffer = ByteBuffer.allocate(OBJECT_ID_LENGTH);
- putToByteBuffer(buffer);
- return buffer.array(); // using .allocate ensures there is a backing array that can be returned
+ // using .allocate ensures there is a backing array that can be returned
+ return ByteBuffer.allocate(OBJECT_ID_LENGTH)
+ .putInt(this.timestamp)
+ .putLong(this.nonce)
+ .array();
}
/**
@@ -257,18 +249,14 @@ public void putToByteBuffer(final ByteBuffer buffer) {
notNull("buffer", buffer);
isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH);
- buffer.put(int3(timestamp));
- buffer.put(int2(timestamp));
- buffer.put(int1(timestamp));
- buffer.put(int0(timestamp));
- buffer.put(int2(randomValue1));
- buffer.put(int1(randomValue1));
- buffer.put(int0(randomValue1));
- buffer.put(short1(randomValue2));
- buffer.put(short0(randomValue2));
- buffer.put(int2(counter));
- buffer.put(int1(counter));
- buffer.put(int0(counter));
+ ByteOrder originalOrder = buffer.order();
+ try {
+ buffer.order(ByteOrder.BIG_ENDIAN);
+ buffer.putInt(this.timestamp);
+ buffer.putLong(this.nonce);
+ } finally {
+ buffer.order(originalOrder);
+ }
}
/**
@@ -313,49 +301,26 @@ public boolean equals(final Object o) {
return false;
}
- ObjectId objectId = (ObjectId) o;
-
- if (counter != objectId.counter) {
- return false;
- }
- if (timestamp != objectId.timestamp) {
- return false;
- }
-
- if (randomValue1 != objectId.randomValue1) {
+ ObjectId other = (ObjectId) o;
+ if (timestamp != other.timestamp) {
return false;
}
-
- if (randomValue2 != objectId.randomValue2) {
- return false;
- }
-
- return true;
+ return nonce == other.nonce;
}
@Override
public int hashCode() {
- int result = timestamp;
- result = 31 * result + counter;
- result = 31 * result + randomValue1;
- result = 31 * result + randomValue2;
- return result;
+ return 31 * timestamp + Long.hashCode(nonce);
}
@Override
public int compareTo(final ObjectId other) {
- if (other == null) {
- throw new NullPointerException();
+ int cmp = Integer.compareUnsigned(this.timestamp, other.timestamp);
+ if (cmp != 0) {
+ return cmp;
}
- byte[] byteArray = toByteArray();
- byte[] otherByteArray = other.toByteArray();
- for (int i = 0; i < OBJECT_ID_LENGTH; i++) {
- if (byteArray[i] != otherByteArray[i]) {
- return ((byteArray[i] & 0xff) < (otherByteArray[i] & 0xff)) ? -1 : 1;
- }
- }
- return 0;
+ return Long.compareUnsigned(nonce, other.nonce);
}
@Override
@@ -407,8 +372,7 @@ private Object readResolve() {
static {
try {
SecureRandom secureRandom = new SecureRandom();
- RANDOM_VALUE1 = secureRandom.nextInt(0x01000000);
- RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000);
+ RANDOM_VALUE = secureRandom.nextLong() & ~LOW_ORDER_THREE_BYTES;
NEXT_COUNTER = new AtomicInteger(secureRandom.nextInt());
} catch (Exception e) {
throw new RuntimeException(e);
@@ -443,46 +407,4 @@ private static int hexCharToInt(final char c) {
private static int dateToTimestampSeconds(final Date time) {
return (int) (time.getTime() / 1000);
}
-
- // Big-Endian helpers, in this class because all other BSON numbers are little-endian
-
- private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) {
- // CHECKSTYLE:OFF
- return (((b3) << 24) |
- ((b2 & 0xff) << 16) |
- ((b1 & 0xff) << 8) |
- ((b0 & 0xff)));
- // CHECKSTYLE:ON
- }
-
- private static short makeShort(final byte b1, final byte b0) {
- // CHECKSTYLE:OFF
- return (short) (((b1 & 0xff) << 8) | ((b0 & 0xff)));
- // CHECKSTYLE:ON
- }
-
- private static byte int3(final int x) {
- return (byte) (x >> 24);
- }
-
- private static byte int2(final int x) {
- return (byte) (x >> 16);
- }
-
- private static byte int1(final int x) {
- return (byte) (x >> 8);
- }
-
- private static byte int0(final int x) {
- return (byte) (x);
- }
-
- private static byte short1(final short x) {
- return (byte) (x >> 8);
- }
-
- private static byte short0(final short x) {
- return (byte) (x);
- }
-
}
diff --git a/bson/src/test/unit/org/bson/types/ObjectIdTest.java b/bson/src/test/unit/org/bson/types/ObjectIdTest.java
index 14c8241f55a..cfe04623b90 100644
--- a/bson/src/test/unit/org/bson/types/ObjectIdTest.java
+++ b/bson/src/test/unit/org/bson/types/ObjectIdTest.java
@@ -17,36 +17,72 @@
package org.bson.types;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.nio.Buffer;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
import java.util.Locale;
import java.util.Random;
+import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class ObjectIdTest {
- @Test
- public void testToBytes() {
+
+ /** Calls the base method of ByteBuffer.position(int) since the override is not available in jdk8. */
+ private static ByteBuffer setPosition(final ByteBuffer buf, final int pos) {
+ ((Buffer) buf).position(pos);
+ return buf;
+ }
+
+ /**
+ * MethodSource for valid ByteBuffers that can hold an ObjectID
+ */
+ public static List validOutputBuffers() {
+ List result = new ArrayList<>();
+ result.add(ByteBuffer.allocate(12));
+ result.add(ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN));
+ result.add(ByteBuffer.allocate(24).put(new byte[12]));
+ result.add(ByteBuffer.allocateDirect(12));
+ result.add(ByteBuffer.allocateDirect(12).order(ByteOrder.LITTLE_ENDIAN));
+ return result;
+ }
+
+ @MethodSource("validOutputBuffers")
+ @ParameterizedTest
+ public void testToBytes(final ByteBuffer output) {
+ int originalPosition = output.position();
+ ByteOrder originalOrder = output.order();
byte[] expectedBytes = {81, 6, -4, -102, -68, -126, 55, 85, -127, 54, -46, -119};
+ byte[] result = new byte[12];
ObjectId objectId = new ObjectId(expectedBytes);
assertArrayEquals(expectedBytes, objectId.toByteArray());
- ByteBuffer buffer = ByteBuffer.allocate(12);
- objectId.putToByteBuffer(buffer);
- assertArrayEquals(expectedBytes, buffer.array());
+ objectId.putToByteBuffer(output);
+ ((Buffer) output).position(output.position() - 12);
+ output.get(result); // read last 12 bytes leaving position intact
+
+ assertArrayEquals(expectedBytes, result);
+ assertEquals(originalPosition + 12, output.position());
+ assertEquals(originalOrder, output.order());
}
@Test
@@ -136,8 +172,64 @@ public void testTime() {
}
@Test
- public void testDateCons() {
+ public void testDateConstructor() {
assertEquals(new Date().getTime() / 1000, new ObjectId(new Date()).getDate().getTime() / 1000);
+ assertNotEquals(new ObjectId(new Date(1_000)), new ObjectId(new Date(1_000)));
+ assertEquals("00000001", new ObjectId(new Date(1_000)).toHexString().substring(0, 8));
+ }
+
+ @Test
+ public void testDateConstructorWithCounter() {
+ assertEquals(new ObjectId(new Date(1_000), 1), new ObjectId(new Date(1_000), 1));
+ assertEquals("00000001", new ObjectId(new Date(1_000), 1).toHexString().substring(0, 8));
+ assertThrows(NullPointerException.class, () -> new ObjectId(null, Integer.MAX_VALUE));
+ assertThrows(IllegalArgumentException.class, () -> new ObjectId(new Date(1_000), Integer.MAX_VALUE));
+ }
+
+ @Test
+ public void testTimestampConstructor() {
+ assertEquals(1_000, new ObjectId(1_000, 1).getTimestamp());
+ assertEquals(new ObjectId(1_000, 1), new ObjectId(1_000, 1));
+ assertEquals("7fffffff", new ObjectId(Integer.MAX_VALUE, 1).toHexString().substring(0, 8));
+ assertThrows(IllegalArgumentException.class, () -> new ObjectId(Integer.MAX_VALUE, Integer.MAX_VALUE));
+ }
+
+ /**
+ * MethodSource for valid ByteBuffers containing an ObjectID at the current position.
+ */
+ public static List validInputBuffers() {
+ byte[] data = new byte[12];
+ for (byte i = 0; i < data.length; ++i) {
+ data[i] = i;
+ }
+
+ List result = new ArrayList<>();
+ result.add(ByteBuffer.wrap(data));
+ result.add(ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN));
+ result.add(setPosition(ByteBuffer.allocateDirect(data.length).put(data), 0));
+ result.add(setPosition(ByteBuffer.allocateDirect(data.length).put(data).order(ByteOrder.LITTLE_ENDIAN), 0));
+ result.add(setPosition(ByteBuffer.allocate(2 * data.length).put(data), 0));
+ result.add(setPosition(ByteBuffer.allocate(2 * data.length).put(new byte[12]).put(data), 12));
+ return result;
+ }
+
+ @ParameterizedTest
+ @MethodSource(value = "validInputBuffers")
+ public void testByteBufferConstructor(final ByteBuffer input) {
+ ByteOrder order = input.order();
+ int position = input.position();
+
+ byte[] result = new ObjectId(input).toByteArray();
+
+ assertArrayEquals(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, result);
+ assertEquals(order, input.order());
+ assertEquals(position + 12, input.position());
+ }
+
+ @Test
+ public void testInvalidByteBufferConstructor() {
+ assertThrows(IllegalArgumentException.class, () -> new ObjectId((ByteBuffer) null));
+ assertThrows(IllegalArgumentException.class, () -> new ObjectId(ByteBuffer.allocate(11)));
}
@Test
@@ -162,6 +254,23 @@ public void testCompareTo() {
assertEquals(-1, first.compareTo(third));
assertEquals(1, second.compareTo(first));
assertEquals(1, third.compareTo(first));
+ assertThrows(NullPointerException.class, () -> first.compareTo(null));
+ }
+
+ @Test
+ public void testEquals() {
+ Date dateOne = new Date();
+ Date dateTwo = new Date(dateOne.getTime() + 10000);
+ ObjectId first = new ObjectId(dateOne, 0);
+ ObjectId second = new ObjectId(dateOne, 1);
+ ObjectId third = new ObjectId(dateTwo, 0);
+ ObjectId fourth = new ObjectId(first.toByteArray());
+ assertEquals(first, first);
+ assertEquals(first, fourth);
+ assertNotEquals(first, second);
+ assertNotEquals(first, third);
+ assertNotEquals(second, third);
+ assertFalse(first.equals(null));
}
@Test