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