From 2468e98d67c6d73f26e712283f14b92b67dae67c Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Tue, 13 Jun 2023 09:02:34 -0400 Subject: [PATCH] ByteBufBsonDocument remains a ByteBuf (#1119) * Ensure that ByteBufBsonDocument never hydrates itself into a HashMap * Add ByteBufBsonArray and ensure it never hydrates itself into an ArrayList JAVA-4917 --- bson/src/main/org/bson/BsonArray.java | 2 +- .../internal/connection/ByteBufBsonArray.java | 300 ++++++++++++++++++ .../connection/ByteBufBsonDocument.java | 150 +++++++-- .../connection/ByteBufBsonHelper.java | 126 ++++++++ .../connection/ByteBufBsonArrayTest.java | 268 ++++++++++++++++ .../ByteBufBsonDocumentSpecification.groovy | 16 - 6 files changed, 819 insertions(+), 43 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonArray.java create mode 100644 driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonHelper.java create mode 100644 driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonArrayTest.java diff --git a/bson/src/main/org/bson/BsonArray.java b/bson/src/main/org/bson/BsonArray.java index 88cbca5933c..876858b01b0 100644 --- a/bson/src/main/org/bson/BsonArray.java +++ b/bson/src/main/org/bson/BsonArray.java @@ -237,7 +237,7 @@ public int hashCode() { @Override public String toString() { return "BsonArray{" - + "values=" + values + + "values=" + getValues() + '}'; } diff --git a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonArray.java b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonArray.java new file mode 100644 index 00000000000..e02cee12629 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonArray.java @@ -0,0 +1,300 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.connection; + +import org.bson.BsonArray; +import org.bson.BsonBinaryReader; +import org.bson.BsonType; +import org.bson.BsonValue; +import org.bson.ByteBuf; +import org.bson.io.ByteBufferBsonInput; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +import static com.mongodb.internal.connection.ByteBufBsonHelper.readBsonValue; + +final class ByteBufBsonArray extends BsonArray { + private final ByteBuf byteBuf; + + ByteBufBsonArray(final ByteBuf byteBuf) { + this.byteBuf = byteBuf; + } + + @Override + public Iterator iterator() { + return new ByteBufBsonArrayIterator(); + } + + @Override + public List getValues() { + List values = new ArrayList<>(); + for (BsonValue cur: this) { + //noinspection UseBulkOperation + values.add(cur); + } + return values; + } + + private static final String READ_ONLY_MESSAGE = "This BsonArray instance is read-only"; + + @Override + public int size() { + int size = 0; + for (BsonValue ignored : this) { + size++; + } + return size; + } + + @Override + public boolean isEmpty() { + return !iterator().hasNext(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof List)) { + return false; + } + Iterator e1 = iterator(); + Iterator e2 = ((List) o).iterator(); + while (e1.hasNext() && e2.hasNext()) { + if (!(Objects.equals(e1.next(), e2.next()))) { + return false; + } + } + return !(e1.hasNext() || e2.hasNext()); + } + + @Override + public int hashCode() { + int hashCode = 1; + for (BsonValue cur : this) { + hashCode = 31 * hashCode + (cur == null ? 0 : cur.hashCode()); + } + return hashCode; + } + + @Override + public boolean contains(final Object o) { + for (BsonValue cur : this) { + if (Objects.equals(o, cur)) { + return true; + } + } + + return false; + } + + @Override + public Object[] toArray() { + Object[] retVal = new Object[size()]; + Iterator it = iterator(); + for (int i = 0; i < retVal.length; i++) { + retVal[i] = it.next(); + } + return retVal; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(final T[] a) { + int size = size(); + T[] retVal = a.length >= size ? a : (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size); + Iterator it = iterator(); + for (int i = 0; i < retVal.length; i++) { + retVal[i] = (T) it.next(); + } + return retVal; + } + + @Override + public boolean containsAll(final Collection c) { + for (Object e : c) { + if (!contains(e)) { + return false; + } + } + return true; + } + + @Override + public BsonValue get(final int index) { + if (index < 0) { + throw new IndexOutOfBoundsException("Index out of range: " + index); + } + + int i = 0; + for (BsonValue cur : this) { + if (i++ == index) { + return cur; + } + } + + throw new IndexOutOfBoundsException("Index out of range: " + index); + } + + @Override + public int indexOf(final Object o) { + int i = 0; + for (BsonValue cur : this) { + if (Objects.equals(o, cur)) { + return i; + } + i++; + } + + return -1; + } + + @Override + public int lastIndexOf(final Object o) { + ListIterator listIterator = listIterator(size()); + while (listIterator.hasPrevious()) { + if (Objects.equals(o, listIterator.previous())) { + return listIterator.nextIndex(); + } + } + return -1; + } + + @Override + public ListIterator listIterator() { + return listIterator(0); + } + + @Override + public ListIterator listIterator(final int index) { + // Not the most efficient way to do this, but unlikely anyone will notice in practice + return new ArrayList<>(this).listIterator(index); + } + + @Override + public List subList(final int fromIndex, final int toIndex) { + if (fromIndex < 0) { + throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); + } + if (fromIndex > toIndex) { + throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); + } + List subList = new ArrayList<>(); + int i = 0; + for (BsonValue cur: this) { + if (i == toIndex) { + break; + } + if (i >= fromIndex) { + subList.add(cur); + } + i++; + } + if (toIndex > i) { + throw new IndexOutOfBoundsException("toIndex = " + toIndex); + } + return subList; + } + + @Override + public boolean add(final BsonValue bsonValue) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public boolean addAll(final Collection c) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public boolean addAll(final int index, final Collection c) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public boolean removeAll(final Collection c) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public boolean retainAll(final Collection c) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public BsonValue set(final int index, final BsonValue element) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public void add(final int index, final BsonValue element) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + @Override + public BsonValue remove(final int index) { + throw new UnsupportedOperationException(READ_ONLY_MESSAGE); + } + + private class ByteBufBsonArrayIterator implements Iterator { + private final ByteBuf duplicatedByteBuf = byteBuf.duplicate(); + private final BsonBinaryReader bsonReader; + + { + bsonReader = new BsonBinaryReader(new ByteBufferBsonInput(duplicatedByteBuf)); + // While one might expect that this would be a call to BsonReader#readStartArray that doesn't work because BsonBinaryReader + // expects to be positioned at the start at the beginning of a document, not an array. Fortunately, a BSON array has exactly + // the same structure as a BSON document (the keys are just the array indices converted to a strings). So it works fine to + // call BsonReader#readStartDocument here, and just skip all the names via BsonReader#skipName below. + bsonReader.readStartDocument(); + bsonReader.readBsonType(); + } + + @Override + public boolean hasNext() { + return bsonReader.getCurrentBsonType() != BsonType.END_OF_DOCUMENT; + } + + @Override + public BsonValue next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + bsonReader.skipName(); + BsonValue value = readBsonValue(duplicatedByteBuf, bsonReader); + bsonReader.readBsonType(); + return value; + } + } +} diff --git a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java index 676dba2d78c..5ab265c2bc8 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java +++ b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java @@ -19,15 +19,12 @@ import com.mongodb.lang.Nullable; import org.bson.BsonBinaryReader; import org.bson.BsonDocument; -import org.bson.BsonReader; import org.bson.BsonType; import org.bson.BsonValue; import org.bson.ByteBuf; import org.bson.RawBsonDocument; import org.bson.codecs.BsonDocumentCodec; -import org.bson.codecs.BsonValueCodecProvider; import org.bson.codecs.DecoderContext; -import org.bson.codecs.configuration.CodecRegistry; import org.bson.io.ByteBufferBsonInput; import org.bson.json.JsonMode; import org.bson.json.JsonWriter; @@ -36,8 +33,12 @@ import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.StringWriter; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -45,14 +46,11 @@ import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.assertions.Assertions.notNull; -import static org.bson.codecs.BsonValueCodecProvider.getClassForBsonType; -import static org.bson.codecs.configuration.CodecRegistries.fromProviders; +import static com.mongodb.internal.connection.ByteBufBsonHelper.readBsonValue; final class ByteBufBsonDocument extends BsonDocument { private static final long serialVersionUID = 2L; - private static final CodecRegistry REGISTRY = fromProviders(new BsonValueCodecProvider()); - private final transient ByteBuf byteBuf; static List createList(final ByteBufferBsonOutput bsonOutput, final int startPosition) { @@ -109,7 +107,7 @@ public String toJson(final JsonWriterSettings settings) { } @Override - public BsonReader asBsonReader() { + public BsonBinaryReader asBsonReader() { return new BsonBinaryReader(new ByteBufferBsonInput(byteBuf.duplicate())); } @@ -124,10 +122,10 @@ public BsonDocument clone() { @Nullable T findInDocument(final Finder finder) { ByteBuf duplicateByteBuf = byteBuf.duplicate(); - try (BsonBinaryReader bsonReader = new BsonBinaryReader(new ByteBufferBsonInput(byteBuf.duplicate()))) { + try (BsonBinaryReader bsonReader = new BsonBinaryReader(new ByteBufferBsonInput(duplicateByteBuf))) { bsonReader.readStartDocument(); while (bsonReader.readBsonType() != BsonType.END_OF_DOCUMENT) { - T found = finder.find(bsonReader); + T found = finder.find(duplicateByteBuf, bsonReader); if (found != null) { return found; } @@ -186,7 +184,7 @@ public BsonValue remove(final Object key) { public boolean isEmpty() { return assertNotNull(findInDocument(new Finder() { @Override - public Boolean find(final BsonReader bsonReader) { + public Boolean find(final ByteBuf byteBuf, final BsonBinaryReader bsonReader) { return false; } @@ -204,7 +202,7 @@ public int size() { @Override @Nullable - public Integer find(final BsonReader bsonReader) { + public Integer find(final ByteBuf byteBuf, final BsonBinaryReader bsonReader) { size++; bsonReader.readName(); bsonReader.skipValue(); @@ -220,17 +218,17 @@ public Integer notFound() { @Override public Set> entrySet() { - return toBaseBsonDocument().entrySet(); + return new ByteBufBsonDocumentEntrySet(); } @Override public Collection values() { - return toBaseBsonDocument().values(); + return new ByteBufBsonDocumentValuesCollection(); } @Override public Set keySet() { - return toBaseBsonDocument().keySet(); + return new ByteBufBsonDocumentKeySet(); } @Override @@ -241,7 +239,7 @@ public boolean containsKey(final Object key) { Boolean containsKey = findInDocument(new Finder() { @Override - public Boolean find(final BsonReader bsonReader) { + public Boolean find(final ByteBuf byteBuf, final BsonBinaryReader bsonReader) { if (bsonReader.readName().equals(key)) { return true; } @@ -261,9 +259,9 @@ public Boolean notFound() { public boolean containsValue(final Object value) { Boolean containsValue = findInDocument(new Finder() { @Override - public Boolean find(final BsonReader bsonReader) { + public Boolean find(final ByteBuf byteBuf, final BsonBinaryReader bsonReader) { bsonReader.skipName(); - if (deserializeBsonValue(bsonReader).equals(value)) { + if (readBsonValue(byteBuf, bsonReader).equals(value)) { return true; } return null; @@ -283,9 +281,9 @@ public BsonValue get(final Object key) { notNull("key", key); return findInDocument(new Finder() { @Override - public BsonValue find(final BsonReader bsonReader) { + public BsonValue find(final ByteBuf byteBuf, final BsonBinaryReader bsonReader) { if (bsonReader.readName().equals(key)) { - return deserializeBsonValue(bsonReader); + return readBsonValue(byteBuf, bsonReader); } bsonReader.skipValue(); return null; @@ -308,7 +306,7 @@ public BsonValue notFound() { public String getFirstKey() { return assertNotNull(findInDocument(new Finder() { @Override - public String find(final BsonReader bsonReader) { + public String find(final ByteBuf byteBuf, final BsonBinaryReader bsonReader) { return bsonReader.readName(); } @@ -321,15 +319,11 @@ public String notFound() { private interface Finder { @Nullable - T find(BsonReader bsonReader); + T find(ByteBuf byteBuf, BsonBinaryReader bsonReader); @Nullable T notFound(); } - private BsonValue deserializeBsonValue(final BsonReader bsonReader) { - return REGISTRY.get(getClassForBsonType(bsonReader.getCurrentBsonType())).decode(bsonReader, DecoderContext.builder().build()); - } - // see https://docs.oracle.com/javase/6/docs/platform/serialization/spec/output.html private Object writeReplace() { return toBaseBsonDocument(); @@ -339,4 +333,108 @@ private Object writeReplace() { private void readObject(final ObjectInputStream stream) throws InvalidObjectException { throw new InvalidObjectException("Proxy required"); } + + private class ByteBufBsonDocumentEntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new Iterator>() { + private final ByteBuf duplicatedByteBuf = byteBuf.duplicate(); + private final BsonBinaryReader bsonReader; + + { + bsonReader = new BsonBinaryReader(new ByteBufferBsonInput(duplicatedByteBuf)); + bsonReader.readStartDocument(); + bsonReader.readBsonType(); + } + + @Override + public boolean hasNext() { + return bsonReader.getCurrentBsonType() != BsonType.END_OF_DOCUMENT; + } + + @Override + public Entry next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + String key = bsonReader.readName(); + BsonValue value = readBsonValue(duplicatedByteBuf, bsonReader); + bsonReader.readBsonType(); + return new AbstractMap.SimpleEntry<>(key, value); + } + + }; + } + + @Override + public boolean isEmpty() { + return !iterator().hasNext(); + } + + @Override + public int size() { + return ByteBufBsonDocument.this.size(); + } + } + + private class ByteBufBsonDocumentKeySet extends AbstractSet { + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private final Set> entrySet = new ByteBufBsonDocumentEntrySet(); + + @Override + public Iterator iterator() { + final Iterator> entrySetIterator = entrySet.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return entrySetIterator.hasNext(); + } + + @Override + public String next() { + return entrySetIterator.next().getKey(); + } + }; + } + + @Override + public boolean isEmpty() { + return entrySet.isEmpty(); + } + + @Override + public int size() { + return entrySet.size(); + } + } + + private class ByteBufBsonDocumentValuesCollection extends AbstractCollection { + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private final Set> entrySet = new ByteBufBsonDocumentEntrySet(); + + @Override + public Iterator iterator() { + final Iterator> entrySetIterator = entrySet.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return entrySetIterator.hasNext(); + } + + @Override + public BsonValue next() { + return entrySetIterator.next().getValue(); + } + }; + } + + @Override + public boolean isEmpty() { + return entrySet.isEmpty(); + } + @Override + public int size() { + return entrySet.size(); + } + } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonHelper.java b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonHelper.java new file mode 100644 index 00000000000..55054112bf2 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonHelper.java @@ -0,0 +1,126 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.connection; + +import org.bson.BsonBinaryReader; +import org.bson.BsonBoolean; +import org.bson.BsonDateTime; +import org.bson.BsonDecimal128; +import org.bson.BsonDocument; +import org.bson.BsonDouble; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonJavaScript; +import org.bson.BsonJavaScriptWithScope; +import org.bson.BsonMaxKey; +import org.bson.BsonMinKey; +import org.bson.BsonNull; +import org.bson.BsonObjectId; +import org.bson.BsonString; +import org.bson.BsonSymbol; +import org.bson.BsonUndefined; +import org.bson.BsonValue; +import org.bson.ByteBuf; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.DecoderContext; + +final class ByteBufBsonHelper { + static BsonValue readBsonValue(final ByteBuf byteBuf, final BsonBinaryReader bsonReader) { + BsonValue value; + switch (bsonReader.getCurrentBsonType()) { + case DOCUMENT: + ByteBuf documentByteBuf = byteBuf.duplicate(); + value = new ByteBufBsonDocument(documentByteBuf); + bsonReader.skipValue(); + break; + case ARRAY: + ByteBuf arrayByteBuf = byteBuf.duplicate(); + value = new ByteBufBsonArray(arrayByteBuf); + bsonReader.skipValue(); + break; + case INT32: + value = new BsonInt32(bsonReader.readInt32()); + break; + case INT64: + value = new BsonInt64(bsonReader.readInt64()); + break; + case DOUBLE: + value = new BsonDouble(bsonReader.readDouble()); + break; + case DECIMAL128: + value = new BsonDecimal128(bsonReader.readDecimal128()); + break; + case DATE_TIME: + value = new BsonDateTime(bsonReader.readDateTime()); + break; + case TIMESTAMP: + value = bsonReader.readTimestamp(); + break; + case BOOLEAN: + value = new BsonBoolean(bsonReader.readBoolean()); + break; + case OBJECT_ID: + value = new BsonObjectId(bsonReader.readObjectId()); + break; + case STRING: + value = new BsonString(bsonReader.readString()); + break; + case BINARY: + value = bsonReader.readBinaryData(); + break; + case SYMBOL: + value = new BsonSymbol(bsonReader.readSymbol()); + break; + case UNDEFINED: + bsonReader.readUndefined(); + value = new BsonUndefined(); + break; + case REGULAR_EXPRESSION: + value = bsonReader.readRegularExpression(); + break; + case DB_POINTER: + value = bsonReader.readDBPointer(); + break; + case JAVASCRIPT: + value = new BsonJavaScript(bsonReader.readJavaScript()); + break; + case JAVASCRIPT_WITH_SCOPE: + String code = bsonReader.readJavaScriptWithScope(); + BsonDocument scope = new BsonDocumentCodec().decode(bsonReader, DecoderContext.builder().build()); + value = new BsonJavaScriptWithScope(code, scope); + break; + case MIN_KEY: + bsonReader.readMinKey(); + value = new BsonMinKey(); + break; + case MAX_KEY: + bsonReader.readMaxKey(); + value = new BsonMaxKey(); + break; + case NULL: + bsonReader.readNull(); + value = new BsonNull(); + break; + default: + throw new UnsupportedOperationException("Unexpected BSON type: " + bsonReader.getCurrentBsonType()); + } + return value; + } + + private ByteBufBsonHelper() { + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonArrayTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonArrayTest.java new file mode 100644 index 00000000000..f7cefbf57c0 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonArrayTest.java @@ -0,0 +1,268 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.connection; + +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonBinaryWriter; +import org.bson.BsonBoolean; +import org.bson.BsonDateTime; +import org.bson.BsonDbPointer; +import org.bson.BsonDecimal128; +import org.bson.BsonDocument; +import org.bson.BsonDouble; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonJavaScript; +import org.bson.BsonJavaScriptWithScope; +import org.bson.BsonMaxKey; +import org.bson.BsonMinKey; +import org.bson.BsonNull; +import org.bson.BsonObjectId; +import org.bson.BsonRegularExpression; +import org.bson.BsonString; +import org.bson.BsonSymbol; +import org.bson.BsonTimestamp; +import org.bson.BsonUndefined; +import org.bson.BsonValue; +import org.bson.ByteBuf; +import org.bson.ByteBufNIO; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.EncoderContext; +import org.bson.io.BasicOutputBuffer; +import org.bson.types.Decimal128; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.bson.BsonBoolean.FALSE; +import static org.bson.BsonBoolean.TRUE; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ByteBufBsonArrayTest { + + @Test + void testGetValues() { + List values = asList(new BsonInt32(0), new BsonInt32(1), new BsonInt32(2)); + ByteBufBsonArray bsonArray = fromBsonValues(values); + assertEquals(values, bsonArray.getValues()); + } + + @Test + void testSize() { + assertEquals(0, fromBsonValues(emptyList()).size()); + assertEquals(1, fromBsonValues(singletonList(TRUE)).size()); + assertEquals(2, fromBsonValues(asList(TRUE, TRUE)).size()); + } + + @Test + void testIsEmpty() { + assertTrue(fromBsonValues(emptyList()).isEmpty()); + assertFalse(fromBsonValues(singletonList(TRUE)).isEmpty()); + assertFalse(fromBsonValues(asList(TRUE, TRUE)).isEmpty()); + } + + @Test + void testContains() { + assertFalse(fromBsonValues(emptyList()).contains(TRUE)); + assertTrue(fromBsonValues(singletonList(TRUE)).contains(TRUE)); + assertTrue(fromBsonValues(asList(FALSE, TRUE)).contains(TRUE)); + assertFalse(fromBsonValues(singletonList(FALSE)).contains(TRUE)); + assertFalse(fromBsonValues(asList(FALSE, FALSE)).contains(TRUE)); + } + + @Test + void testIterator() { + Iterator iterator = fromBsonValues(emptyList()).iterator(); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + + iterator = fromBsonValues(singletonList(TRUE)).iterator(); + assertTrue(iterator.hasNext()); + assertEquals(TRUE, iterator.next()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + + iterator = fromBsonValues(asList(TRUE, FALSE)).iterator(); + assertTrue(iterator.hasNext()); + assertEquals(TRUE, iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals(FALSE, iterator.next()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void testToArray() { + assertArrayEquals(new BsonValue[]{TRUE, FALSE}, fromBsonValues(asList(TRUE, FALSE)).toArray()); + assertArrayEquals(new BsonValue[]{TRUE, FALSE}, fromBsonValues(asList(TRUE, FALSE)).toArray(new BsonValue[0])); + } + + @Test + void testContainsAll() { + assertTrue(fromBsonValues(asList(TRUE, FALSE)).containsAll(asList(TRUE, FALSE))); + assertFalse(fromBsonValues(asList(TRUE, TRUE)).containsAll(asList(TRUE, FALSE))); + } + + @Test + void testGet() { + ByteBufBsonArray bsonArray = fromBsonValues(asList(TRUE, FALSE)); + assertEquals(TRUE, bsonArray.get(0)); + assertEquals(FALSE, bsonArray.get(1)); + assertThrows(IndexOutOfBoundsException.class, () -> bsonArray.get(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> bsonArray.get(2)); + } + + @Test + void testIndexOf() { + ByteBufBsonArray bsonArray = fromBsonValues(asList(TRUE, FALSE)); + assertEquals(0, bsonArray.indexOf(TRUE)); + assertEquals(1, bsonArray.indexOf(FALSE)); + assertEquals(-1, bsonArray.indexOf(BsonNull.VALUE)); + } + + @Test + void testLastIndexOf() { + ByteBufBsonArray bsonArray = fromBsonValues(asList(TRUE, FALSE, TRUE, FALSE)); + assertEquals(2, bsonArray.lastIndexOf(TRUE)); + assertEquals(3, bsonArray.lastIndexOf(FALSE)); + assertEquals(-1, bsonArray.lastIndexOf(BsonNull.VALUE)); + } + + @Test + void testListIterator() { + // implementation is delegated to ArrayList, so not much testing is needed + ListIterator iterator = fromBsonValues(emptyList()).listIterator(); + assertFalse(iterator.hasNext()); + assertFalse(iterator.hasPrevious()); + } + + @Test + void testSubList() { + ByteBufBsonArray bsonArray = fromBsonValues(asList(new BsonInt32(0), new BsonInt32(1), new BsonInt32(2))); + assertEquals(emptyList(), bsonArray.subList(0, 0)); + assertEquals(singletonList(new BsonInt32(0)), bsonArray.subList(0, 1)); + assertEquals(singletonList(new BsonInt32(2)), bsonArray.subList(2, 3)); + assertThrows(IndexOutOfBoundsException.class, () -> bsonArray.subList(-1, 1)); + assertThrows(IllegalArgumentException.class, () -> bsonArray.subList(3, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> bsonArray.subList(2, 4)); + } + + @Test + void testEquals() { + assertEquals(new BsonArray(asList(TRUE, FALSE)), fromBsonValues(asList(TRUE, FALSE))); + assertEquals(fromBsonValues(asList(TRUE, FALSE)), new BsonArray(asList(TRUE, FALSE))); + + assertNotEquals(new BsonArray(asList(TRUE, FALSE)), fromBsonValues(asList(FALSE, TRUE))); + assertNotEquals(fromBsonValues(asList(TRUE, FALSE)), new BsonArray(asList(FALSE, TRUE))); + + assertNotEquals(new BsonArray(asList(TRUE, FALSE)), fromBsonValues(asList(TRUE, FALSE, TRUE))); + assertNotEquals(fromBsonValues(asList(TRUE, FALSE)), new BsonArray(asList(TRUE, FALSE, TRUE))); + assertNotEquals(fromBsonValues(asList(TRUE, FALSE, TRUE)), new BsonArray(asList(TRUE, FALSE))); + } + + @Test + void testHashCode() { + assertEquals(new BsonArray(asList(TRUE, FALSE)).hashCode(), fromBsonValues(asList(TRUE, FALSE)).hashCode()); + } + + @Test + void testToString() { + assertEquals(new BsonArray(asList(TRUE, FALSE)).toString(), fromBsonValues(asList(TRUE, FALSE)).toString()); + } + + @Test + void testAllBsonTypes() { + BsonValue bsonNull = new BsonNull(); + BsonValue bsonInt32 = new BsonInt32(42); + BsonValue bsonInt64 = new BsonInt64(52L); + BsonValue bsonDecimal128 = new BsonDecimal128(Decimal128.parse("1.0")); + BsonValue bsonBoolean = new BsonBoolean(true); + BsonValue bsonDateTime = new BsonDateTime(new Date().getTime()); + BsonValue bsonDouble = new BsonDouble(62.0); + BsonValue bsonString = new BsonString("the fox ..."); + BsonValue minKey = new BsonMinKey(); + BsonValue maxKey = new BsonMaxKey(); + BsonValue javaScript = new BsonJavaScript("int i = 0;"); + BsonValue objectId = new BsonObjectId(new ObjectId()); + BsonValue scope = new BsonJavaScriptWithScope("int x = y", new BsonDocument("y", new BsonInt32(1))); + BsonValue regularExpression = new BsonRegularExpression("^test.*regex.*xyz$", "i"); + BsonValue symbol = new BsonSymbol("ruby stuff"); + BsonValue timestamp = new BsonTimestamp(0x12345678, 5); + BsonValue undefined = new BsonUndefined(); + BsonValue binary = new BsonBinary((byte) 80, new byte[] {5, 4, 3, 2, 1}); + BsonValue array = new BsonArray(); + BsonValue document = new BsonDocument("a", new BsonInt32(1)); + BsonValue dbPointer = new BsonDbPointer("db.coll", new ObjectId()); + + ByteBufBsonArray bsonArray = fromBsonValues(asList( + bsonNull, bsonInt32, bsonInt64, bsonDecimal128, bsonBoolean, bsonDateTime, bsonDouble, bsonString, minKey, maxKey, + javaScript, objectId, scope, regularExpression, symbol, timestamp, undefined, binary, array, document, dbPointer)); + assertEquals(bsonNull, bsonArray.get(0)); + assertEquals(bsonInt32, bsonArray.get(1)); + assertEquals(bsonInt64, bsonArray.get(2)); + assertEquals(bsonDecimal128, bsonArray.get(3)); + assertEquals(bsonBoolean, bsonArray.get(4)); + assertEquals(bsonDateTime, bsonArray.get(5)); + assertEquals(bsonDouble, bsonArray.get(6)); + assertEquals(bsonString, bsonArray.get(7)); + assertEquals(minKey, bsonArray.get(8)); + assertEquals(maxKey, bsonArray.get(9)); + assertEquals(javaScript, bsonArray.get(10)); + assertEquals(objectId, bsonArray.get(11)); + assertEquals(scope, bsonArray.get(12)); + assertEquals(regularExpression, bsonArray.get(13)); + assertEquals(symbol, bsonArray.get(14)); + assertEquals(timestamp, bsonArray.get(15)); + assertEquals(undefined, bsonArray.get(16)); + assertEquals(binary, bsonArray.get(17)); + assertEquals(array, bsonArray.get(18)); + assertEquals(document, bsonArray.get(19)); + assertEquals(dbPointer, bsonArray.get(20)); + } + + static ByteBufBsonArray fromBsonValues(final List values) { + BsonDocument document = new BsonDocument() + .append("a", new BsonArray(values)); + BasicOutputBuffer buffer = new BasicOutputBuffer(); + new BsonDocumentCodec().encode(new BsonBinaryWriter(buffer), document, EncoderContext.builder().build()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + buffer.pipe(baos); + } catch (IOException e) { + throw new RuntimeException("impossible!"); + } + ByteBuf documentByteBuf = new ByteBufNIO(ByteBuffer.wrap(baos.toByteArray())); + return (ByteBufBsonArray) new ByteBufBsonDocument(documentByteBuf).entrySet().iterator().next().getValue(); + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonDocumentSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonDocumentSpecification.groovy index 7f60b08cfd7..8dc599706a9 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonDocumentSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufBsonDocumentSpecification.groovy @@ -36,7 +36,6 @@ import spock.lang.Specification import java.nio.ByteBuffer import static java.util.Arrays.asList -import static util.GroovyHelpers.areEqual class ByteBufBsonDocumentSpecification extends Specification { def emptyDocumentByteBuf = new ByteBufNIO(ByteBuffer.wrap([5, 0, 0, 0, 0] as byte[])) @@ -224,21 +223,6 @@ class ByteBufBsonDocumentSpecification extends Specification { reader.close() } - def 'hashCode should equal hash code of identical BsonDocument'() { - expect: - byteBufDocument.hashCode() == document.hashCode() - documentByteBuf.referenceCount == 1 - } - - def 'equals should equal identical BsonDocument'() { - expect: - areEqual(byteBufDocument, document) - areEqual(document, byteBufDocument) - areEqual(byteBufDocument, byteBufDocument) - !areEqual(byteBufDocument, emptyByteBufDocument) - documentByteBuf.referenceCount == 1 - } - def 'clone should make a deep copy'() { when: BsonDocument cloned = byteBufDocument.clone()