diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/TypeData.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/TypeData.java index 72b34361d5a..a5eee08d52f 100644 --- a/core/src/main/java/dev/morphia/mapping/codec/pojo/TypeData.java +++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/TypeData.java @@ -9,8 +9,11 @@ import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.Spliterator; import java.util.StringJoiner; +import java.util.function.Consumer; import dev.morphia.annotations.internal.MorphiaInternal; import dev.morphia.sofia.Sofia; @@ -69,8 +72,9 @@ public static Builder builder(Class type) { private static TypeData getTypeData(Type type) { if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; + TypeParameters parameters = TypeParameters.of(pType); Builder paramBuilder = TypeData.builder((Class) pType.getRawType()); - for (Type argType : pType.getActualTypeArguments()) { + for (Type argType : parameters) { paramBuilder.addTypeParameter(getTypeData(argType)); } return paramBuilder.build(); @@ -85,8 +89,8 @@ private static TypeData getTypeData(Type type) { return TypeData.builder(Object.class).build(); } else if (type instanceof Class) { Builder builder = TypeData.builder((Class) type); - for (TypeVariable typeParameter : builder.type.getTypeParameters()) { - builder.addTypeParameter(getTypeData(typeParameter)); + for (Type argType : TypeParameters.of(type)) { + builder.addTypeParameter(getTypeData(argType)); } return builder.build(); } else if (type instanceof GenericArrayType) { @@ -99,6 +103,91 @@ private static TypeData getTypeData(Type type) { throw new UnsupportedOperationException(Sofia.unhandledTypeData(type.getClass())); } + static class TypeParameters implements Iterable { + final List params = new ArrayList<>(); + + public static TypeParameters of(Type type) { + if (type instanceof Class) { + Class klass = (Class) type; + TypeVariable>[] parameters = ((Class) type).getTypeParameters(); + TypeParameters params = new TypeParameters(); + for (TypeVariable> parameter : parameters) { + params.add(new Param(parameter.getName(), Object.class)); + } + Type superclass = klass.getGenericSuperclass(); + if (!klass.isEnum() && superclass instanceof ParameterizedType) { + return of((ParameterizedType) superclass, params); + } + return params; + } else { + throw new UnsupportedOperationException("Unsupported type passed: " + type); + } + } + + public static TypeParameters of(ParameterizedType type) { + return of(type, new TypeParameters()); + } + + private static TypeParameters of(ParameterizedType type, TypeParameters subtypeParams) { + TypeParameters params = new TypeParameters(); + Type[] typeArguments = type.getActualTypeArguments(); + TypeVariable[] typeParameters = ((Class) type.getRawType()).getTypeParameters(); + int index = 0; + for (int i = 0; i < typeParameters.length; i++) { + Type typeArgument = typeArguments[i]; + if (typeArgument instanceof TypeVariable && index < subtypeParams.params.size()) { + typeArgument = subtypeParams.params.get(index++).type; + } + params.add(new Param(typeParameters[i].getName(), typeArgument)); + } + Type genericSuperclass = ((Class) type.getRawType()).getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + params = TypeParameters.of((ParameterizedType) genericSuperclass, params); + } + return params; + } + + @Override + public void forEach(Consumer action) { + throw new UnsupportedOperationException(); + } + + @Override + public Spliterator spliterator() { + return Iterable.super.spliterator(); + } + + @Override + public Iterator iterator() { + return params.stream().map(p -> p.type) + .iterator(); + } + + private void add(Param param) { + params.add(param); + } + + @Override + public String toString() { + return format("TypeParameters{%s}", params); + } + + private static class Param { + private String name; + private Type type; + + public Param(String name, Type type) { + this.name = name; + this.type = type; + } + + @Override + public String toString() { + return format("Param{name='%s', type=%s}", name, type.getTypeName()); + } + } + } + private static String nestedTypeParameters(List> typeParameters) { StringBuilder builder = new StringBuilder(); int count = 0; diff --git a/core/src/test/java/dev/morphia/test/mapping/codec/pojo/TypeDataTest.java b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/TypeDataTest.java index bab087882b8..bc38ce4e60d 100644 --- a/core/src/test/java/dev/morphia/test/mapping/codec/pojo/TypeDataTest.java +++ b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/TypeDataTest.java @@ -1,28 +1,75 @@ package dev.morphia.test.mapping.codec.pojo; +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.BitSet; import java.util.List; +import java.util.Locale; import dev.morphia.mapping.codec.pojo.TypeData; import dev.morphia.test.TestBase; +import dev.morphia.test.mapping.codec.pojo.generics.FullHashMap; +import dev.morphia.test.mapping.codec.pojo.generics.PartialHashMap; +import dev.morphia.test.mapping.codec.pojo.generics.PartialList; +import dev.morphia.test.mapping.codec.pojo.generics.Subtypes; +import dev.morphia.test.models.Hotel; +import dev.morphia.test.models.Hotel.Type; -import org.testng.Assert; import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + public class TypeDataTest extends TestBase { @Test public void testWildcards() throws NoSuchFieldException { TypeData typeData = TypeData.newInstance(WildCard.class.getDeclaredField("listOfLists")); - Assert.assertEquals(typeData.getType(), List.class); + assertEquals(typeData.getType(), List.class); List> typeParameters = typeData.getTypeParameters(); typeData = typeParameters.get(0); - Assert.assertEquals(typeData.getType(), List.class); + assertEquals(typeData.getType(), List.class); typeParameters = typeData.getTypeParameters(); typeData = typeParameters.get(0); - Assert.assertEquals(typeData.getType(), String.class); + assertEquals(typeData.getType(), String.class); + + } + + @Test + public void testEnums() { + try { + typeData(Hotel.class, "type", Type.class); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + @Test + public void testSubtypes() { + getMapper().mapPackageFromClass(Subtypes.class); + try { + typeData(Subtypes.class, "partialHashMap", PartialHashMap.class, String.class, Integer.class); + typeData(Subtypes.class, "fullHashMap", FullHashMap.class, String.class, LocalDateTime.class); + typeData(Subtypes.class, "genericList", List.class, Locale.class); + typeData(Subtypes.class, "partialList", PartialList.class, BitSet.class); + typeData(Subtypes.class, "name", String.class); + typeData(Subtypes.class, "age", int.class); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static void typeData(Class owner, String fieldName, Class fieldType, Class... parameterTypes) + throws NoSuchFieldException { + Field field = owner.getDeclaredField(fieldName); + TypeData typeData = TypeData.newInstance(field); + assertEquals(typeData.getType(), fieldType); + List> typeParameters = typeData.getTypeParameters(); + assertEquals(typeParameters.size(), parameterTypes.length); + for (int i = 0; i < parameterTypes.length; i++) { + assertEquals(typeParameters.get(i).getType(), parameterTypes[i]); + } } private static class WildCard { diff --git a/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/FullHashMap.java b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/FullHashMap.java new file mode 100644 index 00000000000..1fd7568efae --- /dev/null +++ b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/FullHashMap.java @@ -0,0 +1,7 @@ +package dev.morphia.test.mapping.codec.pojo.generics; + +import java.time.LocalDateTime; + +public class FullHashMap extends PartialHashMap { + +} diff --git a/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/PartialHashMap.java b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/PartialHashMap.java new file mode 100644 index 00000000000..b31837abf6e --- /dev/null +++ b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/PartialHashMap.java @@ -0,0 +1,7 @@ +package dev.morphia.test.mapping.codec.pojo.generics; + +import java.util.HashMap; + +public class PartialHashMap extends HashMap { + +} diff --git a/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/PartialList.java b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/PartialList.java new file mode 100644 index 00000000000..5f29ee70d88 --- /dev/null +++ b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/PartialList.java @@ -0,0 +1,7 @@ +package dev.morphia.test.mapping.codec.pojo.generics; + +import java.util.ArrayList; +import java.util.BitSet; + +public class PartialList extends ArrayList { +} diff --git a/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/Subtypes.java b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/Subtypes.java new file mode 100644 index 00000000000..e5734220440 --- /dev/null +++ b/core/src/test/java/dev/morphia/test/mapping/codec/pojo/generics/Subtypes.java @@ -0,0 +1,28 @@ +package dev.morphia.test.mapping.codec.pojo.generics; + +import java.util.List; +import java.util.Locale; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; +import dev.morphia.annotations.PostPersist; + +import org.bson.Document; + +@Entity +public class Subtypes { + @Id + public String id; + public PartialHashMap partialHashMap; + public FullHashMap fullHashMap; + public List genericList; + public PartialList partialList; + public String name; + public int age; + + @PostPersist + public void pre(Document document) { + System.out.println("document = " + document); + } + +}