From d1dbf2b1ef3e4a0d5be2be3ecbdb58867c4f114b Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Mon, 24 Dec 2018 14:31:02 +0100 Subject: [PATCH] Implement substitution of generics --- .../net/jodah/typetools/TypeResolver.java | 115 ++++++++++++++++++ .../net/jodah/typetools/TypeResolverTest.java | 74 +++++++++++ .../net/jodah/typetools/issues/Issue8.java | 35 ++++++ 3 files changed, 224 insertions(+) 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..52ede80 100644 --- a/src/main/java/net/jodah/typetools/TypeResolver.java +++ b/src/main/java/net/jodah/typetools/TypeResolver.java @@ -28,12 +28,14 @@ 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; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.WeakHashMap; import sun.misc.Unsafe; @@ -56,6 +58,7 @@ public final class TypeResolver { private static final Map OBJECT_METHODS = new HashMap(); private static final Map, Class> PRIMITIVE_WRAPPERS; private static final Double JAVA_VERSION; + private static final String NEW_ISSUE_URL = "https://github.com/jhalterman/typetools/issues/new"; static { JAVA_VERSION = Double.parseDouble(System.getProperty("java.specification.version", "0")); @@ -115,6 +118,58 @@ private Unknown() { } } + private static class ResolvedParameterizedType implements ParameterizedType { + private final ParameterizedType original; + private final Type[] resolvedTypeArguments; + + private ResolvedParameterizedType(ParameterizedType original, Type[] resolvedTypeArguments) { + Objects.requireNonNull(original); + Objects.requireNonNull(resolvedTypeArguments); + + this.original = original; + this.resolvedTypeArguments = resolvedTypeArguments; + } + + public Type[] getGenericActualTypeArguments() { + return original.getActualTypeArguments(); + } + + @Override + public Type[] getActualTypeArguments() { + return resolvedTypeArguments; + } + + @Override + public Type getRawType() { + return original.getRawType(); + } + + @Override + public Type getOwnerType() { + return original.getOwnerType(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResolvedParameterizedType that = (ResolvedParameterizedType) o; + return original.equals(that.original) && + Arrays.equals(resolvedTypeArguments, that.resolvedTypeArguments); + } + + @Override + public int hashCode() { + int result = Objects.hash(original); + result = 31 * result + Arrays.hashCode(resolvedTypeArguments); + return result; + } + } + private TypeResolver() { } @@ -182,6 +237,14 @@ public static Class[] resolveRawArguments(Class type, Cla return resolveRawArguments(resolveGenericType(type, subType), subType); } + public static Type resolveType(Class type, Class subType) { + return substituteGenerics(resolveGenericType(type, subType), getTypeVariableMap(subType, null)); + } + + public static Type resolveType(Type type, Class context) { + return substituteGenerics(type, getTypeVariableMap(context, null)); + } + /** * 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 @@ -334,6 +397,58 @@ private static Map, Type> getTypeVariableMap(final Class targ return map; } + private static Type substituteGenerics(final Type genericType, final Map, Type> typeVariableMap) { + if (genericType == null) { + return null; + } + if (genericType instanceof Class) { + return genericType; + } + if (genericType instanceof TypeVariable) { + final TypeVariable typeVariable = (TypeVariable) genericType; + final Type mapping = typeVariableMap.get(typeVariable); + if (mapping != null) { + return substituteGenerics(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. + return substituteGenerics(upperBounds[0], typeVariableMap); + } + 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 substituteGenerics(upperBounds[0], typeVariableMap); + } + throw new UnsupportedOperationException( + "Resolution of wildcard types is only supported for the trivial case of exactly one upper bound " + + "and no lower bounds. If you require resolution in a more complex case, please file an issue via " + + NEW_ISSUE_URL + ); + } + if (genericType instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) genericType; + final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + final Type[] resolvedTypeArguments = new Type[actualTypeArguments.length]; + + boolean changed = false; + for (int i = 0; i < actualTypeArguments.length; i++) { + resolvedTypeArguments[i] = substituteGenerics(actualTypeArguments[i], typeVariableMap); + changed = changed || (resolvedTypeArguments[i] != actualTypeArguments[i]); + } + + return changed ? new ResolvedParameterizedType(parameterizedType, resolvedTypeArguments) : parameterizedType; + } + throw new UnsupportedOperationException( + "Cannot substitute generics for type with name '" + genericType.getTypeName() + "' and " + + "class name '" + genericType.getClass().getName() + "'. Please file an issue including this message via " + + NEW_ISSUE_URL + ); + } + /** * Populates the {@code map} with with variable/argument pairs for the given {@code types}. */ diff --git a/src/test/java/net/jodah/typetools/TypeResolverTest.java b/src/test/java/net/jodah/typetools/TypeResolverTest.java index c210287..1141e0e 100644 --- a/src/test/java/net/jodah/typetools/TypeResolverTest.java +++ b/src/test/java/net/jodah/typetools/TypeResolverTest.java @@ -6,6 +6,7 @@ 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.HashMap; @@ -86,12 +87,53 @@ public void shouldResolveArgumentForList() { assertEquals(TypeResolver.resolveRawArgument(List.class, SomeList.class), Integer.class); } + public void shouldResolveTypeForList() { + Type resolvedType = TypeResolver.resolveType(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.resolveType(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.resolveType(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,6 +197,14 @@ static class TypeArrayFixture { static class TypeArrayImpl extends TypeArrayFixture { } + static class TypeListFixture { + List testList; + T testPlain; + } + + static class TypeListImpl extends TypeListFixture { + } + public void shouldResolveGenericTypeArray() throws Throwable { Type arrayField = TypeArrayFixture.class.getDeclaredField("test").getGenericType(); @@ -162,6 +212,30 @@ public void shouldResolveGenericTypeArray() throws Throwable { assertEquals(arg, String[].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 shouldResolveTypeList() throws Throwable { + Type listField = TypeListFixture.class.getDeclaredField("testList").getGenericType(); + + Type arg = TypeResolver.resolveType(listField, TypeListImpl.class); + assert arg instanceof ParameterizedType; + ParameterizedType parameterizedType = (ParameterizedType) arg; + assertEquals(parameterizedType.getRawType(), List.class); + assertEquals(parameterizedType.getActualTypeArguments()[0], String.class); + } + + public void shouldResolveTypePlain() throws Throwable { + Type plainField = TypeListFixture.class.getDeclaredField("testPlain").getGenericType(); + + Type arg = TypeResolver.resolveType(plainField, TypeListImpl.class); + assert arg == String.class; + } + public void shouldReturnNullOnResolveArgumentsForNonParameterizedType() { assertNull(TypeResolver.resolveRawArguments(Object.class, String.class)); } 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..5fadb41 --- /dev/null +++ b/src/test/java/net/jodah/typetools/issues/Issue8.java @@ -0,0 +1,35 @@ +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.resolveType(Foo.class, Bar.class); + 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); + } +}