Skip to content

Commit

Permalink
Merge pull request #46 from lorenzleutgeb/resolve
Browse files Browse the repository at this point in the history
Reification
  • Loading branch information
jhalterman authored Dec 26, 2018
2 parents dc5f6d0 + 5bafe91 commit 15cf7a1
Show file tree
Hide file tree
Showing 3 changed files with 460 additions and 1 deletion.
257 changes: 257 additions & 0 deletions src/main/java/net/jodah/typetools/TypeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
}

Expand Down Expand Up @@ -182,6 +270,107 @@ public static <T, S extends T> Class<?>[] resolveRawArguments(Class<T> 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 <T, S extends T> Type reify(Class<T> type, Class<S> 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:
* <blockquote><pre>{@code
* class A<T> {
* public T something;
* public Optional<List<T>> compute() { ... }
* }
*
* class B extends A<Number> {
* public <? extends Collection<List<?>>> collect() { ... }
* }
* }</pre></blockquote>
*
* Reifying the generic return type of the method {@code compute} with {@code B.class} as {@code context} will
* yield the parameterized type {@code Optional<List<Number>>}. 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:
* <blockquote><pre>{@code
* class X {
* public List<? extends Collection<List<? extends Number>>> collectList() { ... }
* public Set<?> collectSet() { ... }
* }
* }</pre></blockquote>
*
* Reifying the generic return type of the method {@code collectList} will yield the parameterized type
* {@code List<Collection<List<Number>>>}.
* Reifying the generic return type of the method {@code collectSet} will yield the parameterized type
* {@code Set<Object>}, 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<TypeVariable<?>, 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
Expand Down Expand Up @@ -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<TypeVariable<?>, 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<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targetType,
Class<?> functionalInterface) {
Reference<Map<TypeVariable<?>, Type>> ref = TYPE_VARIABLE_CACHE.get(targetType);
Expand Down
Loading

0 comments on commit 15cf7a1

Please sign in to comment.