diff --git a/bson/src/main/org/bson/internal/BsonUtil.java b/bson/src/main/org/bson/internal/BsonUtil.java new file mode 100644 index 00000000000..6879c4c0e12 --- /dev/null +++ b/bson/src/main/org/bson/internal/BsonUtil.java @@ -0,0 +1,65 @@ +/* + * 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 org.bson.internal; + +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonJavaScriptWithScope; +import org.bson.BsonValue; + +/** + *

This class is not part of the public API and may be removed or changed at any time

+ */ +public final class BsonUtil { + public static BsonDocument mutableDeepCopy(final BsonDocument original) { + BsonDocument copy = new BsonDocument(original.size()); + original.forEach((key, value) -> copy.put(key, mutableDeepCopy(value))); + return copy; + } + + private static BsonArray mutableDeepCopy(final BsonArray original) { + BsonArray copy = new BsonArray(original.size()); + original.forEach(element -> copy.add(mutableDeepCopy(element))); + return copy; + } + + private static BsonBinary mutableDeepCopy(final BsonBinary original) { + return new BsonBinary(original.getType(), original.getData().clone()); + } + + private static BsonJavaScriptWithScope mutableDeepCopy(final BsonJavaScriptWithScope original) { + return new BsonJavaScriptWithScope(original.getCode(), mutableDeepCopy(original.getScope())); + } + + private static BsonValue mutableDeepCopy(final BsonValue original) { + switch (original.getBsonType()) { + case DOCUMENT: + return mutableDeepCopy(original.asDocument()); + case ARRAY: + return mutableDeepCopy(original.asArray()); + case BINARY: + return mutableDeepCopy(original.asBinary()); + case JAVASCRIPT_WITH_SCOPE: + return mutableDeepCopy(original.asJavaScriptWithScope()); + default: + return original; + } + } + + private BsonUtil() { + } +} diff --git a/bson/src/test/unit/org/bson/internal/BsonUtilTest.java b/bson/src/test/unit/org/bson/internal/BsonUtilTest.java new file mode 100644 index 00000000000..8c41c45b1b3 --- /dev/null +++ b/bson/src/test/unit/org/bson/internal/BsonUtilTest.java @@ -0,0 +1,131 @@ +/* + * 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 org.bson.internal; + +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonDocumentWrapper; +import org.bson.BsonJavaScriptWithScope; +import org.bson.BsonValue; +import org.bson.RawBsonArray; +import org.bson.RawBsonDocument; +import org.bson.conversions.Bson; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Map.Entry; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; + +final class BsonUtilTest { + @Test + public void mutableDeepCopy() { + Entry originalBsonBinaryEntry = new SimpleImmutableEntry<>( + "bsonBinary", + new BsonBinary("bsonBinary".getBytes(StandardCharsets.UTF_8)) + ); + Entry originalBsonJavaScriptWithScopeEntry = new SimpleImmutableEntry<>( + "bsonJavaScriptWithScopeEntry", + new BsonJavaScriptWithScope("\"use strict\";", new BsonDocument()) + ); + Entry originalRawBsonDocumentEntry = new SimpleImmutableEntry<>( + "rawBsonDocument", + RawBsonDocument.parse("{rawBsonDocument: 'rawBsonDocument_value'}") + ); + Entry> originalBsonDocumentWrapperEntry = new SimpleImmutableEntry<>( + "bsonDocumentWrapper", + new BsonDocumentWrapper<>(originalRawBsonDocumentEntry.getValue(), Bson.DEFAULT_CODEC_REGISTRY.get(RawBsonDocument.class)) + ); + Entry originalBsonDocumentEntry = new SimpleImmutableEntry<>( + "bsonDocument", + new BsonDocument() + .append(originalBsonBinaryEntry.getKey(), originalBsonBinaryEntry.getValue()) + .append(originalBsonJavaScriptWithScopeEntry.getKey(), originalBsonJavaScriptWithScopeEntry.getValue()) + .append(originalRawBsonDocumentEntry.getKey(), originalRawBsonDocumentEntry.getValue()) + .append(originalBsonDocumentWrapperEntry.getKey(), originalBsonDocumentWrapperEntry.getValue()) + ); + Entry originalBsonArrayEntry = new SimpleImmutableEntry<>( + "bsonArray", + new BsonArray(singletonList(new BsonArray())) + ); + Entry originalRawBsonArrayEntry = new SimpleImmutableEntry<>( + "rawBsonArray", + rawBsonArray( + originalBsonBinaryEntry.getValue(), + originalBsonJavaScriptWithScopeEntry.getValue(), + originalRawBsonDocumentEntry.getValue(), + originalBsonDocumentWrapperEntry.getValue(), + originalBsonDocumentEntry.getValue(), + originalBsonArrayEntry.getValue()) + ); + BsonDocument original = new BsonDocument() + .append(originalBsonBinaryEntry.getKey(), originalBsonBinaryEntry.getValue()) + .append(originalBsonJavaScriptWithScopeEntry.getKey(), originalBsonJavaScriptWithScopeEntry.getValue()) + .append(originalRawBsonDocumentEntry.getKey(), originalRawBsonDocumentEntry.getValue()) + .append(originalBsonDocumentWrapperEntry.getKey(), originalBsonDocumentWrapperEntry.getValue()) + .append(originalBsonDocumentEntry.getKey(), originalBsonDocumentEntry.getValue()) + .append(originalBsonArrayEntry.getKey(), originalBsonArrayEntry.getValue()) + .append(originalRawBsonArrayEntry.getKey(), originalRawBsonArrayEntry.getValue()); + BsonDocument copy = BsonUtil.mutableDeepCopy(original); + assertEqualNotSameAndMutable(original, copy); + original.forEach((key, value) -> assertEqualNotSameAndMutable(value, copy.get(key))); + // check nested document + String nestedDocumentKey = originalBsonDocumentEntry.getKey(); + BsonDocument originalNestedDocument = original.getDocument(nestedDocumentKey); + BsonDocument copyNestedDocument = copy.getDocument(nestedDocumentKey); + assertEqualNotSameAndMutable(originalNestedDocument, copyNestedDocument); + originalNestedDocument.forEach((key, value) -> assertEqualNotSameAndMutable(value, copyNestedDocument.get(key))); + // check nested array + String nestedArrayKey = originalRawBsonArrayEntry.getKey(); + BsonArray originalNestedArray = original.getArray(nestedArrayKey); + BsonArray copyNestedArray = copy.getArray(nestedArrayKey); + assertEqualNotSameAndMutable(originalNestedArray, copyNestedArray); + for (int i = 0; i < originalNestedArray.size(); i++) { + assertEqualNotSameAndMutable(originalNestedArray.get(i), copyNestedArray.get(i)); + } + } + + private static RawBsonArray rawBsonArray(final BsonValue... elements) { + return (RawBsonArray) new RawBsonDocument( + new BsonDocument("a", new BsonArray(asList(elements))), Bson.DEFAULT_CODEC_REGISTRY.get(BsonDocument.class)) + .get("a"); + } + + private static void assertEqualNotSameAndMutable(final Object expected, final Object actual) { + assertEquals(expected, actual); + assertNotSame(expected, actual); + Class actualClass = actual.getClass(); + if (expected instanceof BsonDocument) { + assertEquals(BsonDocument.class, actualClass); + } else if (expected instanceof BsonArray) { + assertEquals(BsonArray.class, actualClass); + } else if (expected instanceof BsonBinary) { + assertEquals(BsonBinary.class, actualClass); + } else if (expected instanceof BsonJavaScriptWithScope) { + assertEquals(BsonJavaScriptWithScope.class, actualClass); + } else { + org.bson.assertions.Assertions.fail("Unexpected " + expected.getClass().toString()); + } + } + + private BsonUtilTest() { + } +} diff --git a/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java b/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java index 138e62f5d3f..278f7e273be 100644 --- a/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java +++ b/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java @@ -27,6 +27,8 @@ import java.util.Optional; import java.util.function.Consumer; +import static org.bson.internal.BsonUtil.mutableDeepCopy; + /** * A {@link Bson} that allows constructing new instances via {@link #newAppended(String, Object)} instead of mutating {@code this}. * See {@link #AbstractConstructibleBson(Bson, Document)} for the note on mutability. @@ -141,7 +143,7 @@ public String toString() { } static BsonDocument newMerged(final BsonDocument base, final BsonDocument appended) { - BsonDocument result = base.clone(); + BsonDocument result = mutableDeepCopy(base); result.putAll(appended); return result; } diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java index 4887e0109a9..5b4331fa982 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java @@ -63,6 +63,7 @@ import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static org.bson.internal.BsonUtil.mutableDeepCopy; /** *

This class is not part of the public API and may be removed or changed at any time

@@ -223,7 +224,7 @@ public Publisher createEncryptedCollection(final MongoDatabase dat return Mono.defer(() -> { // `Mono.defer` results in `maybeUpdatedEncryptedFields` and `dataKeyMightBeCreated` (mutable state) // being created once per `Subscriber`, which allows the produced `Mono` to support multiple `Subscribers`. - BsonDocument maybeUpdatedEncryptedFields = encryptedFields.clone(); + BsonDocument maybeUpdatedEncryptedFields = mutableDeepCopy(encryptedFields); AtomicBoolean dataKeyMightBeCreated = new AtomicBoolean(); Iterable> publishersOfUpdatedFields = () -> maybeUpdatedEncryptedFields.get("fields").asArray() .stream() diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java index d1080245922..b8462f058c2 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java @@ -58,6 +58,7 @@ import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static org.bson.internal.BsonUtil.mutableDeepCopy; /** *

This class is not part of the public API and may be removed or changed at any time

@@ -207,7 +208,7 @@ public BsonDocument createEncryptedCollection(final MongoDatabase database, fina dataKeyOptions.masterKey(masterKey); } String keyIdBsonKey = "keyId"; - BsonDocument maybeUpdatedEncryptedFields = encryptedFields.clone(); + BsonDocument maybeUpdatedEncryptedFields = mutableDeepCopy(encryptedFields); // only the mutability of `dataKeyMightBeCreated` is important, it does not need to be thread-safe AtomicBoolean dataKeyMightBeCreated = new AtomicBoolean(); try {