Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reification #46

Merged
merged 1 commit into from
Dec 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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