From 5bafe916acc6179e27dfac72ed431d317951ecb6 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Mon, 24 Dec 2018 14:31:02 +0100 Subject: [PATCH] Implement reification --- .../net/jodah/typetools/TypeResolver.java | 257 ++++++++++++++++++ .../net/jodah/typetools/TypeResolverTest.java | 166 ++++++++++- .../net/jodah/typetools/issues/Issue8.java | 38 +++ 3 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/jodah/typetools/issues/Issue8.java diff --git a/src/main/java/net/jodah/typetools/TypeResolver.java b/src/main/java/net/jodah/typetools/TypeResolver.java index 0ed4542..0b9f17d 100644 --- a/src/main/java/net/jodah/typetools/TypeResolver.java +++ b/src/main/java/net/jodah/typetools/TypeResolver.java @@ -28,6 +28,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.security.AccessController; import java.security.PrivilegedExceptionAction; import java.util.Arrays; @@ -115,6 +116,93 @@ private Unknown() { } } + private static class ReifiedParameterizedType implements ParameterizedType { + private final ParameterizedType original; + private final Type[] resolvedTypeArguments; + + private ReifiedParameterizedType(ParameterizedType original, Type[] resolvedTypeArguments) { + this.original = original; + this.resolvedTypeArguments = resolvedTypeArguments; + } + + @Override + public Type[] getActualTypeArguments() { + return resolvedTypeArguments; + } + + @Override + public Type getRawType() { + return original.getRawType(); + } + + @Override + public Type getOwnerType() { + return original.getOwnerType(); + } + + /** Keep this consistent with {@link sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl#toString} */ + @Override + public String toString() { + final Type ownerType = getOwnerType(); + final Type rawType = getRawType(); + final Type[] actualTypeArguments = getActualTypeArguments(); + + StringBuilder sb = new StringBuilder(); + + if (ownerType != null) { + if (ownerType instanceof Class) + sb.append(((Class) ownerType).getName()); + else + sb.append(ownerType.toString()); + + sb.append("."); + + if (ownerType instanceof ParameterizedType) { + // Find simple name of nested type by removing the + // shared prefix with owner. + sb.append(rawType.getTypeName() + .replace( ((ParameterizedType)ownerType).getRawType().getTypeName() + "$", "")); + } else + sb.append(rawType.getTypeName()); + } else + sb.append(rawType.getTypeName()); + + if (actualTypeArguments != null && actualTypeArguments.length > 0) { + sb.append("<"); + + boolean first = true; + for (Type t: actualTypeArguments) { + if (!first) + sb.append(", "); + sb.append(t.getTypeName()); + first = false; + } + sb.append(">"); + } + + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + ReifiedParameterizedType that = (ReifiedParameterizedType) o; + return original.equals(that.original) && + Arrays.equals(resolvedTypeArguments, that.resolvedTypeArguments); + } + + @Override + public int hashCode() { + int result = original.hashCode(); + result = 31 * result + Arrays.hashCode(resolvedTypeArguments); + return result; + } + } + private TypeResolver() { } @@ -182,6 +270,107 @@ public static Class[] resolveRawArguments(Class type, Cla return resolveRawArguments(resolveGenericType(type, subType), subType); } + /** + * Traverses a generic type and replaces all type variables and wildcard types with concrete types (if possible), + * by using the type information from given {@code context}. + * A convenience method which largely works the same as {@link #reify(Type, Class)}, but first resolves the + * generic type of {@code type}. + * + * @param type the class whose generic type to traverse + * @param context the class that serves as starting point to resolve replacements of type variables + * @return a type that is structurally the same as {@code type}, except that type variables and wildcard types + * have been replaced with concrete types + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is not an instance of one of + * the following types: {@link Class}, {@link TypeVariable}, {@link WildcardType}, {@link ParameterizedType}, + * {@link GenericArrayType}. + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is a {@link WildcardType} that + * does not have exactly one upper bound, or does not have no lower bounds. + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is a {@link GenericArrayType} + * whose generic component type cannot be reified to an instance of {@link Class}. + */ + public static Type reify(Class type, Class context) { + return reify(resolveGenericType(type, context), getTypeVariableMap(context, null)); + } + + /** + * Traverses a generic type and replaces all type variables and wildcard types with concrete types (if possible), + * by using the type information from given {@code context}. + * + * Generic types used as input to this method are commonly obtained using reflection, e.g. via + * {@link Field#getGenericType()}, {@link Method#getGenericReturnType()}, {@link Method#getGenericParameterTypes()}. + * + * Example: + *
{@code
+   *   class A {
+   *     public T something;
+   *     public Optional> compute() { ... }
+   *   }
+   *
+   *   class B extends A {
+   *     public >> collect() { ... }
+   *   }
+   * }
+ * + * Reifying the generic return type of the method {@code compute} with {@code B.class} as {@code context} will + * yield the parameterized type {@code Optional>}. Note that not the raw type ({@link java.util.Optional} + * is returned, but the input type is reified recursively. + * Reifying the generic type of the field {@code something} with {@code B.class} as {@code context} will yield + * {@code Number.class}. + * + * Note that type variables with no explicit upper bound are reified to {@link Object}, and {@code Unknown.class} is + * never returned. + * + * @param type the generic type to traverse + * @param context the class that serves as starting point to resolve replacements of type variables + * @return a type that is structurally the same as {@code type}, except that type variables and wildcard types + * have been replaced with concrete types + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is not an instance of one of + * the following types: {@link Class}, {@link TypeVariable}, {@link WildcardType}, {@link ParameterizedType}, + * {@link GenericArrayType}. + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is a {@link WildcardType} that + * does not have exactly one upper bound, or does not have no lower bounds. + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is a {@link GenericArrayType} + * whose generic component type cannot be reified to an instance of {@link Class}. + */ + public static Type reify(Type type, Class context) { + return reify(type, getTypeVariableMap(context, null)); + } + + /** + * Traverses a generic type and replaces all type variables and wildcard types with concrete types (if possible). + * A convenience wrapper around {@link #reify(Type, Class)}, for when no context is needed/available. + * + * Generic types used as input to this method are commonly obtained using reflection, e.g. via + * {@link Field#getGenericType()}, {@link Method#getGenericReturnType()}, {@link Method#getGenericParameterTypes()}. + * + * Example: + *
{@code
+   *   class X {
+   *     public List>> collectList() { ... }
+   *     public Set collectSet() { ... }
+   *   }
+   * }
+ * + * Reifying the generic return type of the method {@code collectList} will yield the parameterized type + * {@code List>>}. + * Reifying the generic return type of the method {@code collectSet} will yield the parameterized type + * {@code Set}, since there is no explicit upper bound for the wildcard type given. + * + * @param type the generic type to traverse + * @return a type that is structurally the same as {@code type}, except that type variables and wildcard types + * have been replaced with concrete types + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is not an instance of one of + * the following types: {@link Class}, {@link TypeVariable}, {@link WildcardType}, {@link ParameterizedType}, + * {@link GenericArrayType}. + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is a {@link WildcardType} that + * does not have exactly one upper bound, or does not have no lower bounds. + * @throws UnsupportedOperationException if {@code type} (or a type that it references) is a {@link GenericArrayType} + * whose generic component type cannot be reified to an instance of {@link Class}. + */ + public static Type reify(Type type) { + return reify(type, new HashMap, Type>(0)); + } + /** * Returns an array of raw classes representing arguments for the {@code genericType} using type variable information * from the {@code subType}. Arguments for {@code genericType} that cannot be resolved are returned as @@ -290,6 +479,74 @@ private static Class resolveRawClass(Type genericType, Class subType, Clas return genericType instanceof Class ? (Class) genericType : Unknown.class; } + /** + * Works like {@link #resolveRawClass(Type, Class, Class)} but does not stop at raw classes. Instead, traverses + * referenced types. + */ + private static Type reify(final Type genericType, final Map, Type> typeVariableMap) { + if (genericType == null) { + return null; + } else if (genericType instanceof Class) { + return genericType; + } else if (genericType instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) genericType; + final Type[] genericTypeArguments = parameterizedType.getActualTypeArguments(); + final Type[] reifiedTypeArguments = new Type[genericTypeArguments.length]; + + boolean changed = false; + for (int i = 0; i < genericTypeArguments.length; i++) { + reifiedTypeArguments[i] = reify(genericTypeArguments[i], typeVariableMap); + changed = changed || (reifiedTypeArguments[i] != genericTypeArguments[i]); + } + + return changed + ? new ReifiedParameterizedType(parameterizedType, reifiedTypeArguments) + : parameterizedType; + } else if (genericType instanceof GenericArrayType) { + final GenericArrayType genericArrayType = (GenericArrayType) genericType; + final Type genericComponentType = genericArrayType.getGenericComponentType(); + final Type reifiedComponentType = reify(genericArrayType.getGenericComponentType(), typeVariableMap); + + if (genericComponentType == reifiedComponentType) + return genericComponentType; + + if (reifiedComponentType instanceof Class) + return Array.newInstance((Class) reifiedComponentType, 0).getClass(); + + throw new UnsupportedOperationException( + "Attempted to reify generic array type, whose generic component type " + + "could not be reified to some Class. Handling for this case is not implemented"); + } else if (genericType instanceof TypeVariable) { + final TypeVariable typeVariable = (TypeVariable) genericType; + final Type mapping = typeVariableMap.get(typeVariable); + if (mapping != null) + return reify(mapping, typeVariableMap); + + final Type[] upperBounds = typeVariable.getBounds(); + // NOTE: According to https://docs.oracle.com/javase/tutorial/java/generics/bounded.html + // if there are multiple upper bounds where one bound is a class, then this must be the + // leftmost/first bound. Therefore we blindly take this one, hoping is the most relevant. + // Hibernate does the same when erasing types, see also + // https://github.com/hibernate/hibernate-validator/blob/6.0/engine/src/main/java/org/hibernate/validator/internal/util/TypeHelper.java#L181-L186 + return reify(upperBounds[0], typeVariableMap); + } else if (genericType instanceof WildcardType) { + final WildcardType wildcardType = (WildcardType) genericType; + final Type[] upperBounds = wildcardType.getUpperBounds(); + final Type[] lowerBounds = wildcardType.getLowerBounds(); + if (upperBounds.length == 1 && lowerBounds.length == 0) + return reify(upperBounds[0], typeVariableMap); + + throw new UnsupportedOperationException( + "Attempted to reify wildcard type with name '" + wildcardType + "' which has " + + upperBounds.length + " upper bounds and " + lowerBounds.length + " lower bounds. " + + "Reification of wildcard types is only supported for " + + "the trivial case of exactly one upper bound and no lower bounds."); + } + throw new UnsupportedOperationException( + "Reification of type with name '" + genericType.getTypeName() + "' and " + + "class name '" + genericType.getClass().getName() + "' is not implemented."); + } + private static Map, Type> getTypeVariableMap(final Class targetType, Class functionalInterface) { Reference, Type>> ref = TYPE_VARIABLE_CACHE.get(targetType); diff --git a/src/test/java/net/jodah/typetools/TypeResolverTest.java b/src/test/java/net/jodah/typetools/TypeResolverTest.java index c210287..899863b 100644 --- a/src/test/java/net/jodah/typetools/TypeResolverTest.java +++ b/src/test/java/net/jodah/typetools/TypeResolverTest.java @@ -3,11 +3,14 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; +import java.io.Closeable; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -86,12 +89,53 @@ public void shouldResolveArgumentForList() { assertEquals(TypeResolver.resolveRawArgument(List.class, SomeList.class), Integer.class); } + public void shouldResolveTypeForList() { + Type resolvedType = TypeResolver.reify(List.class, SomeList.class); + assert resolvedType instanceof ParameterizedType; + assertEquals(((ParameterizedType) resolvedType).getActualTypeArguments()[0], Integer.class); + } + public void shouldResolveArgumentsForBazFromFoo() { Class[] typeArguments = TypeResolver.resolveRawArguments(Baz.class, Foo.class); assert typeArguments[0] == HashSet.class; assert typeArguments[1] == ArrayList.class; } + public void shouldResolveParameterizedTypeForBazFromFoo() { + Type type = TypeResolver.reify(Baz.class, Foo.class); + + // Now we walk the type hierarchy: + assert type instanceof ParameterizedType; + Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + + assert typeArguments[0] instanceof ParameterizedType; + ParameterizedType firstTypeArgument = (ParameterizedType) typeArguments[0]; + assert firstTypeArgument.getRawType() == HashSet.class; + assert firstTypeArgument.getActualTypeArguments()[0] == Object.class; + + assert typeArguments[1] instanceof ParameterizedType; + ParameterizedType secondTypeArgument = (ParameterizedType) typeArguments[1]; + assert secondTypeArgument.getRawType() == ArrayList.class; + assert secondTypeArgument.getActualTypeArguments()[0] == Object.class; + } + + public void shouldResolvePartialParameterizedTypeForBazFromBar() { + Type type = TypeResolver.reify(Baz.class, Bar.class); + + assert type instanceof ParameterizedType; + Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + + assert typeArguments[0] instanceof ParameterizedType; + ParameterizedType firstTypeArgument = (ParameterizedType) typeArguments[0]; + assert firstTypeArgument.getRawType() == HashSet.class; + assert firstTypeArgument.getActualTypeArguments()[0] == Object.class; + + assert typeArguments[1] instanceof ParameterizedType; + ParameterizedType secondTypeArgument = (ParameterizedType) typeArguments[1]; + assert secondTypeArgument.getRawType() == List.class; + assert secondTypeArgument.getActualTypeArguments()[0] == Object.class; + } + public void shouldResolveArgumentsForIRepoFromRepoImplA() { Class[] types = TypeResolver.resolveRawArguments(IRepo.class, RepoImplA.class); assertEquals(types[0], Map.class); @@ -155,13 +199,76 @@ static class TypeArrayFixture { static class TypeArrayImpl extends TypeArrayFixture { } - public void shouldResolveGenericTypeArray() throws Throwable { + static class TypeArrayNonClassImpl extends TypeArrayFixture { + } + + static class TypeListFixture { + List testList; + + T testPlain; + + public void testMethod(List arg) { + } + } + + static class TypeListImpl extends TypeListFixture { + } + + static abstract class GenericMethodHolder { + public abstract T genericMethod(); + } + + public void shouldResolveGenericClassTypeArray() throws Throwable { Type arrayField = TypeArrayFixture.class.getDeclaredField("test").getGenericType(); Class arg = TypeResolver.resolveRawClass(arrayField, TypeArrayImpl.class); assertEquals(arg, String[].class); } + public void shouldReifyGenericArrayTypeWithInterface() throws Throwable { + Type arrayField = TypeArrayFixture.class.getDeclaredField("test").getGenericType(); + + Type arg = TypeResolver.reify(arrayField, TypeArrayNonClassImpl.class); + assertEquals(arg, Closeable[].class); + } + + public void shouldResolveRawTypeList() throws Throwable { + Type listField = TypeListFixture.class.getDeclaredField("testList").getGenericType(); + + Class arg = TypeResolver.resolveRawClass(listField, TypeListImpl.class); + assertEquals(arg, List.class); + } + + public void shouldReifyGenericArrayTypeWithClass() throws Throwable { + Type arrayField = TypeArrayFixture.class.getDeclaredField("test").getGenericType(); + + Type arg = TypeResolver.reify(arrayField, TypeArrayImpl.class); + assertEquals(arg, String[].class); + } + + public void shouldReifyList() throws Throwable { + Type listField = TypeListFixture.class.getDeclaredField("testList").getGenericType(); + + Type arg = TypeResolver.reify(listField, TypeListImpl.class); + assert arg instanceof ParameterizedType; + ParameterizedType parameterizedType = (ParameterizedType) arg; + assertEquals(parameterizedType.getRawType(), List.class); + assertEquals(parameterizedType.getActualTypeArguments()[0], String.class); + } + + public void shouldReifyTypeVariable() throws Throwable { + Type plainField = TypeListFixture.class.getDeclaredField("testPlain").getGenericType(); + + Type arg = TypeResolver.reify(plainField, TypeListImpl.class); + assert arg == String.class; + } + + public void shouldReifyWildcardFromGenericMethod() throws Exception { + Type type = TypeResolver.reify( + GenericMethodHolder.class.getMethod("genericMethod").getGenericReturnType()); + assert type == Number.class; + } + public void shouldReturnNullOnResolveArgumentsForNonParameterizedType() { assertNull(TypeResolver.resolveRawArguments(Object.class, String.class)); } @@ -170,12 +277,69 @@ public void shouldReturnUnknownOnResolveArgumentForNonParameterizedType() { assertEquals(TypeResolver.resolveRawArgument(Object.class, String.class), Unknown.class); } + private static abstract class WildcardWithBoundFixture { + public abstract List getNumberList(); + public abstract List>> collect(); + } + + public void shouldReifyWildcardToUpperBound() throws Exception { + Type type = TypeResolver.reify( + WildcardWithBoundFixture.class.getMethod("getNumberList").getGenericReturnType(), + WildcardWithBoundFixture.class + ); + + assert type instanceof ParameterizedType; + ParameterizedType parameterizedType = (ParameterizedType) type; + assert parameterizedType.getRawType() == List.class; + assert parameterizedType.getActualTypeArguments()[0] == Number.class; + } + + public void shouldReifyWildcardWithComplexUpperBound() throws Exception { + Type type = TypeResolver.reify( + WildcardWithBoundFixture.class.getMethod("collect").getGenericReturnType(), + WildcardWithBoundFixture.class + ); + + assertEquals(type.toString(), "java.util.List>>"); + + assert type instanceof ParameterizedType; + ParameterizedType parameterizedType = (ParameterizedType) type; + assert parameterizedType.getRawType() == List.class; + + assert parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType; + parameterizedType = (ParameterizedType) parameterizedType.getActualTypeArguments()[0]; + + assert parameterizedType.getRawType() == Collection.class; + + assert parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType; + parameterizedType = (ParameterizedType) parameterizedType.getActualTypeArguments()[0]; + + assert parameterizedType.getRawType() == List.class; + assert parameterizedType.getActualTypeArguments()[0] == Number.class; + } + @Test(expectedExceptions = IllegalArgumentException.class) public void shouldThrowOnResolveArgumentForTypeWithMultipleArguments() { TypeResolver.resolveRawArgument(Map.class, new HashMap() { }.getClass()); } + @Test(expectedExceptions = UnsupportedOperationException.class) + public void shouldThrowOnReifyForUnknownImplementation() { + TypeResolver.reify(new Type() { + @Override + public String getTypeName() { + return "unknown"; + } + }, Bar.class); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void shouldThrowOnReifyForNontrivialBounds() throws Exception { + Type toResolve = TypeListImpl.class.getMethod("testMethod", List.class).getGenericParameterTypes()[0]; + TypeResolver.reify(toResolve, TypeListImpl.class); + } + public void shouldResolveTypeParamFromAnonymousClass() { List stringList = new ArrayList() { }; diff --git a/src/test/java/net/jodah/typetools/issues/Issue8.java b/src/test/java/net/jodah/typetools/issues/Issue8.java new file mode 100644 index 0000000..aa74d5d --- /dev/null +++ b/src/test/java/net/jodah/typetools/issues/Issue8.java @@ -0,0 +1,38 @@ +package net.jodah.typetools.issues; + +import net.jodah.typetools.TypeResolver; +import org.testng.annotations.Test; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * https://github.com/jhalterman/typetools/issues/8 + */ +@Test +public class Issue8 { + interface Foo { + } + + class Bar implements Foo, List> { + } + + public void test() { + Type typeArgs = TypeResolver.reify(Foo.class, Bar.class); + assertEquals( + typeArgs.toString(), + "net.jodah.typetools.issues.Issue8$Foo, java.util.List>"); + assertTrue(typeArgs instanceof ParameterizedType); + ParameterizedType par = (ParameterizedType) typeArgs; + assertEquals(par.getRawType(), Foo.class); + assertEquals(par.getActualTypeArguments().length, 2); + assertTrue(par.getActualTypeArguments()[0] instanceof ParameterizedType); + ParameterizedType firstArg = (ParameterizedType) par.getActualTypeArguments()[0]; + assertEquals(firstArg.getRawType(), List.class); + assertEquals(firstArg.getActualTypeArguments()[0], Integer.class); + } +}