Skip to content

Commit 7d73eee

Browse files
authored
Support recursive types in parameterized records (#1007)
Provide codec support for records whose type declarations contain cycles. JAVA-4745
1 parent fbc5e4e commit 7d73eee

File tree

15 files changed

+417
-41
lines changed

15 files changed

+417
-41
lines changed

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,11 @@ Object getValue(final Record record) throws InvocationTargetException, IllegalAc
8989
@SuppressWarnings("deprecation")
9090
private static Codec<?> computeCodec(final List<Type> typeParameters, final RecordComponent component,
9191
final CodecRegistry codecRegistry) {
92-
var codec = codecRegistry.get(toWrapper(resolveComponentType(typeParameters, component)));
93-
if (codec instanceof Parameterizable parameterizableCodec
94-
&& component.getGenericType() instanceof ParameterizedType parameterizedType) {
95-
codec = parameterizableCodec.parameterize(codecRegistry,
96-
resolveActualTypeArguments(typeParameters, component.getDeclaringRecord(), parameterizedType));
97-
}
92+
var rawType = toWrapper(resolveComponentType(typeParameters, component));
93+
var codec = component.getGenericType() instanceof ParameterizedType parameterizedType
94+
? codecRegistry.get(rawType,
95+
resolveActualTypeArguments(typeParameters, component.getDeclaringRecord(), parameterizedType))
96+
: codecRegistry.get(rawType);
9897
BsonType bsonRepresentationType = null;
9998

10099
if (component.isAnnotationPresent(BsonRepresentation.class)) {
@@ -271,7 +270,7 @@ private static <T extends Annotation> void validateAnnotationOnlyOnField(final R
271270
}
272271

273272
RecordCodec(final Class<T> clazz, final CodecRegistry codecRegistry, final List<Type> types) {
274-
if (types.size() != clazz.getTypeParameters().length) {
273+
if (types.size() != clazz.getTypeParameters().length || types.isEmpty()) {
275274
throw new CodecConfigurationException("Unexpected number of type parameters for record class " + clazz);
276275
}
277276
this.clazz = notNull("class", clazz);

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import org.bson.codecs.record.samples.TestRecordWithNestedParameterizedRecord;
5151
import org.bson.codecs.record.samples.TestRecordWithParameterizedRecord;
5252
import org.bson.codecs.record.samples.TestRecordWithPojoAnnotations;
53+
import org.bson.codecs.record.samples.TestSelfReferentialHolderRecord;
54+
import org.bson.codecs.record.samples.TestSelfReferentialRecord;
5355
import org.bson.conversions.Bson;
5456
import org.bson.types.ObjectId;
5557
import org.junit.jupiter.api.Test;
@@ -341,6 +343,36 @@ public void testRecordWithExtraData() {
341343
assertEquals(testRecord, decoded);
342344
}
343345

346+
@Test
347+
public void testSelfReferentialRecords() {
348+
var registry = fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY);
349+
var codec = registry.get(TestSelfReferentialHolderRecord.class);
350+
var testRecord = new TestSelfReferentialHolderRecord("0",
351+
new TestSelfReferentialRecord<>("1",
352+
new TestSelfReferentialRecord<>("2", null, null),
353+
new TestSelfReferentialRecord<>("3", null, null)));
354+
355+
var document = new BsonDocument();
356+
357+
// when
358+
codec.encode(new BsonDocumentWriter(document), testRecord, EncoderContext.builder().build());
359+
360+
// then
361+
assertEquals(
362+
new BsonDocument("_id", new BsonString("0"))
363+
.append("selfReferentialRecord",
364+
new BsonDocument("name", new BsonString("1"))
365+
.append("left", new BsonDocument("name", new BsonString("2")))
366+
.append("right", new BsonDocument("name", new BsonString("3")))),
367+
document);
368+
369+
// when
370+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
371+
372+
// then
373+
assertEquals(testRecord, decoded);
374+
}
375+
344376
@Test
345377
public void testExceptionsForAnnotationsNotOnRecordComponent() {
346378
assertThrows(CodecConfigurationException.class, () ->
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
21+
public record TestSelfReferentialHolderRecord(@BsonId String id,
22+
TestSelfReferentialRecord<String> selfReferentialRecord) {
23+
}
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 com.mongodb.lang.Nullable;
20+
21+
public record TestSelfReferentialRecord<T>(T name,
22+
@Nullable TestSelfReferentialRecord<T> left,
23+
@Nullable TestSelfReferentialRecord<T> right) {
24+
}

bson/src/main/org/bson/assertions/Assertions.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
package org.bson.assertions;
1919

20+
import javax.annotation.Nullable;
21+
2022
/**
2123
* <p>Design by contract assertions.</p> <p>This class is not part of the public API and may be removed or changed at any time.</p>
2224
*/
@@ -82,6 +84,38 @@ public static <T> T isTrueArgument(final String name, final T value, final boole
8284
return value;
8385
}
8486

87+
/**
88+
* @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail()}.
89+
* This may be helpful in non-{@code void} methods.
90+
* @throws AssertionError Always
91+
*/
92+
public static AssertionError fail() throws AssertionError {
93+
throw new AssertionError();
94+
}
95+
96+
/**
97+
* @param msg The failure message.
98+
* @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail("failure message")}.
99+
* This may be helpful in non-{@code void} methods.
100+
* @throws AssertionError Always
101+
*/
102+
public static AssertionError fail(final String msg) throws AssertionError {
103+
throw new AssertionError(assertNotNull(msg));
104+
}
105+
106+
/**
107+
* @param value A value to check.
108+
* @param <T> The type of {@code value}.
109+
* @return {@code value}
110+
* @throws AssertionError If {@code value} is {@code null}.
111+
*/
112+
public static <T> T assertNotNull(@Nullable final T value) throws AssertionError {
113+
if (value == null) {
114+
throw new AssertionError();
115+
}
116+
return value;
117+
}
118+
85119
/**
86120
* Cast an object to the given class and return it, or throw IllegalArgumentException if it's not assignable to that class.
87121
*

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,7 @@ static Codec<?> getCodec(final CodecRegistry codecRegistry, final Type type) {
7171
return codecRegistry.get((Class<?>) type);
7272
} else if (type instanceof ParameterizedType) {
7373
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-
}
74+
return codecRegistry.get((Class<?>) parameterizedType.getRawType(), Arrays.asList(parameterizedType.getActualTypeArguments()));
8075
} else {
8176
throw new CodecConfigurationException("Unsupported generic type of container: " + type);
8277
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.bson.codecs.configuration.CodecRegistry;
2020

2121
import java.lang.reflect.Type;
22+
import java.util.Collection;
2223
import java.util.List;
2324

2425
/**
@@ -31,7 +32,10 @@ public interface Parameterizable {
3132
* Recursively parameterize the codec with the given registry and generic type arguments.
3233
*
3334
* @param codecRegistry the code registry to use to resolve codecs for the generic type arguments
34-
* @param types the types that are parameterizing the containing type.
35+
* @param types the types that are parameterizing the containing type. The size of the list should be equal to the number of type
36+
* parameters of the class whose codec is being parameterized, e.g. for a {@link Collection} the size of the list
37+
* would be one since {@code Collection} has a single type parameter. Additionally, the size will never be 0
38+
* since there is no purpose in parameterizing a codec for a type that has no type parameters.
3539
* @return the Codec parameterized with the given types
3640
*/
3741
Codec<?> parameterize(CodecRegistry codecRegistry, List<Type> types);

bson/src/main/org/bson/codecs/configuration/CodecRegistry.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
package org.bson.codecs.configuration;
1818

19+
import org.bson.assertions.Assertions;
1920
import org.bson.codecs.Codec;
2021

22+
import java.lang.reflect.Type;
23+
import java.util.List;
24+
2125
/**
2226
* A registry of Codec instances searchable by the class that the Codec can encode and decode.
2327
*
@@ -28,7 +32,11 @@
2832
*
2933
* <p>As of the 4.0 release, this class extends the {@code CodecProvider} interface. This capability was introduced to enable nesting
3034
* registries inside another registry.</p>
35+
*
36+
* <p>Applications are encouraged to NOT implement this interface, but rather use the factory methods in {@link CodecRegistries}.</p>
37+
*
3138
* @since 3.0
39+
* @see CodecRegistries
3240
*/
3341
public interface CodecRegistry extends CodecProvider {
3442
/**
@@ -40,4 +48,26 @@ public interface CodecRegistry extends CodecProvider {
4048
* @throws CodecConfigurationException if the registry does not contain a codec for the given class.
4149
*/
4250
<T> Codec<T> get(Class<T> clazz);
51+
52+
/**
53+
* Gets a Codec for the given parameterized class, after resolving any type variables with the given type arguments.
54+
*
55+
* <p>
56+
* The default behavior is to throw a {@link AssertionError}, as it's expected that {@code CodecRegistry} implementations are always
57+
* provided by this library and will override the method appropriately.
58+
* </p>
59+
*
60+
* @param clazz the parameterized class
61+
* @param typeArguments the type arguments to apply to the parameterized class. This list may be empty but not null.
62+
* @param <T> the class type
63+
* @return a codec for the given class, with the given type parameters resolved
64+
* @throws CodecConfigurationException if no codec can be found for the given class and type arguments.
65+
* @throws AssertionError by default, if the implementation does not override this method, or if no codec can be found
66+
* for the given class and type arguments.
67+
* @see org.bson.codecs.Parameterizable
68+
* @since 4.8
69+
*/
70+
default <T> Codec<T> get(Class<T> clazz, List<Type> typeArguments) {
71+
throw Assertions.fail("This method should have been overridden but was not.");
72+
}
4373
}

bson/src/main/org/bson/internal/ChildCodecRegistry.java

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,64 @@
2020
import org.bson.codecs.Codec;
2121
import org.bson.codecs.configuration.CodecRegistry;
2222

23+
import java.lang.reflect.Type;
24+
import java.util.List;
25+
import java.util.Optional;
26+
27+
import static java.lang.String.format;
28+
import static org.bson.assertions.Assertions.isTrueArgument;
29+
import static org.bson.assertions.Assertions.notNull;
30+
2331
// An implementation of CodecRegistry that is used to detect cyclic dependencies between Codecs
2432
class ChildCodecRegistry<T> implements CodecRegistry {
2533

2634
private final ChildCodecRegistry<?> parent;
2735
private final CycleDetectingCodecRegistry registry;
2836
private final Class<T> codecClass;
37+
private final List<Type> types;
2938

30-
ChildCodecRegistry(final CycleDetectingCodecRegistry registry, final Class<T> codecClass) {
39+
ChildCodecRegistry(final CycleDetectingCodecRegistry registry, final Class<T> codecClass, final List<Type> types) {
3140
this.codecClass = codecClass;
3241
this.parent = null;
3342
this.registry = registry;
43+
this.types = types;
3444
}
3545

36-
37-
private ChildCodecRegistry(final ChildCodecRegistry<?> parent, final Class<T> codecClass) {
46+
private ChildCodecRegistry(final ChildCodecRegistry<?> parent, final Class<T> codecClass, final List<Type> types) {
3847
this.parent = parent;
3948
this.codecClass = codecClass;
4049
this.registry = parent.registry;
50+
this.types = types;
4151
}
4252

4353
public Class<T> getCodecClass() {
4454
return codecClass;
4555
}
4656

57+
public Optional<List<Type>> getTypes() {
58+
return Optional.ofNullable(types);
59+
}
60+
4761
// Gets a Codec, but if it detects a cyclic dependency, return a LazyCodec which breaks the chain.
4862
public <U> Codec<U> get(final Class<U> clazz) {
4963
if (hasCycles(clazz)) {
50-
return new LazyCodec<U>(registry, clazz);
64+
return new LazyCodec<>(registry, clazz, null);
65+
} else {
66+
return registry.get(new ChildCodecRegistry<>(this, clazz, null));
67+
}
68+
}
69+
70+
@Override
71+
public <U> Codec<U> get(final Class<U> clazz, final List<Type> typeArguments) {
72+
notNull("typeArguments", typeArguments);
73+
isTrueArgument("typeArguments is not empty", !typeArguments.isEmpty());
74+
isTrueArgument(format("typeArguments size should equal the number of type parameters in class %s, but is %d",
75+
clazz, typeArguments.size()),
76+
clazz.getTypeParameters().length == typeArguments.size());
77+
if (hasCycles(clazz)) {
78+
return new LazyCodec<U>(registry, clazz, typeArguments);
5179
} else {
52-
return registry.get(new ChildCodecRegistry<U>(this, clazz));
80+
return registry.get(new ChildCodecRegistry<>(this, clazz, typeArguments));
5381
}
5482
}
5583

0 commit comments

Comments
 (0)