Skip to content

Commit 132041e

Browse files
committed
Support recursive types in parameterized records
Provide codec support for records whose type declarations contain cycles. JAVA-4745
1 parent ba39a2c commit 132041e

File tree

12 files changed

+257
-40
lines changed

12 files changed

+257
-40
lines changed

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

Lines changed: 5 additions & 6 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)) {

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

Lines changed: 34 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,38 @@ 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 identifier = new ObjectId();
351+
var testRecord = new TestSelfReferentialHolderRecord("0",
352+
new TestSelfReferentialRecord<>("1",
353+
new TestSelfReferentialRecord<>("2", null, null),
354+
new TestSelfReferentialRecord<>("3", null, null)));
355+
356+
var document = new BsonDocument();
357+
var writer = new BsonDocumentWriter(document);
358+
359+
// when
360+
codec.encode(writer, testRecord, EncoderContext.builder().build());
361+
362+
// then
363+
assertEquals(
364+
new BsonDocument("_id", new BsonString("0"))
365+
.append("selfReferentialRecord",
366+
new BsonDocument("name", new BsonString("1"))
367+
.append("left", new BsonDocument("name", new BsonString("2")))
368+
.append("right", new BsonDocument("name", new BsonString("3")))),
369+
document);
370+
371+
// when
372+
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());
373+
374+
// then
375+
assertEquals(testRecord, decoded);
376+
}
377+
344378
@Test
345379
public void testExceptionsForAnnotationsNotOnRecordComponent() {
346380
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/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/configuration/CodecRegistry.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
import org.bson.codecs.Codec;
2020

21+
import java.lang.reflect.Type;
22+
import java.util.List;
23+
2124
/**
2225
* A registry of Codec instances searchable by the class that the Codec can encode and decode.
2326
*
@@ -28,7 +31,11 @@
2831
*
2932
* <p>As of the 4.0 release, this class extends the {@code CodecProvider} interface. This capability was introduced to enable nesting
3033
* registries inside another registry.</p>
34+
*
35+
* <p>Applications are encouraged to NOT implement this interface, but rather use the factory methods in {@link CodecRegistries}.</p>
36+
*
3137
* @since 3.0
38+
* @see CodecRegistries
3239
*/
3340
public interface CodecRegistry extends CodecProvider {
3441
/**
@@ -40,4 +47,24 @@ public interface CodecRegistry extends CodecProvider {
4047
* @throws CodecConfigurationException if the registry does not contain a codec for the given class.
4148
*/
4249
<T> Codec<T> get(Class<T> clazz);
50+
51+
/**
52+
* Gets a Codec for the given parameterized class, after resolving any type variables with the given type arguments.
53+
*
54+
* <p>
55+
* The default behavior is to throw a {@link CodecConfigurationException}. Applications are encouraged to either override this
56+
* method in their own implementations of this interface, or else use the factory methods in {@link CodecRegistries}.
57+
* </p>
58+
*
59+
* @param clazz the parameterized class
60+
* @param typeArguments the type arguments to apply to the parameterized class
61+
* @return a codec for the given class, with the given type parameters resolved
62+
* @throws CodecConfigurationException by default, if the implementation does not override this method, or if no codec can be found
63+
* for the given class and type arguments.
64+
* @param <T> the class type
65+
* @since 4.8
66+
*/
67+
default <T> Codec<T> get(Class<T> clazz, List<Type> typeArguments) {
68+
throw new CodecConfigurationException("Make this message really informative"); // TODO
69+
}
4370
}

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

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

23+
import javax.annotation.Nullable;
24+
import java.lang.reflect.Type;
25+
import java.util.List;
26+
2327
// An implementation of CodecRegistry that is used to detect cyclic dependencies between Codecs
2428
class ChildCodecRegistry<T> implements CodecRegistry {
2529

2630
private final ChildCodecRegistry<?> parent;
2731
private final CycleDetectingCodecRegistry registry;
2832
private final Class<T> codecClass;
33+
private final List<Type> types;
2934

3035
ChildCodecRegistry(final CycleDetectingCodecRegistry registry, final Class<T> codecClass) {
36+
this(registry, codecClass, null);
37+
}
38+
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, @Nullable 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 List<Type> getTypes() {
58+
return 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<U>(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+
if (hasCycles(clazz)) {
73+
return new LazyCodec<U>(registry, clazz, typeArguments);
5174
} else {
52-
return registry.get(new ChildCodecRegistry<U>(this, clazz));
75+
return registry.get(new ChildCodecRegistry<>(this, clazz, typeArguments));
5376
}
5477
}
5578

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

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,75 @@
1919
import org.bson.codecs.Codec;
2020
import org.bson.codecs.configuration.CodecConfigurationException;
2121

22+
import java.lang.reflect.Type;
23+
import java.util.List;
24+
import java.util.Objects;
2225
import java.util.Optional;
2326
import java.util.concurrent.ConcurrentHashMap;
2427
import java.util.concurrent.ConcurrentMap;
2528

2629
import static java.lang.String.format;
2730

2831
final class CodecCache {
29-
private final ConcurrentMap<Class<?>, Optional<Codec<?>>> codecCache = new ConcurrentHashMap<>();
3032

31-
public boolean containsKey(final Class<?> clazz) {
32-
return codecCache.containsKey(clazz);
33+
static final class CodecCacheKey {
34+
private final Class<?> clazz;
35+
private final List<Type> types;
36+
37+
CodecCacheKey(final Class<?> clazz, final List<Type> types) {
38+
this.clazz = clazz;
39+
this.types = types;
40+
}
41+
42+
@Override
43+
public boolean equals(final Object o) {
44+
if (this == o) {
45+
return true;
46+
}
47+
if (o == null || getClass() != o.getClass()) {
48+
return false;
49+
}
50+
CodecCacheKey that = (CodecCacheKey) o;
51+
return clazz.equals(that.clazz) && Objects.equals(types, that.types);
52+
}
53+
54+
@Override
55+
public int hashCode() {
56+
return Objects.hash(clazz, types);
57+
}
58+
59+
@Override
60+
public String toString() {
61+
return "CodecCacheKey{"
62+
+ "clazz=" + clazz
63+
+ ", types=" + types
64+
+ '}';
65+
}
66+
}
67+
68+
private final ConcurrentMap<CodecCacheKey, Optional<Codec<?>>> codecCache = new ConcurrentHashMap<>();
69+
70+
public boolean containsKey(final CodecCacheKey codecCacheKey) {
71+
return codecCache.containsKey(codecCacheKey);
3372
}
3473

35-
public void put(final Class<?> clazz, final Codec<?> codec){
36-
codecCache.put(clazz, Optional.ofNullable(codec));
74+
public void put(final CodecCacheKey codecCacheKey, final Codec<?> codec){
75+
codecCache.put(codecCacheKey, Optional.ofNullable(codec));
3776
}
3877

3978
@SuppressWarnings("unchecked")
40-
public synchronized <T> Codec<T> putIfMissing(final Class<T> clazz, final Codec<T> codec) {
41-
Optional<Codec<?>> cachedCodec = codecCache.computeIfAbsent(clazz, clz -> Optional.of(codec));
79+
public synchronized <T> Codec<T> putIfMissing(final CodecCacheKey codecCacheKey, final Codec<T> codec) {
80+
Optional<Codec<?>> cachedCodec = codecCache.computeIfAbsent(codecCacheKey, clz -> Optional.of(codec));
4281
if (cachedCodec.isPresent()) {
4382
return (Codec<T>) cachedCodec.get();
4483
}
45-
codecCache.put(clazz, Optional.of(codec));
84+
codecCache.put(codecCacheKey, Optional.of(codec));
4685
return codec;
4786
}
4887

4988
@SuppressWarnings("unchecked")
50-
public <T> Codec<T> getOrThrow(final Class<T> clazz) {
51-
return (Codec<T>) codecCache.getOrDefault(clazz, Optional.empty()).orElseThrow(
52-
() -> new CodecConfigurationException(format("Can't find a codec for %s.", clazz)));
89+
public <T> Codec<T> getOrThrow(final CodecCacheKey codecCacheKey) {
90+
return (Codec<T>) codecCache.getOrDefault(codecCacheKey, Optional.empty()).orElseThrow(
91+
() -> new CodecConfigurationException(format("Can't find a codec for %s.", codecCacheKey)));
5392
}
5493
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,19 @@
2323
import org.bson.codecs.EncoderContext;
2424
import org.bson.codecs.configuration.CodecRegistry;
2525

26+
import java.lang.reflect.Type;
27+
import java.util.List;
28+
2629
class LazyCodec<T> implements Codec<T> {
2730
private final CodecRegistry registry;
2831
private final Class<T> clazz;
32+
private final List<Type> types;
2933
private volatile Codec<T> wrapped;
3034

31-
LazyCodec(final CodecRegistry registry, final Class<T> clazz) {
35+
LazyCodec(final CodecRegistry registry, final Class<T> clazz, final List<Type> types) {
3236
this.registry = registry;
3337
this.clazz = clazz;
38+
this.types = types;
3439
}
3540

3641
@Override
@@ -50,7 +55,11 @@ public T decode(final BsonReader reader, final DecoderContext decoderContext) {
5055

5156
private Codec<T> getWrapped() {
5257
if (wrapped == null) {
53-
wrapped = registry.get(clazz);
58+
if (types == null) {
59+
wrapped = registry.get(clazz);
60+
} else {
61+
wrapped = registry.get(clazz, types);
62+
}
5463
}
5564

5665
return wrapped;

0 commit comments

Comments
 (0)