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 {