Skip to content

Commit

Permalink
Implement substitution of generics
Browse files Browse the repository at this point in the history
  • Loading branch information
lorenzleutgeb committed Dec 24, 2018
1 parent dc5f6d0 commit 867c0ca
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
117 changes: 117 additions & 0 deletions src/main/java/net/jodah/typetools/TypeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,6 +58,7 @@ public final class TypeResolver {
private static final Map<String, Method> OBJECT_METHODS = new HashMap<String, Method>();
private static final Map<Class<?>, 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"));
Expand Down Expand Up @@ -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() {
}

Expand Down Expand Up @@ -182,6 +237,14 @@ public static <T, S extends T> Class<?>[] resolveRawArguments(Class<T> type, Cla
return resolveRawArguments(resolveGenericType(type, subType), subType);
}

public static <T, S extends T> Type resolveType(Class<T> type, Class<S> 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
Expand Down Expand Up @@ -334,6 +397,60 @@ private static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targ
return map;
}

private static Type substituteGenerics(final Type genericType, final Map<TypeVariable<?>, 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(
"Attempted to resolve type " + wildcardType + " which has " + upperBounds.length + " upper bounds " +
"and " + lowerBounds.length + " lower bounds. " +
"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 this 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}.
*/
Expand Down
94 changes: 94 additions & 0 deletions src/test/java/net/jodah/typetools/TypeResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -155,13 +197,49 @@ static class TypeArrayFixture<T> {
static class TypeArrayImpl extends TypeArrayFixture<String> {
}

static class TypeListFixture<T> {
List<T> testList;

T testPlain;

public void testMethod(List<? super T> arg) {
}
}

static class TypeListImpl extends TypeListFixture<String> {
}

public void shouldResolveGenericTypeArray() throws Throwable {
Type arrayField = TypeArrayFixture.class.getDeclaredField("test").getGenericType();

Class<?> arg = TypeResolver.resolveRawClass(arrayField, TypeArrayImpl.class);
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));
}
Expand All @@ -176,6 +254,22 @@ public void shouldThrowOnResolveArgumentForTypeWithMultipleArguments() {
}.getClass());
}

@Test(expectedExceptions = UnsupportedOperationException.class)
public void shouldThrowOnResolveArgumentForUnknownImplementation() {
TypeResolver.resolveType(new Type() {
@Override
public String getTypeName() {
return "unknown";
}
}, Bar.class);
}

@Test(expectedExceptions = UnsupportedOperationException.class)
public void shouldThrowOnResolveOfNontrivialBounds() throws Exception {
Type toResolve = TypeListImpl.class.getMethod("testMethod", List.class).getGenericParameterTypes()[0];
TypeResolver.resolveType(toResolve, TypeListImpl.class);
}

public void shouldResolveTypeParamFromAnonymousClass() {
List<String> stringList = new ArrayList<String>() {
};
Expand Down
35 changes: 35 additions & 0 deletions src/test/java/net/jodah/typetools/issues/Issue8.java
Original file line number Diff line number Diff line change
@@ -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<T1, T2> {
}

class Bar implements Foo<List<Integer>, List<String>> {
}

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);
}
}

0 comments on commit 867c0ca

Please sign in to comment.