Skip to content
Draft
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
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
68 changes: 68 additions & 0 deletions bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@

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.HolderConcreteMapModel;
import org.bson.codecs.pojo.entities.InterfaceBasedModel;
import org.bson.codecs.pojo.entities.ListGenericExtendedModel;
import org.bson.codecs.pojo.entities.ListListGenericExtendedModel;
Expand Down Expand Up @@ -266,6 +273,67 @@ public void testSimpleWithStaticModel() {

}

@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() {
// T[] fields in a generic base class are GenericArrayType; getTypeParameterMap does not
// handle GenericArrayType component types, so the T[] 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());
}

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

assertEquals(1, classModel.getPropertyModels().size());
assertEquals(createTypeData(Map.class, String.class, Object.class), classModel.getPropertyModel("value").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<>();
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