/***************************************************************************
* Copyright 2009-2012 by Christian Ihle *
* kontakt@usikkert.net *
* *
* This file is part of KouInject. *
* *
* KouInject is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 3 of *
* the License, or (at your option) any later version. *
* *
* KouInject is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with KouInject. *
* If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
package net.usikkert.kouinject.generics;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import org.apache.commons.lang.Validate;
/**
* Helper class for common operations on generics.
*
* @author Christian Ihle
*/
public final class GenericsHelper {
private GenericsHelper() {
// Static helpers only
}
/**
* Returns the generic argument used on the type, as a type.
*
* <p>Example: the type <code>Set<List<String>></code> would return <code>List<String></code>.</p>
*
* @param type The type to get the generic argument from. Supports only 1 generic argument.
* @return The argument of the type, as a type.
*/
public static Type getGenericArgumentAsType(final Type type) {
Validate.notNull(type, "Type can not be null");
final ParameterizedType parameterizedType = getAsParameterizedType(type);
final Type[] typeArguments = parameterizedType.getActualTypeArguments();
Validate.isTrue(typeArguments.length == 1, "Wrong number of type arguments (expected 1): " + type);
return typeArguments[0];
}
/**
* Returns the generic argument used on the type, as a class.
*
* <p>Example: the type <code>Set<List<String>></code> would return the class for <code>List</code>.</p>
*
* @param type The type to get the generic argument from. Supports only 1 generic argument.
* @return The argument of the type, as a class.
*/
public static Class<?> getGenericArgumentAsClass(final Type type) {
Validate.notNull(type, "Type can not be null");
final ParameterizedType parameterizedType = getAsParameterizedType(type);
final Type argument = getGenericArgumentAsType(parameterizedType);
return getAsClass(argument);
}
/**
* Returns the type as a class.
*
* <p>Example: the type <code>Set<List<String>></code> would return the class for <code>Set</code>.</p>
*
* @param type The type to get as a class.
* @return The type as a class.
* @throws IllegalArgumentException If the class could not be determined.
*/
public static Class<?> getAsClass(final Type type) {
final Class<?> classOrNull = getAsClassOrNull(type);
if (classOrNull == null) {
throw new IllegalArgumentException("Unsupported generic type: " + type.getClass() + " - " + type);
}
return classOrNull;
}
/**
* Returns the type as a class, if possible.
*
* <p>Example: the type <code>Set<List<String>></code> would return the class for <code>Set</code>.</p>
*
* @param type The type to get as a class.
* @return The type as a class, or <code>null</code> if the class could not be determined.
*/
public static Class<?> getAsClassOrNull(final Type type) {
Validate.notNull(type, "Type can not be null");
if (isClass(type)) {
return (Class<?>) type;
}
else if (isParameterizedType(type)) {
final ParameterizedType parameterizedType = getAsParameterizedType(type);
return (Class<?>) parameterizedType.getRawType();
}
else if (isGenericArrayType(type)) {
return getArrayClassFromGenericArray((GenericArrayType) type);
}
return null;
}
/**
* Gets the actual array class representing the generic array type.
*
* <p>Example: a generic array type for <code>String</code> would return <code>String[]</code>.
* A generic array type with a nested generic array type with a <code>String</code> would
* return <code>String[][]</code>.</p>
*
* <p>Returns <code>null</code> when the generic array type does not hold a class or another
* generic array type, like a <code>List<String>[]</code>.</p>
*
* @param genericArrayType The generic array type to get the array class from.
* @return The array class, or <code>null</code> if not found.
*/
public static Class<?> getArrayClassFromGenericArray(final GenericArrayType genericArrayType) {
Validate.notNull(genericArrayType, "Generic array type can not be null");
final Class<?> classFromGenericArray = getClassFromGenericArray(genericArrayType);
if (classFromGenericArray == null) {
return null;
}
final int dimensions = getArrayDimensions(genericArrayType);
// Can also use Class.forName(), but support for array classes seems to be ClassLoader dependent
return Array.newInstance(classFromGenericArray, new int[dimensions]).getClass();
}
/**
* Checks if the type is a class, as opposed to a type with generic parameters.
*
* <p>Example: <code>String</code> would return true, <code>List<String></code> would return false.</p>
*
* @param type The type to check.
* @return If the type is a regular class.
*/
public static boolean isClass(final Type type) {
return type instanceof Class;
}
/**
* Checks if the type has generic parameters, as opposed to a regular class.
*
* <p>Example: <code>String</code> would return false, <code>List<String></code> would return true.</p>
*
* @param type The type to check.
* @return If the type is parameterized.
*/
public static boolean isParameterizedType(final Type type) {
return type instanceof ParameterizedType;
}
/**
* Checks if the type is a type variable, as opposed to an actual type.
*
* <p>Example: <code>T</code> would return true, <code>Number</code> would return false.</p>
*
* @param type The type to check.
* @return If the type is a type variable.
*/
public static boolean isTypeVariable(final Type type) {
return type instanceof TypeVariable<?>;
}
/**
* Checks if the type is a wildcard, as opposed to a class or a type with generic parameters.
*
* <p>Example: <code>? extends Number</code> would return true, <code>Number</code> would return false.</p>
*
* @param type The type to check.
* @return If the type is a wildcard.
*/
public static boolean isWildcard(final Type type) {
return type instanceof WildcardType;
}
/**
* Checks if the type is a generic array type.
*
* <p>Generic array types appear when using arrays inside a generic type, or when the
* generic type itself is an array.</p>
*
* @param type The type to check.
* @return If the type is a generic array type.
*/
public static boolean isGenericArrayType(final Type type) {
return type instanceof GenericArrayType;
}
/**
* Determines if "this" type is either the same as, or a supertype of, "that" type.
*
* <p>The assignability logic is mostly based on the principle that if it compiles, it's assignable.
* Supports the following comparisons:</p>
*
* <ul>
* <li>Class vs class - Class 1 must be assignable from class 2.</li>
* <li>Array class vs array class - Array class 1 must be assignable from array class 2, and have the same dimensions.</li>
* <li>Parameterized type vs parameterized type - The raw class of type 1 must be assignable from the raw
* class of type 2, and all the parameters of both must be equal.</li>
* <li>Wildcard vs wildcard - The bounds of each wildcard must be assignable from each other.</li>
* <li>Wildcard vs class or parameterized type - The bounds of the wildcard must be assignable from the class or type.</li>
* <li>Class vs parameterized type - Class 1 must be assignable from the raw class of type 2.</li>
* </ul>
*
* <p>Does not support parameterized type vs class.</p>
*
* <p>Type variables in "that" will be automatically resolved to an actual type, if possible.
* "This" as a type variable is not automatically resolved, as it requires knowing the class the type belongs to.
* Can be solved with {@link #mapTypeVariablesToActualTypes(Class)} and
* {@link #wrapTypeAndReplaceTypeVariables(java.lang.reflect.Type, TypeMap)} before using this method.</p>
*
* @param thisType The left hand side, usually an injection point.
* @param thatType The right hand side, usually a bean.
* @return If "this" is assignable from "that".
*/
public static boolean isAssignableFrom(final Type thisType, final Type thatType) {
Validate.notNull(thisType, "This type can not be null");
Validate.notNull(thatType, "That type can not be null");
return isAssignableFromUsingMap(thisType, thatType, new TypeMap());
}
/**
* Creates a map with all the type variables used on the specified class as keys, and their actual types
* as values.
*
* <p>All super-classes and super-interfaces of the specified class are also searched. If no actual type is
* found for a type variable, then the value for that key will be <code>null</code>.</p>
*
* <p>Example:</p>
*
* <pre>
* class Basket<T> {}
* class FruitBasket extends Basket<Fruit> {}
* </pre>
*
* <p>If <code>FruitBasket.class</code> was the parameter, you would get a map with <code>T</code> as key,
* and <code>Fruit.class</code> as value. If <code>Basket.class</code> was the parameter then you would get
* a map with <code>T</code> as key, and <code>null</code> as value.
*
*
* @param aClass The class to search for type variables on.
* @return A map with the type variables and the actual types that was found on the specified class.
*/
public static TypeMap mapTypeVariablesToActualTypes(final Class<?> aClass) {
final TypeMap typeMap = new TypeMap();
mapTypeVariablesToActualTypes(aClass, typeMap);
return typeMap;
}
/**
* Takes a type, and replaces any type variables with an actual type from the type map.
*
* <p>Supports parameterized types, wildcards and generic array types. The replacement is recursive, so if the type
* variable is in a nested type then the whole hierarchy of types will be wrapped. If no actual type
* is found in the map, then the type variable is kept as is.</p>
*
* <p>Example:</p>
*
* <p>If type is <code>List<Basket<T>></code>, and map contains key <code>T</code> and
* value <code>Fruit.class</code>, then the wrapped type will be <code>List<Basket<Fruit>></code>.</p>
*
* @param type The type to wrap.
* @param typeMap The map containing the actual types to use when replacing type variables.
* @return A wrapped type with type variables replaced with actual types from the map.
* @see #mapTypeVariablesToActualTypes(Class)
*/
public static Type wrapTypeAndReplaceTypeVariables(final Type type, final TypeMap typeMap) {
if (isParameterizedType(type)) {
final ParameterizedType parameterizedType = getAsParameterizedType(type);
final Type[] wrappedArguments = wrapTypeParameters(parameterizedType.getActualTypeArguments(), typeMap);
final Class<?> asClass = getAsClass(type);
return new WrappedParameterizedType(asClass, wrappedArguments);
}
else if (isWildcard(type)) {
final WildcardType wildcardType = (WildcardType) type;
final Type[] wrappedUpperBounds = wrapTypeParameters(wildcardType.getUpperBounds(), typeMap);
final Type[] wrappedLowerBounds = wrapTypeParameters(wildcardType.getLowerBounds(), typeMap);
return new WrappedWildcardType(wrappedUpperBounds, wrappedLowerBounds);
}
else if (isGenericArrayType(type)) {
final GenericArrayType genericArrayType = (GenericArrayType) type;
final Type wrappedType = wrapTypeAndReplaceTypeVariables(genericArrayType.getGenericComponentType(), typeMap);
return new WrappedGenericArrayType(wrappedType);
}
else if (isTypeVariable(type)) {
final Type actualType = typeMap.getActualType((TypeVariable<?>) type);
if (actualType != null) {
return actualType;
}
}
return type;
}
private static boolean isAssignableFromUsingMap(final Type thisType, final Type thatType, final TypeMap typeMap) {
if (thisType.equals(thatType)) {
return true;
}
// To support wildcards in providers and collections
if (isWildcard(thisType)) {
return isAssignableFromWildcard((WildcardType) thisType, thatType, typeMap);
}
final Class<?> thisClass = getAsClassOrNull(thisType);
final Class<?> thatClass = getAsClassOrNull(thatType);
if (thisClass == null || thatClass == null) {
return false;
}
if (thisClass.isAssignableFrom(thatClass)) {
if (isClass(thisType) && isClass(thatType)) {
return true;
}
else if (isParameterizedType(thisType) && isParameterizedType(thatType)) {
mapTypeVariablesToActualTypes(thatType, thatClass, typeMap);
if (typesHaveTheSameParameters(thisType, thatType, typeMap)) {
return true;
}
}
// Assignment from generic type to raw type, e.g. List list = new ArrayList<String>();
else if (isClass(thisType) && isParameterizedType(thatType)) {
return true;
}
// To support arrays in providers and collections - the assignment check on the classes
// above makes sure that both are compatible arrays
else if (isGenericArrayType(thisType) && isClass(thatType)) {
return true;
}
// Assigning from a class that implements a generic interface or class to a generic type
if (isAssignableFromSuperTypes(thisType, thatClass, typeMap)) {
return true;
}
}
return false;
}
private static boolean isAssignableFromSuperTypes(final Type thisType, final Class<?> thatClass, final TypeMap typeMap) {
return isAssignableFromSuperInterfaces(thisType, thatClass, typeMap)
|| isAssignableFromSuperClass(thisType, thatClass, typeMap);
}
private static boolean isAssignableFromSuperInterfaces(final Type thisType, final Class<?> thatClass, final TypeMap typeMap) {
final Type[] genericInterfaces = thatClass.getGenericInterfaces();
for (final Type genericInterface : genericInterfaces) {
if (isAssignableFromUsingMap(thisType, genericInterface, typeMap)) {
return true;
}
}
return false;
}
private static boolean isAssignableFromSuperClass(final Type thisType, final Class<?> thatClass, final TypeMap typeMap) {
final Type genericSuperclass = thatClass.getGenericSuperclass();
return genericSuperclass != null && isAssignableFromUsingMap(thisType, genericSuperclass, typeMap);
}
private static boolean typesHaveTheSameParameters(final Type thisType, final Type thatType, final TypeMap typeMap) {
final Type[] thisArguments = getGenericArgumentsAsType(thisType);
final Type[] thatArguments = getGenericArgumentsAsType(thatType);
if (thisArguments.length != thatArguments.length) {
return false;
}
for (int i = 0; i < thisArguments.length; i++) {
final Type thisArgument = thisArguments[i];
final Type thatArgument = thatArguments[i];
if (!typesHaveTheSameParameter(thisArgument, thatArgument, typeMap)) {
return false;
}
}
return true;
}
private static Type[] getGenericArgumentsAsType(final Type type) {
final ParameterizedType parameterizedType = getAsParameterizedType(type);
return parameterizedType.getActualTypeArguments();
}
private static boolean typesHaveTheSameParameter(final Type thisArgument, final Type thatArgument, final TypeMap typeMap) {
if (thisArgument.equals(thatArgument)) {
return true;
}
if (isWildcard(thisArgument)) {
if (isWildcard(thatArgument)) {
return wildcardsAreAssignable((WildcardType) thisArgument, (WildcardType) thatArgument, typeMap);
}
else {
return isAssignableFromWildcard((WildcardType) thisArgument, thatArgument, typeMap);
}
}
else if (isTypeVariable(thatArgument)) {
final TypeVariable<?> thatTypeVariable = (TypeVariable<?>) thatArgument;
final Type thatResolvedType = typeMap.getActualType(thatTypeVariable);
return typesHaveTheSameParameter(thisArgument, thatResolvedType, typeMap);
}
// This only happens with Java 7.
// In Java 6 you will always get two GenericArrayTypes, and the match will happen on an earlier equals.
else if (isClass(thisArgument) && isGenericArrayType(thatArgument)) {
final Class<?> arrayClassFromGenericArray = getArrayClassFromGenericArray((GenericArrayType) thatArgument);
return thisArgument.equals(arrayClassFromGenericArray);
}
return false;
}
private static boolean isAssignableFromWildcard(final WildcardType thisWildcard, final Type thatType, final TypeMap typeMap) {
final Type[] upperBounds = thisWildcard.getUpperBounds();
final Type[] lowerBounds = thisWildcard.getLowerBounds();
for (final Type upperBound : upperBounds) {
if (!isAssignableFromUsingMap(upperBound, thatType, typeMap)) {
return false;
}
}
for (final Type lowerBound : lowerBounds) {
if (!isAssignableFromUsingMap(thatType, lowerBound, typeMap)) {
return false;
}
}
return true;
}
private static boolean wildcardsAreAssignable(final WildcardType thisWildcard, final WildcardType thatWildcard,
final TypeMap typeMap) {
final Type[] thisWildcardUpperBounds = thisWildcard.getUpperBounds();
final Type[] thisWildcardLowerBounds = thisWildcard.getLowerBounds();
final Type[] thatWildcardUpperBounds = thatWildcard.getUpperBounds();
final Type[] thatWildcardLowerBounds = thatWildcard.getLowerBounds();
if (thisWildcardLowerBounds.length != thatWildcardLowerBounds.length
|| thisWildcardUpperBounds.length != thatWildcardUpperBounds.length) {
return false;
}
for (int i = 0; i < thisWildcardUpperBounds.length; i++) {
final Type thisWildcardUpperBound = thisWildcardUpperBounds[i];
final Type thatWildcardUpperBound = thatWildcardUpperBounds[i];
if (!isAssignableFromUsingMap(thisWildcardUpperBound, thatWildcardUpperBound, typeMap)) {
return false;
}
}
for (int i = 0; i < thisWildcardLowerBounds.length; i++) {
final Type thisWildcardLowerBound = thisWildcardLowerBounds[i];
final Type thatWildcardLowerBound = thatWildcardLowerBounds[i];
if (!isAssignableFromUsingMap(thatWildcardLowerBound, thisWildcardLowerBound, typeMap)) {
return false;
}
}
return true;
}
private static ParameterizedType getAsParameterizedType(final Type type) {
if (isClass(type)) {
throw new IllegalArgumentException("Generic type <T> is required: " + type);
}
else if (isParameterizedType(type)) {
return (ParameterizedType) type;
}
throw new IllegalArgumentException("Unsupported generic type: " + type);
}
private static void mapTypeVariablesToActualTypes(final Type type, final TypeMap typeMap) {
final Class<?> asClass = getAsClass(type);
if (isParameterizedType(type)) {
mapTypeVariablesToActualTypes(type, asClass, typeMap);
}
final Type[] genericInterfaces = asClass.getGenericInterfaces();
for (final Type genericInterface : genericInterfaces) {
mapTypeVariablesToActualTypes(genericInterface, typeMap);
}
final Type genericSuperclass = asClass.getGenericSuperclass();
if (genericSuperclass != null) {
mapTypeVariablesToActualTypes(genericSuperclass, typeMap);
}
}
private static void mapTypeVariablesToActualTypes(final Type thatType, final Class<?> thatClass, final TypeMap typeMap) {
final ParameterizedType thatParameterizedType = getAsParameterizedType(thatType);
final Type[] actualTypeArguments = thatParameterizedType.getActualTypeArguments();
final TypeVariable<?>[] typeParameters = thatClass.getTypeParameters();
for (int i = 0; i < typeParameters.length; i++) {
final TypeVariable<?> typeParameter = typeParameters[i];
final Type actualTypeArgument = actualTypeArguments[i];
typeMap.addActualType(typeParameter, actualTypeArgument);
}
}
private static Type[] wrapTypeParameters(final Type[] parameters, final TypeMap typeMap) {
final Type[] wrappedParameters = new Type[parameters.length];
for (int i = 0; i < parameters.length; i++) {
wrappedParameters[i] = wrapTypeAndReplaceTypeVariables(parameters[i], typeMap);
}
return wrappedParameters;
}
private static int getArrayDimensions(final GenericArrayType genericArrayType) {
int dimensions = 1;
Type innerType = genericArrayType.getGenericComponentType();
while (isGenericArrayType(innerType)) {
final GenericArrayType innerArray = (GenericArrayType) innerType;
innerType = innerArray.getGenericComponentType();
dimensions++;
}
return dimensions;
}
private static Class<?> getClassFromGenericArray(final GenericArrayType genericArrayType) {
Type innerType = genericArrayType.getGenericComponentType();
while (isGenericArrayType(innerType)) {
final GenericArrayType innerArray = (GenericArrayType) innerType;
innerType = innerArray.getGenericComponentType();
}
if (isClass(innerType)) {
return (Class<?>) innerType;
}
return null;
}
}