Skip to content

Commit a525a64

Browse files
committed
Introduce Util.mutableDeepCopy
JAVA-4874
1 parent d439e79 commit a525a64

File tree

8 files changed

+218
-36
lines changed

8 files changed

+218
-36
lines changed

bson/src/main/org/bson/RawBsonDocument.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ public int hashCode() {
328328

329329
@Override
330330
public BsonDocument clone() {
331-
return toBaseBsonDocument();
331+
return new RawBsonDocument(bytes.clone(), offset, length);
332332
}
333333

334334
private BsonBinaryReader createReader() {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.bson.internal;
17+
18+
import org.bson.BsonArray;
19+
import org.bson.BsonBinary;
20+
import org.bson.BsonDocument;
21+
import org.bson.BsonJavaScriptWithScope;
22+
import org.bson.BsonValue;
23+
24+
/**
25+
* <p>This class is not part of the public API and may be removed or changed at any time</p>
26+
*/
27+
public final class Util {
28+
public static BsonDocument mutableDeepCopy(final BsonDocument original) {
29+
BsonDocument copy = new BsonDocument(original.size());
30+
original.forEach((key, value) -> copy.put(key, mutableDeepCopy(value)));
31+
return copy;
32+
}
33+
34+
private static BsonArray mutableDeepCopy(final BsonArray original) {
35+
BsonArray copy = new BsonArray(original.size());
36+
original.forEach(element -> copy.add(mutableDeepCopy(element)));
37+
return copy;
38+
}
39+
40+
private static BsonBinary mutableDeepCopy(final BsonBinary original) {
41+
return new BsonBinary(original.getType(), original.getData().clone());
42+
}
43+
44+
private static BsonJavaScriptWithScope mutableDeepCopy(final BsonJavaScriptWithScope original) {
45+
return new BsonJavaScriptWithScope(original.getCode(), mutableDeepCopy(original.getScope()));
46+
}
47+
48+
private static <T extends BsonValue> T mutableDeepCopy(final T original) {
49+
BsonValue copy;
50+
switch (original.getBsonType()) {
51+
case DOCUMENT:
52+
copy = mutableDeepCopy(original.asDocument());
53+
break;
54+
case ARRAY:
55+
copy = mutableDeepCopy(original.asArray());
56+
break;
57+
case BINARY:
58+
copy = mutableDeepCopy(original.asBinary());
59+
break;
60+
case JAVASCRIPT_WITH_SCOPE:
61+
copy = mutableDeepCopy(original.asJavaScriptWithScope());
62+
break;
63+
default:
64+
copy = original;
65+
}
66+
@SuppressWarnings("unchecked")
67+
T result = (T) copy;
68+
return result;
69+
}
70+
71+
private Util() {
72+
}
73+
}

bson/src/test/unit/org/bson/BsonDocumentTest.java

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,13 @@
2323
import org.bson.json.JsonReader;
2424
import org.bson.json.JsonWriter;
2525
import org.bson.json.JsonWriterSettings;
26-
import org.junit.jupiter.api.Test;
26+
import org.junit.Test;
2727

2828
import java.io.StringWriter;
2929
import java.util.Arrays;
30-
import java.util.function.Consumer;
3130

32-
import static org.junit.jupiter.api.Assertions.assertAll;
33-
import static org.junit.jupiter.api.Assertions.assertEquals;
34-
import static org.junit.jupiter.api.Assertions.assertNotEquals;
35-
import static org.junit.jupiter.api.Assertions.assertNotSame;
31+
import static org.junit.Assert.assertEquals;
32+
import static org.junit.Assert.assertNotEquals;
3633

3734
// Don't convert to Spock, as Groovy intercepts equals/hashCode methods that we are trying to test
3835
public class BsonDocumentTest {
@@ -109,28 +106,4 @@ public void toStringShouldEqualToJson() {
109106
public void shouldParseJson() {
110107
assertEquals(new BsonDocument("a", new BsonInt32(1)), BsonDocument.parse("{\"a\" : 1}"));
111108
}
112-
113-
@Test
114-
public void cloneIsDeepCopyAndMutable() {
115-
Consumer<BsonDocument> assertCloneDeepCopyMutable = original -> {
116-
BsonDocument clone = original.clone();
117-
assertNotSame(original, clone);
118-
assertEquals(original, clone);
119-
// check that mutating `clone` does not mutate `original`
120-
clone.getDocument("k1").put("k2", new BsonString("clone"));
121-
assertEquals(new BsonString("clone"), clone.getDocument("k1").get("k2"));
122-
assertEquals(BsonNull.VALUE, original.getDocument("k1").get("k2"));
123-
// check that mutating `original` (if it is mutable) does not mutate `clone`
124-
if (!(original instanceof RawBsonDocument)) {
125-
original.put("k1", new BsonDocument("k2", new BsonString("original")));
126-
assertEquals(new BsonString("original"), original.getDocument("k1").get("k2"));
127-
assertEquals(new BsonString("clone"), clone.getDocument("k1").get("k2"));
128-
}
129-
};
130-
assertAll(
131-
() -> assertCloneDeepCopyMutable.accept(new BsonDocument("k1", new BsonDocument("k2", BsonNull.VALUE))),
132-
() -> assertCloneDeepCopyMutable.accept(new BsonDocument("k1", RawBsonDocument.parse("{'k2': null}"))),
133-
() -> assertCloneDeepCopyMutable.accept(RawBsonDocument.parse("{'k1': {'k2': null}}"))
134-
);
135-
}
136109
}

bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,10 +395,11 @@ class RawBsonDocumentSpecification extends Specification {
395395

396396
def 'clone should make a deep copy'() {
397397
when:
398-
BsonDocument cloned = rawDocument.clone()
398+
RawBsonDocument cloned = rawDocument.clone()
399399

400400
then:
401-
cloned.getClass() == BsonDocument
401+
!cloned.getByteBuffer().array().is(createRawDocumenFromDocument().getByteBuffer().array())
402+
cloned.getByteBuffer().remaining() == rawDocument.getByteBuffer().remaining()
402403
cloned == createRawDocumenFromDocument()
403404

404405
where:
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.bson.internal;
17+
18+
import org.bson.BsonArray;
19+
import org.bson.BsonBinary;
20+
import org.bson.BsonDocument;
21+
import org.bson.BsonDocumentWrapper;
22+
import org.bson.BsonJavaScriptWithScope;
23+
import org.bson.BsonValue;
24+
import org.bson.RawBsonArray;
25+
import org.bson.RawBsonDocument;
26+
import org.bson.conversions.Bson;
27+
import org.junit.jupiter.api.Test;
28+
29+
import java.nio.charset.StandardCharsets;
30+
import java.util.AbstractMap.SimpleImmutableEntry;
31+
import java.util.Map.Entry;
32+
33+
import static java.util.Arrays.asList;
34+
import static java.util.Collections.singletonList;
35+
import static org.junit.jupiter.api.Assertions.assertEquals;
36+
import static org.junit.jupiter.api.Assertions.assertNotSame;
37+
38+
final class UtilTest {
39+
@Test
40+
public void mutableDeepCopy() {
41+
Entry<String, BsonBinary> originalBsonBinaryEntry = new SimpleImmutableEntry<>(
42+
"bsonBinary",
43+
new BsonBinary("bsonBinary".getBytes(StandardCharsets.UTF_8))
44+
);
45+
Entry<String, BsonJavaScriptWithScope> originalBsonJavaScriptWithScopeEntry = new SimpleImmutableEntry<>(
46+
"bsonJavaScriptWithScopeEntry",
47+
new BsonJavaScriptWithScope("\"use strict\";", new BsonDocument())
48+
);
49+
Entry<String, RawBsonDocument> originalRawBsonDocumentEntry = new SimpleImmutableEntry<>(
50+
"rawBsonDocument",
51+
RawBsonDocument.parse("{rawBsonDocument: 'rawBsonDocument_value'}")
52+
);
53+
Entry<String, BsonDocumentWrapper<RawBsonDocument>> originalBsonDocumentWrapperEntry = new SimpleImmutableEntry<>(
54+
"bsonDocumentWrapper",
55+
new BsonDocumentWrapper<>(originalRawBsonDocumentEntry.getValue(), Bson.DEFAULT_CODEC_REGISTRY.get(RawBsonDocument.class))
56+
);
57+
Entry<String, BsonDocument> originalBsonDocumentEntry = new SimpleImmutableEntry<>(
58+
"bsonDocument",
59+
new BsonDocument()
60+
.append(originalBsonBinaryEntry.getKey(), originalBsonBinaryEntry.getValue())
61+
.append(originalBsonJavaScriptWithScopeEntry.getKey(), originalBsonJavaScriptWithScopeEntry.getValue())
62+
.append(originalRawBsonDocumentEntry.getKey(), originalRawBsonDocumentEntry.getValue())
63+
.append(originalBsonDocumentWrapperEntry.getKey(), originalBsonDocumentWrapperEntry.getValue())
64+
);
65+
Entry<String, BsonArray> originalBsonArrayEntry = new SimpleImmutableEntry<>(
66+
"bsonArray",
67+
new BsonArray(singletonList(new BsonArray()))
68+
);
69+
Entry<String, RawBsonArray> originalRawBsonArrayEntry = new SimpleImmutableEntry<>(
70+
"rawBsonArray",
71+
rawBsonArray(
72+
originalBsonBinaryEntry.getValue(),
73+
originalBsonJavaScriptWithScopeEntry.getValue(),
74+
originalRawBsonDocumentEntry.getValue(),
75+
originalBsonDocumentWrapperEntry.getValue(),
76+
originalBsonDocumentEntry.getValue(),
77+
originalBsonArrayEntry.getValue())
78+
);
79+
BsonDocument original = new BsonDocument()
80+
.append(originalBsonBinaryEntry.getKey(), originalBsonBinaryEntry.getValue())
81+
.append(originalBsonJavaScriptWithScopeEntry.getKey(), originalBsonJavaScriptWithScopeEntry.getValue())
82+
.append(originalRawBsonDocumentEntry.getKey(), originalRawBsonDocumentEntry.getValue())
83+
.append(originalBsonDocumentWrapperEntry.getKey(), originalBsonDocumentWrapperEntry.getValue())
84+
.append(originalBsonDocumentEntry.getKey(), originalBsonDocumentEntry.getValue())
85+
.append(originalBsonArrayEntry.getKey(), originalBsonArrayEntry.getValue())
86+
.append(originalRawBsonArrayEntry.getKey(), originalRawBsonArrayEntry.getValue());
87+
BsonDocument copy = Util.mutableDeepCopy(original);
88+
assertEqualNotSameAndMutable(original, copy);
89+
original.forEach((key, value) -> assertEqualNotSameAndMutable(value, copy.get(key)));
90+
// check nested document
91+
String nestedDocumentKey = originalBsonDocumentEntry.getKey();
92+
BsonDocument originalNestedDocument = original.getDocument(nestedDocumentKey);
93+
BsonDocument copyNestedDocument = copy.getDocument(nestedDocumentKey);
94+
assertEqualNotSameAndMutable(originalNestedDocument, copyNestedDocument);
95+
originalNestedDocument.forEach((key, value) -> assertEqualNotSameAndMutable(value, copyNestedDocument.get(key)));
96+
// check nested array
97+
String nestedArrayKey = originalRawBsonArrayEntry.getKey();
98+
BsonArray originalNestedArray = original.getArray(nestedArrayKey);
99+
BsonArray copyNestedArray = copy.getArray(nestedArrayKey);
100+
assertEqualNotSameAndMutable(originalNestedArray, copyNestedArray);
101+
for (int i = 0; i < originalNestedArray.size(); i++) {
102+
assertEqualNotSameAndMutable(originalNestedArray.get(i), copyNestedArray.get(i));
103+
}
104+
}
105+
106+
private static RawBsonArray rawBsonArray(final BsonValue... elements) {
107+
return (RawBsonArray) new RawBsonDocument(
108+
new BsonDocument("a", new BsonArray(asList(elements))), Bson.DEFAULT_CODEC_REGISTRY.get(BsonDocument.class))
109+
.get("a");
110+
}
111+
112+
private static void assertEqualNotSameAndMutable(final Object expected, final Object actual) {
113+
assertEquals(expected, actual);
114+
assertNotSame(expected, actual);
115+
Class<?> actualClass = actual.getClass();
116+
if (expected instanceof BsonDocument) {
117+
assertEquals(BsonDocument.class, actualClass);
118+
} else if (expected instanceof BsonArray) {
119+
assertEquals(BsonArray.class, actualClass);
120+
} else if (expected instanceof BsonBinary) {
121+
assertEquals(BsonBinary.class, actualClass);
122+
} else if (expected instanceof BsonJavaScriptWithScope) {
123+
assertEquals(BsonJavaScriptWithScope.class, actualClass);
124+
} else {
125+
org.bson.assertions.Assertions.fail("Unexpected " + expected.getClass().toString());
126+
}
127+
}
128+
129+
private UtilTest() {
130+
}
131+
}

driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import java.util.Optional;
2828
import java.util.function.Consumer;
2929

30+
import static org.bson.internal.Util.mutableDeepCopy;
31+
3032
/**
3133
* A {@link Bson} that allows constructing new instances via {@link #newAppended(String, Object)} instead of mutating {@code this}.
3234
* See {@link #AbstractConstructibleBson(Bson, Document)} for the note on mutability.
@@ -141,7 +143,7 @@ public String toString() {
141143
}
142144

143145
static BsonDocument newMerged(final BsonDocument base, final BsonDocument appended) {
144-
BsonDocument result = base.clone();
146+
BsonDocument result = mutableDeepCopy(base);
145147
result.putAll(appended);
146148
return result;
147149
}

driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import static java.lang.String.format;
6464
import static java.util.Arrays.asList;
6565
import static java.util.Collections.singletonList;
66+
import static org.bson.internal.Util.mutableDeepCopy;
6667

6768
/**
6869
* <p>This class is not part of the public API and may be removed or changed at any time</p>
@@ -223,7 +224,7 @@ public Publisher<BsonDocument> createEncryptedCollection(final MongoDatabase dat
223224
return Mono.defer(() -> {
224225
// `Mono.defer` results in `maybeUpdatedEncryptedFields` and `dataKeyMightBeCreated` (mutable state)
225226
// being created once per `Subscriber`, which allows the produced `Mono` to support multiple `Subscribers`.
226-
BsonDocument maybeUpdatedEncryptedFields = encryptedFields.clone();
227+
BsonDocument maybeUpdatedEncryptedFields = mutableDeepCopy(encryptedFields);
227228
AtomicBoolean dataKeyMightBeCreated = new AtomicBoolean();
228229
Iterable<Mono<BsonDocument>> publishersOfUpdatedFields = () -> maybeUpdatedEncryptedFields.get("fields").asArray()
229230
.stream()

driver-sync/src/main/com/mongodb/client/internal/ClientEncryptionImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import static java.lang.String.format;
5959
import static java.util.Arrays.asList;
6060
import static java.util.Collections.singletonList;
61+
import static org.bson.internal.Util.mutableDeepCopy;
6162

6263
/**
6364
* <p>This class is not part of the public API and may be removed or changed at any time</p>
@@ -207,7 +208,7 @@ public BsonDocument createEncryptedCollection(final MongoDatabase database, fina
207208
dataKeyOptions.masterKey(masterKey);
208209
}
209210
String keyIdBsonKey = "keyId";
210-
BsonDocument maybeUpdatedEncryptedFields = encryptedFields.clone();
211+
BsonDocument maybeUpdatedEncryptedFields = mutableDeepCopy(encryptedFields);
211212
// only the mutability of `dataKeyMightBeCreated` is important, it does not need to be thread-safe
212213
AtomicBoolean dataKeyMightBeCreated = new AtomicBoolean();
213214
try {

0 commit comments

Comments
 (0)