Skip to content

Commit

Permalink
ensmarten generics discovery
Browse files Browse the repository at this point in the history
fixes #2447
  • Loading branch information
evanchooly committed Jul 20, 2023
1 parent 0b00125 commit 7be0a4b
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 7 deletions.
95 changes: 92 additions & 3 deletions core/src/main/java/dev/morphia/mapping/codec/pojo/TypeData.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,8 +72,9 @@ public static <T> Builder<T> builder(Class<T> 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();
Expand All @@ -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) {
Expand All @@ -99,6 +103,91 @@ private static TypeData<?> getTypeData(Type type) {
throw new UnsupportedOperationException(Sofia.unhandledTypeData(type.getClass()));
}

static class TypeParameters implements Iterable<Type> {
final List<Param> params = new ArrayList<>();

public static TypeParameters of(Type type) {
if (type instanceof Class<?>) {
Class<?> klass = (Class<?>) type;
TypeVariable<? extends Class<?>>[] parameters = ((Class<?>) type).getTypeParameters();
TypeParameters params = new TypeParameters();
for (TypeVariable<? extends Class<?>> 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<? super Type> action) {
throw new UnsupportedOperationException();
}

@Override
public Spliterator<Type> spliterator() {
return Iterable.super.spliterator();
}

@Override
public Iterator<Type> 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<TypeData<?>> typeParameters) {
StringBuilder builder = new StringBuilder();
int count = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TypeData<?>> 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<TypeData<?>> 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.morphia.test.mapping.codec.pojo.generics;

import java.time.LocalDateTime;

public class FullHashMap extends PartialHashMap<LocalDateTime> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.morphia.test.mapping.codec.pojo.generics;

import java.util.HashMap;

public class PartialHashMap<T> extends HashMap<String, T> {

}
Original file line number Diff line number Diff line change
@@ -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<BitSet> {
}
Original file line number Diff line number Diff line change
@@ -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<Integer> partialHashMap;
public FullHashMap fullHashMap;
public List<Locale> genericList;
public PartialList partialList;
public String name;
public int age;

@PostPersist
public void pre(Document document) {
System.out.println("document = " + document);
}

}

0 comments on commit 7be0a4b

Please sign in to comment.