Skip to content

Commit fc342ff

Browse files
committed
Support List<T> and Map<String, T> components in Java records
This enhances the support for any record component that is a generic List or Map where the generic type of the List or Map is a record or POJO. It used to incorrectly decode to org.bson.Document. Now it respects the generic type of the List or Map when decoding. JAVA-4667
1 parent 3718a27 commit fc342ff

13 files changed

+484
-3
lines changed

bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.bson.codecs.Codec;
2323
import org.bson.codecs.DecoderContext;
2424
import org.bson.codecs.EncoderContext;
25+
import org.bson.codecs.Parameterizable;
2526
import org.bson.codecs.RepresentationConfigurable;
2627
import org.bson.codecs.configuration.CodecConfigurationException;
2728
import org.bson.codecs.configuration.CodecRegistry;
@@ -35,6 +36,7 @@
3536
import java.lang.annotation.Annotation;
3637
import java.lang.reflect.Constructor;
3738
import java.lang.reflect.InvocationTargetException;
39+
import java.lang.reflect.ParameterizedType;
3840
import java.lang.reflect.RecordComponent;
3941
import java.util.ArrayList;
4042
import java.util.Arrays;
@@ -83,6 +85,10 @@ Object getValue(final Record record) throws InvocationTargetException, IllegalAc
8385
@SuppressWarnings("deprecation")
8486
private static Codec<?> computeCodec(final RecordComponent component, final CodecRegistry codecRegistry) {
8587
var codec = codecRegistry.get(toWrapper(component.getType()));
88+
if (codec instanceof Parameterizable parameterizableCodec
89+
&& component.getGenericType() instanceof ParameterizedType parameterizedType) {
90+
codec = parameterizableCodec.parameterize(codecRegistry, Arrays.asList(parameterizedType.getActualTypeArguments()));
91+
}
8692
BsonType bsonRepresentationType = null;
8793

8894
if (component.isAnnotationPresent(BsonRepresentation.class)) {

bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.bson.codecs.DecoderContext;
2727
import org.bson.codecs.EncoderContext;
2828
import org.bson.codecs.configuration.CodecConfigurationException;
29+
import org.bson.codecs.record.samples.TestRecordEmbedded;
2930
import org.bson.codecs.record.samples.TestRecordWithDeprecatedAnnotations;
3031
import org.bson.codecs.record.samples.TestRecordWithIllegalBsonCreatorOnConstructor;
3132
import org.bson.codecs.record.samples.TestRecordWithIllegalBsonCreatorOnMethod;
@@ -39,13 +40,19 @@
3940
import org.bson.codecs.record.samples.TestRecordWithIllegalBsonPropertyOnAccessor;
4041
import org.bson.codecs.record.samples.TestRecordWithIllegalBsonPropertyOnCanonicalConstructor;
4142
import org.bson.codecs.record.samples.TestRecordWithIllegalBsonRepresentationOnAccessor;
43+
import org.bson.codecs.record.samples.TestRecordWithListOfListOfRecords;
44+
import org.bson.codecs.record.samples.TestRecordWithListOfRecords;
45+
import org.bson.codecs.record.samples.TestRecordWithMapOfListOfRecords;
46+
import org.bson.codecs.record.samples.TestRecordWithMapOfRecords;
4247
import org.bson.codecs.record.samples.TestRecordWithPojoAnnotations;
4348
import org.bson.conversions.Bson;
4449
import org.bson.types.ObjectId;
4550
import org.junit.jupiter.api.Test;
4651

4752
import java.util.List;
53+
import java.util.Map;
4854

55+
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
4956
import static org.junit.jupiter.api.Assertions.assertEquals;
5057
import static org.junit.jupiter.api.Assertions.assertThrows;
5158

@@ -107,6 +114,119 @@ public void testRecordWithPojoAnnotations() {
107114
assertEquals(testRecord, decoded);
108115
}
109116

117+
@Test
118+
public void testRecordWithNestedListOfRecords() {
119+
var codec = new RecordCodec<>(TestRecordWithListOfRecords.class,
120+
fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY));
121+
var identifier = new ObjectId();
122+
var testRecord = new TestRecordWithListOfRecords(identifier, List.of(new TestRecordEmbedded("embedded")));
123+
124+
var document = new BsonDocument();
125+
var writer = new BsonDocumentWriter(document);
126+
127+
// when
128+
codec.encode(writer, testRecord, EncoderContext.builder().build());
129+
130+
// then
131+
assertEquals(
132+
new BsonDocument("_id", new BsonObjectId(identifier))
133+
.append("nestedRecords", new BsonArray(List.of(new BsonDocument("name", new BsonString("embedded"))))),
134+
document);
135+
assertEquals("_id", document.getFirstKey());
136+
137+
// when
138+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
139+
140+
// then
141+
assertEquals(testRecord, decoded);
142+
}
143+
144+
@Test
145+
public void testRecordWithNestedListOfListOfRecords() {
146+
var codec = new RecordCodec<>(TestRecordWithListOfListOfRecords.class,
147+
fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY));
148+
var identifier = new ObjectId();
149+
var testRecord = new TestRecordWithListOfListOfRecords(identifier, List.of(List.of(new TestRecordEmbedded("embedded"))));
150+
151+
var document = new BsonDocument();
152+
var writer = new BsonDocumentWriter(document);
153+
154+
// when
155+
codec.encode(writer, testRecord, EncoderContext.builder().build());
156+
157+
// then
158+
assertEquals(
159+
new BsonDocument("_id", new BsonObjectId(identifier))
160+
.append("nestedRecords",
161+
new BsonArray(List.of(new BsonArray(List.of(new BsonDocument("name", new BsonString("embedded"))))))),
162+
document);
163+
assertEquals("_id", document.getFirstKey());
164+
165+
// when
166+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
167+
168+
// then
169+
assertEquals(testRecord, decoded);
170+
}
171+
172+
@Test
173+
public void testRecordWithNestedMapOfRecords() {
174+
var codec = new RecordCodec<>(TestRecordWithMapOfRecords.class,
175+
fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY));
176+
var identifier = new ObjectId();
177+
var testRecord = new TestRecordWithMapOfRecords(identifier,
178+
Map.of("first", new TestRecordEmbedded("embedded")));
179+
180+
var document = new BsonDocument();
181+
var writer = new BsonDocumentWriter(document);
182+
183+
// when
184+
codec.encode(writer, testRecord, EncoderContext.builder().build());
185+
186+
// then
187+
assertEquals(
188+
new BsonDocument("_id", new BsonObjectId(identifier))
189+
.append("nestedRecords", new BsonDocument("first", new BsonDocument("name", new BsonString("embedded")))),
190+
document);
191+
assertEquals("_id", document.getFirstKey());
192+
193+
// when
194+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
195+
196+
// then
197+
assertEquals(testRecord, decoded);
198+
}
199+
200+
@Test
201+
public void testRecordWithNestedMapOfListRecords() {
202+
var codec = new RecordCodec<>(TestRecordWithMapOfListOfRecords.class,
203+
fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY));
204+
var identifier = new ObjectId();
205+
var testRecord = new TestRecordWithMapOfListOfRecords(identifier,
206+
Map.of("first", List.of(new TestRecordEmbedded("embedded"))));
207+
208+
var document = new BsonDocument();
209+
var writer = new BsonDocumentWriter(document);
210+
211+
// when
212+
codec.encode(writer, testRecord, EncoderContext.builder().build());
213+
214+
// then
215+
assertEquals(
216+
new BsonDocument("_id", new BsonObjectId(identifier))
217+
.append("nestedRecords",
218+
new BsonDocument("first",
219+
new BsonArray(List.of(new BsonDocument("name", new BsonString("embedded")))))),
220+
document);
221+
assertEquals("_id", document.getFirstKey());
222+
223+
// when
224+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
225+
226+
// then
227+
assertEquals(testRecord, decoded);
228+
}
229+
110230
@Test
111231
public void testRecordWithNulls() {
112232
var codec = new RecordCodec<>(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
17+
package org.bson.codecs.record.samples;
18+
19+
public record TestRecordEmbedded(String name) {
20+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
17+
package org.bson.codecs.record.samples;
18+
19+
import org.bson.codecs.pojo.annotations.BsonId;
20+
import org.bson.types.ObjectId;
21+
22+
import java.util.List;
23+
24+
public record TestRecordWithListOfListOfRecords(@BsonId ObjectId id, List<List<TestRecordEmbedded>> nestedRecords) {
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
17+
package org.bson.codecs.record.samples;
18+
19+
import org.bson.codecs.pojo.annotations.BsonId;
20+
import org.bson.types.ObjectId;
21+
22+
import java.util.List;
23+
24+
public record TestRecordWithListOfRecords(@BsonId ObjectId id, List<TestRecordEmbedded> nestedRecords) {
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
17+
package org.bson.codecs.record.samples;
18+
19+
import org.bson.codecs.pojo.annotations.BsonId;
20+
import org.bson.types.ObjectId;
21+
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
public record TestRecordWithMapOfListOfRecords(@BsonId ObjectId id, Map<String, List<TestRecordEmbedded>> nestedRecords) {
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
17+
package org.bson.codecs.record.samples;
18+
19+
import org.bson.codecs.pojo.annotations.BsonId;
20+
import org.bson.types.ObjectId;
21+
22+
import java.util.Map;
23+
24+
public record TestRecordWithMapOfRecords(@BsonId ObjectId id, Map<String, TestRecordEmbedded> nestedRecords) {
25+
}

bson/src/main/org/bson/codecs/ContainerCodecHelper.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
import org.bson.BsonType;
2121
import org.bson.Transformer;
2222
import org.bson.UuidRepresentation;
23+
import org.bson.codecs.configuration.CodecConfigurationException;
2324
import org.bson.codecs.configuration.CodecRegistry;
2425

26+
import java.lang.reflect.ParameterizedType;
27+
import java.lang.reflect.Type;
28+
import java.util.Arrays;
2529
import java.util.UUID;
2630

2731
/**
@@ -62,6 +66,23 @@ static Object readValue(final BsonReader reader, final DecoderContext decoderCon
6266
}
6367
}
6468

69+
static Codec<?> getCodec(final CodecRegistry codecRegistry, final Type type) {
70+
if (type instanceof Class) {
71+
return codecRegistry.get((Class<?>) type);
72+
} else if (type instanceof ParameterizedType) {
73+
ParameterizedType parameterizedType = (ParameterizedType) type;
74+
Codec<?> rawCodec = codecRegistry.get((Class<?>) parameterizedType.getRawType());
75+
if (rawCodec instanceof Parameterizable) {
76+
return ((Parameterizable) rawCodec).parameterize(codecRegistry, Arrays.asList(parameterizedType.getActualTypeArguments()));
77+
} else {
78+
return rawCodec;
79+
}
80+
} else {
81+
throw new CodecConfigurationException("Unsupported generic type of container: " + type);
82+
}
83+
}
84+
85+
6586
private ContainerCodecHelper() {
6687
}
6788
}

bson/src/main/org/bson/codecs/IterableCodec.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
import org.bson.BsonWriter;
2222
import org.bson.Transformer;
2323
import org.bson.UuidRepresentation;
24+
import org.bson.codecs.configuration.CodecConfigurationException;
2425
import org.bson.codecs.configuration.CodecRegistry;
2526

27+
import java.lang.reflect.Type;
2628
import java.util.ArrayList;
2729
import java.util.List;
2830

2931
import static org.bson.assertions.Assertions.notNull;
32+
import static org.bson.codecs.ContainerCodecHelper.getCodec;
3033
import static org.bson.codecs.ContainerCodecHelper.readValue;
3134

3235
/**
@@ -35,7 +38,7 @@
3538
* @since 3.3
3639
*/
3740
@SuppressWarnings("rawtypes")
38-
public class IterableCodec implements Codec<Iterable>, OverridableUuidRepresentationCodec<Iterable> {
41+
public class IterableCodec implements Codec<Iterable>, OverridableUuidRepresentationCodec<Iterable>, Parameterizable {
3942

4043
private final CodecRegistry registry;
4144
private final BsonTypeCodecMap bsonTypeCodecMap;
@@ -63,7 +66,6 @@ public IterableCodec(final CodecRegistry registry, final BsonTypeClassMap bsonTy
6366
this(registry, new BsonTypeCodecMap(notNull("bsonTypeClassMap", bsonTypeClassMap), registry), valueTransformer,
6467
UuidRepresentation.UNSPECIFIED);
6568
}
66-
6769
private IterableCodec(final CodecRegistry registry, final BsonTypeCodecMap bsonTypeCodecMap, final Transformer valueTransformer,
6870
final UuidRepresentation uuidRepresentation) {
6971
this.registry = notNull("registry", registry);
@@ -78,6 +80,15 @@ public Object transform(final Object objectToTransform) {
7880
}
7981

8082

83+
@Override
84+
public Codec<?> parameterize(final CodecRegistry codecRegistry, final List<Type> types) {
85+
if (types.size() != 1) {
86+
throw new CodecConfigurationException("Expected only one parameterized type for an Iterable, but found " + types.size());
87+
}
88+
89+
return new ParameterizedIterableCodec<>(getCodec(codecRegistry, types.get(0)));
90+
}
91+
8192
@Override
8293
public Codec<Iterable> withUuidRepresentation(final UuidRepresentation uuidRepresentation) {
8394
if (this.uuidRepresentation.equals(uuidRepresentation)) {

0 commit comments

Comments
 (0)