Skip to content
20 changes: 16 additions & 4 deletions bson/src/main/org/bson/codecs/pojo/PojoBuilderHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,23 @@ private static <T> Set<ClassWithParentTypeData<? super T>> getClassHierarchy(fin
TypeData<?> parentClassTypeData = classTypeData;
while (currentClass != null && !currentClass.isEnum() && !currentClass.equals(Object.class)) {
classesToScan.add(new ClassWithParentTypeData<>(currentClass, parentClassTypeData));
parentClassTypeData = TypeData.newInstance(currentClass.getGenericSuperclass(), currentClass);
for (Class<?> interfaceClass : currentClass.getInterfaces()) {
classesToScan.addAll(getClassHierarchy((Class<? super T>) interfaceClass, parentClassTypeData));

List<TypeVariable<?>> currentTypeParams = asList(currentClass.getTypeParameters());
Type[] genericInterfaces = currentClass.getGenericInterfaces();
Class<?>[] interfaces = currentClass.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
TypeData<?> ifaceResolved = TypeData.newInstance(
genericInterfaces[i], interfaces[i], currentTypeParams, parentClassTypeData);
classesToScan.addAll(getClassHierarchy((Class<? super T>) interfaces[i], ifaceResolved));
}

Class<? super T> superClass = currentClass.getSuperclass();
if (superClass != null) {
parentClassTypeData = TypeData.newInstance(
currentClass.getGenericSuperclass(), superClass,
currentTypeParams, parentClassTypeData);
}
currentClass = currentClass.getSuperclass();
currentClass = superClass;
}
return classesToScan;
}
Expand Down
61 changes: 50 additions & 11 deletions bson/src/main/org/bson/codecs/pojo/TypeData.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.bson.codecs.pojo;

import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
Expand Down Expand Up @@ -61,32 +62,70 @@ public static TypeData<?> newInstance(final Field field) {
}

public static <T> TypeData<T> newInstance(final Type genericType, final Class<T> clazz) {
TypeData.Builder<T> builder = TypeData.builder(clazz);
if (genericType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) genericType;
// No enclosing class context: type variables have nothing to resolve against and erase to Object.
return newInstance(genericType, clazz, Collections.<TypeVariable<?>>emptyList(), null);
}

static <T> TypeData<T> newInstance(final Type genericParentType, final Class<T> parentClass,
final List<TypeVariable<?>> currentClassTypeParameters,
@Nullable final TypeData<?> currentClassTypeData) {
TypeData.Builder<T> builder = TypeData.builder(parentClass);
if (genericParentType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) genericParentType;
for (Type argType : pType.getActualTypeArguments()) {
getNestedTypeData(builder, argType);
builder.addTypeParameter(resolveTypeArgument(argType, currentClassTypeParameters, currentClassTypeData));
}
}
return builder.build();
}

@SuppressWarnings({"unchecked", "rawtypes"})
private static <T> void getNestedTypeData(final TypeData.Builder<T> builder, final Type type) {
private static TypeData<?> resolveTypeArgument(final Type type,
final List<TypeVariable<?>> currentClassTypeParameters,
@Nullable final TypeData<?> currentClassTypeData) {
if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
TypeData.Builder paramBuilder = TypeData.builder((Class) pType.getRawType());
for (Type argType : pType.getActualTypeArguments()) {
getNestedTypeData(paramBuilder, argType);
paramBuilder.addTypeParameter(resolveTypeArgument(argType, currentClassTypeParameters, currentClassTypeData));
}
builder.addTypeParameter(paramBuilder.build());
} else if (type instanceof WildcardType) {
builder.addTypeParameter(TypeData.builder((Class) ((WildcardType) type).getUpperBounds()[0]).build());
return paramBuilder.build();
} else if (type instanceof TypeVariable) {
builder.addTypeParameter(TypeData.builder(Object.class).build());
return resolveTypeVariable((TypeVariable<?>) type, currentClassTypeParameters, currentClassTypeData);
} else if (type instanceof Class) {
builder.addTypeParameter(TypeData.builder((Class) type).build());
return TypeData.builder((Class) type).build();
} else if (type instanceof WildcardType) {
// A wildcard cannot be the top-level type argument of an extends/implements clause (JLS §8.1.4,
// §8.1.5), but it can appear nested inside one (e.g. extends Base<List<? extends Number>>) or in a
// field/method generic type. Resolve it to its upper bound so any type variable inside the bound is
// still substituted against the current context.
return resolveTypeArgument(((WildcardType) type).getUpperBounds()[0], currentClassTypeParameters,
currentClassTypeData);
} else {
// Any other Type (e.g. GenericArrayType) is erased to Object.
return TypeData.builder(Object.class).build();
}
}

private static TypeData<?> resolveTypeVariable(final TypeVariable<?> type, final List<TypeVariable<?>> currentClassTypeParameters,
@Nullable final TypeData<?> currentClassTypeData) {
if (currentClassTypeData != null) {
for (int i = 0; i < currentClassTypeParameters.size(); i++) {
// Given 'class B<T> extends A<T> {}':
// - JLS §6.3: the scope of B's type parameter T includes the superclass clause.
// - JLS §6.5.5.1: T in "extends A<T>" therefore denotes B's T, not A's.
// Both reflection paths represent the same declaration, and the TypeVariable
// contract states "all instances representing a type variable must be equal() to
// each other".
if (currentClassTypeParameters.get(i).equals(type)) {
if (i < currentClassTypeData.getTypeParameters().size()) {
return currentClassTypeData.getTypeParameters().get(i);
}
break;
}
}
}
return TypeData.builder(Object.class).build();
}

/**
Expand Down
61 changes: 60 additions & 1 deletion bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@

import org.bson.codecs.pojo.entities.CollectionNestedPojoModel;
import org.bson.codecs.pojo.entities.ConcreteAndNestedAbstractInterfaceModel;
import org.bson.codecs.pojo.entities.ForwardingInterfaceModel;
import org.bson.codecs.pojo.entities.ForwardingDualInterfaceModel;
import org.bson.codecs.pojo.entities.ForwardingMixedModel;
import org.bson.codecs.pojo.entities.ForwardingModel;
import org.bson.codecs.pojo.entities.ForwardingArrayModel;
import org.bson.codecs.pojo.entities.ForwardingNestedModel;
import org.bson.codecs.pojo.entities.GenericHolderModel;
import org.bson.codecs.pojo.entities.InterfaceBasedModel;
import org.bson.codecs.pojo.entities.ListGenericExtendedModel;
Expand Down Expand Up @@ -266,7 +272,60 @@ public void testSimpleWithStaticModel() {

}

<T> TypeData.Builder<T> createBuilder(final Class<T> clazz, final Class<?>... types) {
@Test
public void testForwardingClassChain() {
ClassModel<?> classModel = ClassModel.builder(ForwardingModel.class).build();

assertEquals(1, classModel.getPropertyModels().size());
assertEquals(createTypeData(String.class), classModel.getPropertyModel("value").getTypeData());
}

@Test
public void testForwardingInterfaceChain() {
ClassModel<?> classModel = ClassModel.builder(ForwardingInterfaceModel.class).build();

assertEquals(1, classModel.getPropertyModels().size());
assertEquals(createTypeData(Integer.class), classModel.getPropertyModel("value").getTypeData());
}

@Test
public void testForwardingNested() {
ClassModel<?> classModel = ClassModel.builder(ForwardingNestedModel.class).build();

assertEquals(1, classModel.getPropertyModels().size());
assertEquals(createTypeData(List.class, String.class), classModel.getPropertyModel("value").getTypeData());
}

@Test
public void testForwardingArrayTypeVariableErasedToObject() {
// The type argument `T[]` in `extends ForwardingArrayLevel2<T[]>` is a GenericArrayType;
// getTypeParameterMap does not handle GenericArrayType, so the `value` property erases to
// Object regardless of the concrete binding at the leaf subclass.
ClassModel<?> classModel = ClassModel.builder(ForwardingArrayModel.class).build();

assertEquals(1, classModel.getPropertyModels().size());
assertEquals(createTypeData(Object.class), classModel.getPropertyModel("value").getTypeData());
}

@Test
public void testForwardingMixedClassAndInterface() {
ClassModel<?> classModel = ClassModel.builder(ForwardingMixedModel.class).build();

assertEquals(2, classModel.getPropertyModels().size());
assertEquals(createTypeData(String.class), classModel.getPropertyModel("field1").getTypeData());
assertEquals(createTypeData(Integer.class), classModel.getPropertyModel("field2").getTypeData());
}

@Test
public void testForwardingDualInterface() {
ClassModel<?> classModel = ClassModel.builder(ForwardingDualInterfaceModel.class).build();

assertEquals(2, classModel.getPropertyModels().size());
assertEquals(createTypeData(String.class), classModel.getPropertyModel("field2").getTypeData());
assertEquals(createTypeData(Integer.class), classModel.getPropertyModel("field1").getTypeData());
}

<T> TypeData.Builder<T> createBuilder(final Class<T> clazz, final Class<?>... types) {
TypeData.Builder<T> builder = TypeData.builder(clazz);
List<TypeData<?>> subTypes = new ArrayList<>();
for (final Class<?> type : types) {
Expand Down
26 changes: 26 additions & 0 deletions bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
import org.bson.codecs.pojo.entities.ConventionModel;
import org.bson.codecs.pojo.entities.DuplicateAnnotationAllowedModel;
import org.bson.codecs.pojo.entities.FieldAndPropertyTypeMismatchModel;
import org.bson.codecs.pojo.entities.ForwardingInterfaceModel;
import org.bson.codecs.pojo.entities.ForwardingModel;
import org.bson.codecs.pojo.entities.ForwardingNestedModel;
import org.bson.codecs.pojo.entities.ForwardingWildcardModel;
import org.bson.codecs.pojo.entities.GenericHolderModel;
import org.bson.codecs.pojo.entities.GenericTreeModel;
import org.bson.codecs.pojo.entities.InterfaceBasedModel;
Expand Down Expand Up @@ -526,6 +530,28 @@ private static List<TestData> testCases() {
getPojoCodecProviderBuilder(BsonExtraElementsMapModel.class),
"{'integerField': 42, 'stringField': 'myString', 'a': 'a', 'b': 'b'}"));

data.add(new TestData("Forwarding class chain resolves to String",
new ForwardingModel("hello"),
getPojoCodecProviderBuilder(ForwardingModel.class),
"{'value': 'hello'}"));

data.add(new TestData("Forwarding interface chain resolves to Integer",
new ForwardingInterfaceModel(7),
getPojoCodecProviderBuilder(ForwardingInterfaceModel.class),
"{'value': 7}"));

data.add(new TestData("Forwarding nested generic chain resolves to List<String>",
new ForwardingNestedModel(asList("a", "b", "c")),
getPojoCodecProviderBuilder(ForwardingNestedModel.class),
"{'value': ['a', 'b', 'c']}"));
data.add(new TestData("Forwarding nested wildcard chain resolves to List<? extends ShapeModelAbstract>",
new ForwardingWildcardModel(asList(getShapeModelCircle(), getShapeModelRectangle())),
getPojoCodecProviderBuilder(ForwardingWildcardModel.class, ShapeModelAbstract.class,
ShapeModelCircle.class, ShapeModelRectangle.class),
"{'value': [{'_t': 'org.bson.codecs.pojo.entities.ShapeModelCircle', 'color': 'orange', 'radius': 4.2}, "
+ "{'_t': 'org.bson.codecs.pojo.entities.ShapeModelRectangle', 'color': 'green', 'width': 22.1, "
+ "'height': 105.0}]}"));

return data;
}

Expand Down
Loading