Skip to content

Commit ba39a2c

Browse files
authored
Support encoding/decoding of parameterized records (#1005)
JAVA-4740
1 parent aa5ad82 commit ba39a2c

File tree

6 files changed

+244
-9
lines changed

6 files changed

+244
-9
lines changed

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

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import java.lang.reflect.InvocationTargetException;
3939
import java.lang.reflect.ParameterizedType;
4040
import java.lang.reflect.RecordComponent;
41+
import java.lang.reflect.Type;
42+
import java.lang.reflect.TypeVariable;
4143
import java.util.ArrayList;
4244
import java.util.Arrays;
4345
import java.util.List;
@@ -48,9 +50,10 @@
4850
import static java.lang.String.format;
4951
import static org.bson.assertions.Assertions.notNull;
5052

51-
final class RecordCodec<T extends Record> implements Codec<T> {
53+
final class RecordCodec<T extends Record> implements Codec<T>, Parameterizable {
5254
private static final Logger LOGGER = Loggers.getLogger("RecordCodec");
5355
private final Class<T> clazz;
56+
private final boolean requiresParameterization;
5457
private final Constructor<?> canonicalConstructor;
5558
private final List<ComponentModel> componentModels;
5659
private final ComponentModel componentModelForId;
@@ -62,10 +65,11 @@ private static final class ComponentModel {
6265
private final int index;
6366
private final String fieldName;
6467

65-
private ComponentModel(final RecordComponent component, final CodecRegistry codecRegistry, final int index) {
68+
private ComponentModel(final List<Type> typeParameters, final RecordComponent component, final CodecRegistry codecRegistry,
69+
final int index) {
6670
validateAnnotations(component, index);
6771
this.component = component;
68-
this.codec = computeCodec(component, codecRegistry);
72+
this.codec = computeCodec(typeParameters, component, codecRegistry);
6973
this.index = index;
7074
this.fieldName = computeFieldName(component);
7175
}
@@ -83,11 +87,13 @@ Object getValue(final Record record) throws InvocationTargetException, IllegalAc
8387
}
8488

8589
@SuppressWarnings("deprecation")
86-
private static Codec<?> computeCodec(final RecordComponent component, final CodecRegistry codecRegistry) {
87-
var codec = codecRegistry.get(toWrapper(component.getType()));
90+
private static Codec<?> computeCodec(final List<Type> typeParameters, final RecordComponent component,
91+
final CodecRegistry codecRegistry) {
92+
var codec = codecRegistry.get(toWrapper(resolveComponentType(typeParameters, component)));
8893
if (codec instanceof Parameterizable parameterizableCodec
8994
&& component.getGenericType() instanceof ParameterizedType parameterizedType) {
90-
codec = parameterizableCodec.parameterize(codecRegistry, Arrays.asList(parameterizedType.getActualTypeArguments()));
95+
codec = parameterizableCodec.parameterize(codecRegistry,
96+
resolveActualTypeArguments(typeParameters, component.getDeclaringRecord(), parameterizedType));
9197
}
9298
BsonType bsonRepresentationType = null;
9399

@@ -109,6 +115,36 @@ private static Codec<?> computeCodec(final RecordComponent component, final Code
109115
return codec;
110116
}
111117

118+
private static Class<?> resolveComponentType(final List<Type> typeParameters, final RecordComponent component) {
119+
Type resolvedType = resolveType(component.getGenericType(), typeParameters, component.getDeclaringRecord());
120+
return resolvedType instanceof Class<?> clazz ? clazz : component.getType();
121+
}
122+
123+
private static List<Type> resolveActualTypeArguments(final List<Type> typeParameters, final Class<?> recordClass,
124+
final ParameterizedType parameterizedType) {
125+
return Arrays.stream(parameterizedType.getActualTypeArguments())
126+
.map(type -> resolveType(type, typeParameters, recordClass))
127+
.toList();
128+
}
129+
130+
private static Type resolveType(final Type type, final List<Type> typeParameters, final Class<?> recordClass) {
131+
return type instanceof TypeVariable<?> typeVariable
132+
? typeParameters.get(getIndexOfTypeParameter(typeVariable.getName(), recordClass))
133+
: type;
134+
}
135+
136+
// Get
137+
private static int getIndexOfTypeParameter(final String typeParameterName, final Class<?> recordClass) {
138+
var typeParameters = recordClass.getTypeParameters();
139+
for (int i = 0; i < typeParameters.length; i++) {
140+
if (typeParameters[i].getName().equals(typeParameterName)) {
141+
return i;
142+
}
143+
}
144+
throw new CodecConfigurationException(String.format("Could not find type parameter on record %s with name %s",
145+
recordClass.getName(), typeParameterName));
146+
}
147+
112148
@SuppressWarnings("deprecation")
113149
private static String computeFieldName(final RecordComponent component) {
114150
if (component.isAnnotationPresent(BsonId.class)) {
@@ -218,16 +254,47 @@ private static <T extends Annotation> void validateAnnotationOnlyOnField(final R
218254

219255
RecordCodec(final Class<T> clazz, final CodecRegistry codecRegistry) {
220256
this.clazz = notNull("class", clazz);
257+
if (clazz.getTypeParameters().length > 0) {
258+
requiresParameterization = true;
259+
canonicalConstructor = null;
260+
componentModels = null;
261+
fieldNameToComponentModel = null;
262+
componentModelForId = null;
263+
} else {
264+
requiresParameterization = false;
265+
canonicalConstructor = notNull("canonicalConstructor", getCanonicalConstructor(clazz));
266+
componentModels = getComponentModels(clazz, codecRegistry, List.of());
267+
fieldNameToComponentModel = componentModels.stream()
268+
.collect(Collectors.toMap(ComponentModel::getFieldName, Function.identity()));
269+
componentModelForId = getComponentModelForId(clazz, componentModels);
270+
}
271+
}
272+
273+
RecordCodec(final Class<T> clazz, final CodecRegistry codecRegistry, final List<Type> types) {
274+
if (types.size() != clazz.getTypeParameters().length) {
275+
throw new CodecConfigurationException("Unexpected number of type parameters for record class " + clazz);
276+
}
277+
this.clazz = notNull("class", clazz);
278+
requiresParameterization = false;
221279
canonicalConstructor = notNull("canonicalConstructor", getCanonicalConstructor(clazz));
222-
componentModels = getComponentModels(clazz, codecRegistry);
280+
componentModels = getComponentModels(clazz, codecRegistry, types);
223281
fieldNameToComponentModel = componentModels.stream()
224282
.collect(Collectors.toMap(ComponentModel::getFieldName, Function.identity()));
225283
componentModelForId = getComponentModelForId(clazz, componentModels);
226284
}
227285

286+
@Override
287+
public Codec<?> parameterize(final CodecRegistry codecRegistry, final List<Type> types) {
288+
return new RecordCodec<>(clazz, codecRegistry, types);
289+
}
290+
228291
@SuppressWarnings("unchecked")
229292
@Override
230293
public T decode(final BsonReader reader, final DecoderContext decoderContext) {
294+
if (requiresParameterization) {
295+
throw new CodecConfigurationException("Can not decode to a record with type parameters that has not been parameterized");
296+
}
297+
231298
reader.readStartDocument();
232299

233300
Object[] constructorArguments = new Object[componentModels.size()];
@@ -254,6 +321,10 @@ public T decode(final BsonReader reader, final DecoderContext decoderContext) {
254321

255322
@Override
256323
public void encode(final BsonWriter writer, final T record, final EncoderContext encoderContext) {
324+
if (requiresParameterization) {
325+
throw new CodecConfigurationException("Can not decode to a record with type parameters that has not been parameterized");
326+
}
327+
257328
writer.writeStartDocument();
258329
if (componentModelForId != null) {
259330
writeComponent(writer, record, componentModelForId);
@@ -287,11 +358,12 @@ private void writeComponent(final BsonWriter writer, final T record, final Compo
287358
}
288359
}
289360

290-
private static <T> List<ComponentModel> getComponentModels(final Class<T> clazz, final CodecRegistry codecRegistry) {
361+
private static <T> List<ComponentModel> getComponentModels(final Class<T> clazz, final CodecRegistry codecRegistry,
362+
final List<Type> typeParameters) {
291363
var recordComponents = clazz.getRecordComponents();
292364
var componentModels = new ArrayList<ComponentModel>(recordComponents.length);
293365
for (int i = 0; i < recordComponents.length; i++) {
294-
componentModels.add(new ComponentModel(recordComponents[i], codecRegistry, i));
366+
componentModels.add(new ComponentModel(typeParameters, recordComponents[i], codecRegistry, i));
295367
}
296368
return componentModels;
297369
}

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
import org.bson.BsonDocument;
2121
import org.bson.BsonDocumentReader;
2222
import org.bson.BsonDocumentWriter;
23+
import org.bson.BsonDouble;
2324
import org.bson.BsonInt32;
2425
import org.bson.BsonObjectId;
2526
import org.bson.BsonString;
2627
import org.bson.codecs.DecoderContext;
2728
import org.bson.codecs.EncoderContext;
2829
import org.bson.codecs.configuration.CodecConfigurationException;
2930
import org.bson.codecs.record.samples.TestRecordEmbedded;
31+
import org.bson.codecs.record.samples.TestRecordParameterized;
3032
import org.bson.codecs.record.samples.TestRecordWithDeprecatedAnnotations;
3133
import org.bson.codecs.record.samples.TestRecordWithIllegalBsonCreatorOnConstructor;
3234
import org.bson.codecs.record.samples.TestRecordWithIllegalBsonCreatorOnMethod;
@@ -44,6 +46,9 @@
4446
import org.bson.codecs.record.samples.TestRecordWithListOfRecords;
4547
import org.bson.codecs.record.samples.TestRecordWithMapOfListOfRecords;
4648
import org.bson.codecs.record.samples.TestRecordWithMapOfRecords;
49+
import org.bson.codecs.record.samples.TestRecordWithNestedParameterized;
50+
import org.bson.codecs.record.samples.TestRecordWithNestedParameterizedRecord;
51+
import org.bson.codecs.record.samples.TestRecordWithParameterizedRecord;
4752
import org.bson.codecs.record.samples.TestRecordWithPojoAnnotations;
4853
import org.bson.conversions.Bson;
4954
import org.bson.types.ObjectId;
@@ -227,6 +232,71 @@ public void testRecordWithNestedMapOfListRecords() {
227232
assertEquals(testRecord, decoded);
228233
}
229234

235+
@Test
236+
public void testRecordWithNestedParameterizedRecord() {
237+
var codec = new RecordCodec<>(TestRecordWithParameterizedRecord.class,
238+
fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY));
239+
var identifier = new ObjectId();
240+
var testRecord = new TestRecordWithParameterizedRecord(identifier,
241+
new TestRecordParameterized<>(42.0, List.of(new TestRecordEmbedded("embedded"))));
242+
243+
var document = new BsonDocument();
244+
var writer = new BsonDocumentWriter(document);
245+
246+
// when
247+
codec.encode(writer, testRecord, EncoderContext.builder().build());
248+
249+
// then
250+
assertEquals(
251+
new BsonDocument("_id", new BsonObjectId(identifier))
252+
.append("parameterizedRecord",
253+
new BsonDocument("number", new BsonDouble(42.0))
254+
.append("parameterizedList",
255+
new BsonArray(List.of(new BsonDocument("name", new BsonString("embedded")))))),
256+
document);
257+
assertEquals("_id", document.getFirstKey());
258+
259+
// when
260+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
261+
262+
// then
263+
assertEquals(testRecord, decoded);
264+
}
265+
266+
@Test
267+
public void testRecordWithNestedParameterizedRecordWithDifferentlyOrderedTypeParameters() {
268+
var codec = new RecordCodec<>(TestRecordWithNestedParameterizedRecord.class,
269+
fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY));
270+
var identifier = new ObjectId();
271+
var testRecord = new TestRecordWithNestedParameterizedRecord(identifier,
272+
new TestRecordWithNestedParameterized<>(
273+
new TestRecordParameterized<>(42.0, List.of(new TestRecordEmbedded("p"))),
274+
"o"));
275+
276+
var document = new BsonDocument();
277+
var writer = new BsonDocumentWriter(document);
278+
279+
// when
280+
codec.encode(writer, testRecord, EncoderContext.builder().build());
281+
282+
// then
283+
assertEquals(
284+
new BsonDocument("_id", new BsonObjectId(identifier))
285+
.append("nestedParameterized",
286+
new BsonDocument("parameterizedRecord",
287+
new BsonDocument("number", new BsonDouble(42.0))
288+
.append("parameterizedList",
289+
new BsonArray(List.of(new BsonDocument("name", new BsonString("p"))))))
290+
.append("other", new BsonString("o"))),
291+
document);
292+
293+
// when
294+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
295+
296+
// then
297+
assertEquals(testRecord, decoded);
298+
}
299+
230300
@Test
231301
public void testRecordWithNulls() {
232302
var codec = new RecordCodec<>(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 java.util.List;
20+
21+
public record TestRecordParameterized<N extends Number, T>(N number, List<T> parameterizedList) {
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 TestRecordWithNestedParameterized<A, B, C extends Number>(
20+
TestRecordParameterized<C, A> parameterizedRecord,
21+
B other) {
22+
}
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+
public record TestRecordWithNestedParameterizedRecord(
23+
@BsonId ObjectId id,
24+
TestRecordWithNestedParameterized<TestRecordEmbedded, String, Double> nestedParameterized) {
25+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
public record TestRecordWithParameterizedRecord(@BsonId ObjectId id,
23+
TestRecordParameterized<Double, TestRecordEmbedded> parameterizedRecord) {
24+
}

0 commit comments

Comments
 (0)