Skip to content

Support recursive types in parameterized records #1007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,11 @@ Object getValue(final Record record) throws InvocationTargetException, IllegalAc
@SuppressWarnings("deprecation")
private static Codec<?> computeCodec(final List<Type> typeParameters, final RecordComponent component,
final CodecRegistry codecRegistry) {
var codec = codecRegistry.get(toWrapper(resolveComponentType(typeParameters, component)));
if (codec instanceof Parameterizable parameterizableCodec
&& component.getGenericType() instanceof ParameterizedType parameterizedType) {
codec = parameterizableCodec.parameterize(codecRegistry,
resolveActualTypeArguments(typeParameters, component.getDeclaringRecord(), parameterizedType));
}
var rawType = toWrapper(resolveComponentType(typeParameters, component));
var codec = component.getGenericType() instanceof ParameterizedType parameterizedType
? codecRegistry.get(rawType,
resolveActualTypeArguments(typeParameters, component.getDeclaringRecord(), parameterizedType))
: codecRegistry.get(rawType);
BsonType bsonRepresentationType = null;

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

RecordCodec(final Class<T> clazz, final CodecRegistry codecRegistry, final List<Type> types) {
if (types.size() != clazz.getTypeParameters().length) {
if (types.size() != clazz.getTypeParameters().length || types.isEmpty()) {
throw new CodecConfigurationException("Unexpected number of type parameters for record class " + clazz);
}
this.clazz = notNull("class", clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import org.bson.codecs.record.samples.TestRecordWithNestedParameterizedRecord;
import org.bson.codecs.record.samples.TestRecordWithParameterizedRecord;
import org.bson.codecs.record.samples.TestRecordWithPojoAnnotations;
import org.bson.codecs.record.samples.TestSelfReferentialHolderRecord;
import org.bson.codecs.record.samples.TestSelfReferentialRecord;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -341,6 +343,36 @@ public void testRecordWithExtraData() {
assertEquals(testRecord, decoded);
}

@Test
public void testSelfReferentialRecords() {
var registry = fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY);
var codec = registry.get(TestSelfReferentialHolderRecord.class);
var testRecord = new TestSelfReferentialHolderRecord("0",
new TestSelfReferentialRecord<>("1",
new TestSelfReferentialRecord<>("2", null, null),
new TestSelfReferentialRecord<>("3", null, null)));

var document = new BsonDocument();

// when
codec.encode(new BsonDocumentWriter(document), testRecord, EncoderContext.builder().build());

// then
assertEquals(
new BsonDocument("_id", new BsonString("0"))
.append("selfReferentialRecord",
new BsonDocument("name", new BsonString("1"))
.append("left", new BsonDocument("name", new BsonString("2")))
.append("right", new BsonDocument("name", new BsonString("3")))),
document);

// when
var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build());

// then
assertEquals(testRecord, decoded);
}

@Test
public void testExceptionsForAnnotationsNotOnRecordComponent() {
assertThrows(CodecConfigurationException.class, () ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.codecs.record.samples;

import org.bson.codecs.pojo.annotations.BsonId;

public record TestSelfReferentialHolderRecord(@BsonId String id,
TestSelfReferentialRecord<String> selfReferentialRecord) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.codecs.record.samples;

import com.mongodb.lang.Nullable;

public record TestSelfReferentialRecord<T>(T name,
@Nullable TestSelfReferentialRecord<T> left,
@Nullable TestSelfReferentialRecord<T> right) {
}
34 changes: 34 additions & 0 deletions bson/src/main/org/bson/assertions/Assertions.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package org.bson.assertions;

import javax.annotation.Nullable;

/**
* <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>
*/
Expand Down Expand Up @@ -82,6 +84,38 @@ public static <T> T isTrueArgument(final String name, final T value, final boole
return value;
}

/**
* @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail()}.
* This may be helpful in non-{@code void} methods.
* @throws AssertionError Always
*/
public static AssertionError fail() throws AssertionError {
throw new AssertionError();
}

/**
* @param msg The failure message.
* @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail("failure message")}.
* This may be helpful in non-{@code void} methods.
* @throws AssertionError Always
*/
public static AssertionError fail(final String msg) throws AssertionError {
throw new AssertionError(assertNotNull(msg));
}

/**
* @param value A value to check.
* @param <T> The type of {@code value}.
* @return {@code value}
* @throws AssertionError If {@code value} is {@code null}.
*/
public static <T> T assertNotNull(@Nullable final T value) throws AssertionError {
if (value == null) {
throw new AssertionError();
}
return value;
}

/**
* Cast an object to the given class and return it, or throw IllegalArgumentException if it's not assignable to that class.
*
Expand Down
7 changes: 1 addition & 6 deletions bson/src/main/org/bson/codecs/ContainerCodecHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,7 @@ static Codec<?> getCodec(final CodecRegistry codecRegistry, final Type type) {
return codecRegistry.get((Class<?>) type);
} else if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Codec<?> rawCodec = codecRegistry.get((Class<?>) parameterizedType.getRawType());
if (rawCodec instanceof Parameterizable) {
return ((Parameterizable) rawCodec).parameterize(codecRegistry, Arrays.asList(parameterizedType.getActualTypeArguments()));
} else {
return rawCodec;
}
return codecRegistry.get((Class<?>) parameterizedType.getRawType(), Arrays.asList(parameterizedType.getActualTypeArguments()));
} else {
throw new CodecConfigurationException("Unsupported generic type of container: " + type);
}
Expand Down
6 changes: 5 additions & 1 deletion bson/src/main/org/bson/codecs/Parameterizable.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.bson.codecs.configuration.CodecRegistry;

import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;

/**
Expand All @@ -31,7 +32,10 @@ public interface Parameterizable {
* Recursively parameterize the codec with the given registry and generic type arguments.
*
* @param codecRegistry the code registry to use to resolve codecs for the generic type arguments
* @param types the types that are parameterizing the containing type.
* @param types the types that are parameterizing the containing type. The size of the list should be equal to the number of type
* parameters of the class whose codec is being parameterized, e.g. for a {@link Collection} the size of the list
* would be one since {@code Collection} has a single type parameter. Additionally, the size will never be 0
* since there is no purpose in parameterizing a codec for a type that has no type parameters.
* @return the Codec parameterized with the given types
*/
Codec<?> parameterize(CodecRegistry codecRegistry, List<Type> types);
Expand Down
30 changes: 30 additions & 0 deletions bson/src/main/org/bson/codecs/configuration/CodecRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

package org.bson.codecs.configuration;

import org.bson.assertions.Assertions;
import org.bson.codecs.Codec;

import java.lang.reflect.Type;
import java.util.List;

/**
* A registry of Codec instances searchable by the class that the Codec can encode and decode.
*
Expand All @@ -28,7 +32,11 @@
*
* <p>As of the 4.0 release, this class extends the {@code CodecProvider} interface. This capability was introduced to enable nesting
* registries inside another registry.</p>
*
* <p>Applications are encouraged to NOT implement this interface, but rather use the factory methods in {@link CodecRegistries}.</p>
*
* @since 3.0
* @see CodecRegistries
*/
public interface CodecRegistry extends CodecProvider {
/**
Expand All @@ -40,4 +48,26 @@ public interface CodecRegistry extends CodecProvider {
* @throws CodecConfigurationException if the registry does not contain a codec for the given class.
*/
<T> Codec<T> get(Class<T> clazz);

/**
* Gets a Codec for the given parameterized class, after resolving any type variables with the given type arguments.
*
* <p>
* The default behavior is to throw a {@link AssertionError}, as it's expected that {@code CodecRegistry} implementations are always
* provided by this library and will override the method appropriately.
* </p>
*
* @param clazz the parameterized class
* @param typeArguments the type arguments to apply to the parameterized class. This list may be empty but not null.
* @param <T> the class type
* @return a codec for the given class, with the given type parameters resolved
* @throws CodecConfigurationException if no codec can be found for the given class and type arguments.
* @throws AssertionError by default, if the implementation does not override this method, or if no codec can be found
* for the given class and type arguments.
* @see org.bson.codecs.Parameterizable
* @since 4.8
*/
default <T> Codec<T> get(Class<T> clazz, List<Type> typeArguments) {
throw Assertions.fail("This method should have been overridden but was not.");
}
}
38 changes: 33 additions & 5 deletions bson/src/main/org/bson/internal/ChildCodecRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,64 @@
import org.bson.codecs.Codec;
import org.bson.codecs.configuration.CodecRegistry;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;

import static java.lang.String.format;
import static org.bson.assertions.Assertions.isTrueArgument;
import static org.bson.assertions.Assertions.notNull;

// An implementation of CodecRegistry that is used to detect cyclic dependencies between Codecs
class ChildCodecRegistry<T> implements CodecRegistry {

private final ChildCodecRegistry<?> parent;
private final CycleDetectingCodecRegistry registry;
private final Class<T> codecClass;
private final List<Type> types;

ChildCodecRegistry(final CycleDetectingCodecRegistry registry, final Class<T> codecClass) {
ChildCodecRegistry(final CycleDetectingCodecRegistry registry, final Class<T> codecClass, final List<Type> types) {
this.codecClass = codecClass;
this.parent = null;
this.registry = registry;
this.types = types;
}


private ChildCodecRegistry(final ChildCodecRegistry<?> parent, final Class<T> codecClass) {
private ChildCodecRegistry(final ChildCodecRegistry<?> parent, final Class<T> codecClass, final List<Type> types) {
this.parent = parent;
this.codecClass = codecClass;
this.registry = parent.registry;
this.types = types;
}

public Class<T> getCodecClass() {
return codecClass;
}

public Optional<List<Type>> getTypes() {
return Optional.ofNullable(types);
}

// Gets a Codec, but if it detects a cyclic dependency, return a LazyCodec which breaks the chain.
public <U> Codec<U> get(final Class<U> clazz) {
if (hasCycles(clazz)) {
return new LazyCodec<U>(registry, clazz);
return new LazyCodec<>(registry, clazz, null);
} else {
return registry.get(new ChildCodecRegistry<>(this, clazz, null));
}
}

@Override
public <U> Codec<U> get(final Class<U> clazz, final List<Type> typeArguments) {
notNull("typeArguments", typeArguments);
isTrueArgument("typeArguments is not empty", !typeArguments.isEmpty());
isTrueArgument(format("typeArguments size should equal the number of type parameters in class %s, but is %d",
clazz, typeArguments.size()),
clazz.getTypeParameters().length == typeArguments.size());
if (hasCycles(clazz)) {
return new LazyCodec<U>(registry, clazz, typeArguments);
} else {
return registry.get(new ChildCodecRegistry<U>(this, clazz));
return registry.get(new ChildCodecRegistry<>(this, clazz, typeArguments));
}
}

Expand Down
Loading