/*
* Beanfabrics Framework Copyright (C) by Michael Karneim, beanfabrics.org
* Use is subject to license terms. See license.txt.
*/
package org.beanfabrics.util;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.List;
/**
* The {@link GenericType} is a facade for a {@link Type} object representing a
* Java class. It provides convenient functions for generic type reflection.
*
* @author Michael Karneim
*/
public class GenericType {
private final VariableMapping mapping;
private final Type type;
/**
* Creates a {@link GenericType} with the given mapping for the given
* parameterized type.
*
* @param aMapping
* @param aParameterizedType
*/
private GenericType(VariableMapping aMapping, ParameterizedType aParameterizedType) {
this.mapping = aMapping;
this.type = aParameterizedType;
}
/**
* Creates a {@link GenericType} with the given mapping for the given class.
*
* @param aMapping
* @param aClass
*/
private GenericType(VariableMapping aMapping, Class aClass) {
this.mapping = aMapping;
this.type = aClass;
}
/**
* Creates a {@link GenericType} with the given mapping for the given type
* variable.
*
* @param aMapping
* @param aTypeVariable
*/
private GenericType(VariableMapping aMapping, TypeVariable<?> aTypeVariable) {
this.mapping = aMapping;
this.type = aMapping.tryResolve((TypeVariable<?>) aTypeVariable);
}
/**
* Creates a {@link GenericType} for the given {@link Class}.
*
* @param cls
*/
public GenericType(Class<?> cls) {
this(new VariableMapping(null), cls);
}
/**
* Returns the type represented by this {@link GenericType}. This can be one
* of {@link Class}, {@link ParameterizedType}, and {@link TypeVariable}.
*
* @return the type represented by this {@link GenericType}.
*/
public Type getType() {
return type;
}
/**
* Returns <code>true</code> if this type represents a {@link TypeVariable}.
*
* @return <code>true</code> if this type represents a {@link TypeVariable}
*/
public boolean isTypeVariable() {
return type instanceof TypeVariable;
}
/**
* Returns <code>true</code> if this type represents a {@link Class}.
*
* @return <code>true</code> if this type represents a {@link Class}
*/
public boolean isClass() {
return type instanceof Class;
}
/**
* Returns the class that this type represents.
*
* @return the class this type represents
* @throws IllegalStateException
* if this type does not represent a class
*/
public Class<?> asClass() throws IllegalStateException {
if (isClass()) {
return (Class<?>) type;
}
if (isParameterizedType()) {
return (Class<?>) ((ParameterizedType) type).getRawType();
}
throw new IllegalStateException(String.format("Can't cast type %s to Class!", type));
}
/**
* Returns <code>true</code> if this type represents a
* {@link ParameterizedType}.
*
* @return <code>true</code> if this type represents a
* {@link ParameterizedType}
*/
public boolean isParameterizedType() {
return type instanceof ParameterizedType;
}
/**
* Returns <code>true</code> if this type represents a {@link WildcardType}.
*
* @return <code>true</code> if this type represents a {@link WildcardType}
*/
public boolean isWildcardType() {
return type instanceof WildcardType;
}
/**
* Returns the value of the given {@link TypeVariable} as a
* {@link GenericType}.
*
* @param variable
* must parameterize this type or one of it's super types
* @return the actual value of the given {@link TypeVariable} as a
* {@link GenericType}
* @throws IllegalArgumentException
* if the variable does not parameterize this type
* @throws UnsupportedOperationException
* if the type parameter resolution is not supported for this
* type
*/
public GenericType getTypeParameter(TypeVariable<? extends Class<?>> variable) throws IllegalArgumentException,
IllegalStateException {
assertThatTypeVariableDoesParameterizeAClass(variable);
GenericType result = null;
if (type instanceof Class<?>) {
Class<?> cls = (Class<?>) type;
// Class<?> parameterizedClass = (Class<?>)
// variable.getGenericDeclaration();
// // Break-Condition
// if (parameterizedClass.equals(cls)) {
// GenericType result = new GenericType(mapping, variable);
// return result;
// }
VariableMapping aMapping = resolveTypeVariableInClass(variable, cls, mapping);
if (aMapping != null) {
result = getGenericType(aMapping, variable);
}
} else if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
Class<?> rawType = (Class<?>) pType.getRawType();
VariableMapping aMapping = resolveTypeVariableInClass(variable, rawType, mapping);
if (aMapping != null) {
result = getGenericType(aMapping, variable);
}
} else {
throw new UnsupportedOperationException(String.format("Type parameter resolution not supported for %s",
type));
}
if (result == null) {
throw new IllegalArgumentException(String.format("%s does not parameterize this type %s!", variable, type));
} else {
return result;
}
}
/**
* Returns the type of the field with the given name as a
* {@link GenericType}.
*
* @param fieldname
* must denote a field in this type or one of it's super types.
* @return the type of the field with the given name as a
* {@link GenericType}
* @throws UnsupportedOperationException
* if field type resolution is not supported for this type
*/
public GenericType getFieldType(String fieldname) {
if (type instanceof ParameterizedType) {
Class<?> rawType = (Class<?>) ((ParameterizedType) type).getRawType();
GenericType result = getFieldType(mapping, rawType, fieldname);
return result;
} else if (type instanceof Class<?>) {
Class<?> rawType = (Class<?>) type;
GenericType result = getFieldType(mapping, rawType, fieldname);
return result;
} else {
throw new UnsupportedOperationException(String.format("field type resolution not supported for %s", type));
}
}
/**
* Returns the return type of the method with the given name (having no
* parameters) as a {@link GenericType}.
* <p>
* <b>Please note:</b><br>
* Only methods <b>without parameters</b> are supported right now!
* </p>
*
* @param methodName
* @return the return type of the method as a {@link GenericType}.
* @throws UnsupportedOperationException
* if method return type resolution is not supported for this
* type
*/
public GenericType getMethodReturnType(String methodName) {
if (type instanceof ParameterizedType) {
Class<?> rawType = (Class<?>) ((ParameterizedType) type).getRawType();
GenericType result = getMethodReturnType(mapping, rawType, methodName);
return result;
} else if (type instanceof Class<?>) {
Class<?> rawType = (Class<?>) type;
GenericType result = getMethodReturnType(mapping, rawType, methodName);
return result;
} else {
throw new UnsupportedOperationException(String.format("method return type resolution not supported for %s",
type));
}
}
public Type tryResolve(Type type) {
if (type instanceof TypeVariable<?>) {
return tryResolve((TypeVariable<?>) type);
} else {
return type;
}
}
public Type tryResolve(TypeVariable<?> var) {
Type resolvedType = mapping.tryResolve(var);
return resolvedType;
}
public Type narrow(Type aType, Class<?> targetType) {
if (aType == null) {
return null;
}
aType = tryResolve(aType);
if (aType instanceof Class<?>) {
return (Class<?>) aType;
} else if (aType instanceof ParameterizedType) {
return ((ParameterizedType) aType).getRawType();
} else if (aType instanceof TypeVariable<?> || aType instanceof WildcardType) {
Type[] bounds;
if (aType instanceof WildcardType) {
bounds = ((WildcardType) aType).getUpperBounds();
} else {
bounds = ((TypeVariable<?>) aType).getBounds();
}
Class<?> result = null;
for (Type bound : bounds) {
if (bound instanceof TypeVariable<?>) {
Type resolvedBound = tryResolve((TypeVariable<?>) bound);
if (resolvedBound != null) {
bound = resolvedBound;
}
}
bound = narrow(bound, targetType);
if (bound instanceof Class<?>) {
Class<?> cls = (Class<?>) bound;
if (targetType != null) {
if (targetType.isAssignableFrom(cls)) {
if (result == null || (result != null && result.isAssignableFrom(cls))) {
result = cls;
}
}
} else {
if (result == null || result.isAssignableFrom(cls)) {
result = cls;
}
}
}
}
if (result != null) {
return result;
} else {
return aType;
}
} else {
throw new IllegalStateException("Not Supported: " + aType.getClass());
}
}
/**
* Resolves the variable mapping for the given type variable in the given
* class (or its superclass hierarchy).
*
* @param var
* @param inClass
* @param currentMapping
* @return
*/
private static VariableMapping resolveTypeVariableInClass(TypeVariable<?> var, Class<?> inClass,
final VariableMapping currentMapping) {
assertThatTypeVariableDoesParameterizeAClass(var);
Class<?> parameterizedClass = (Class<?>) var.getGenericDeclaration();
// Break-Condition 1
if (parameterizedClass.equals(inClass)) {
return currentMapping;
}
// if the parameterized class is a super type of forClass
if (parameterizedClass.isAssignableFrom(inClass)) {
// process Superclass
{
Type inSuperType = inClass.getGenericSuperclass();
if (inSuperType != null && isAssignableFrom(parameterizedClass, inSuperType)) {
VariableMapping result = resolveTypeVariableInType(var, inSuperType, currentMapping);
if (result != null) {
return result;
}
}
}
// process Superinterfaces
Type[] superInterfaceTypes = inClass.getGenericInterfaces();
for (Type inSuperType : superInterfaceTypes) {
if (isAssignableFrom(parameterizedClass, inSuperType)) {
VariableMapping result = resolveTypeVariableInType(var, inSuperType, currentMapping);
if (result != null) {
return result;
}
}
}
}
// Break-Condition 2
// the given variable does not parameterize the given class
return null;
}
private static void assertThatTypeVariableDoesParameterizeAClass(TypeVariable<?> var) {
if (!(var.getGenericDeclaration() instanceof Class<?>)) {
throw new IllegalArgumentException(String.format(
"Type parameters are supported only for classes, but %s is a declared for %s", var,
var.getGenericDeclaration()));
}
}
private static VariableMapping resolveTypeVariableInParameterizedType(TypeVariable<?> var,
ParameterizedType inParameterizedType, VariableMapping currentMapping) {
assertThatTypeVariableDoesParameterizeAClass(var);
currentMapping = new VariableMapping(currentMapping, inParameterizedType);
Class<?> inClass = (Class<?>) inParameterizedType.getRawType();
return resolveTypeVariableInClass(var, inClass, currentMapping);
}
private static boolean isAssignableFrom(Class<?> aClass, Type aType) {
if (aType == null) {
return false;
}
if (aType instanceof Class<?>) {
return aClass.isAssignableFrom((Class<?>) aType);
} else if (aType instanceof ParameterizedType) {
return aClass.isAssignableFrom((Class<?>) ((ParameterizedType) aType).getRawType());
} else {
throw new UnsupportedOperationException("aType=" + aType.getClass().getName());
}
}
private static VariableMapping resolveTypeVariableInType(TypeVariable<?> var, Type inType,
final VariableMapping currentMapping) throws AssertionError {
assertThatTypeVariableDoesParameterizeAClass(var);
if (inType instanceof Class<?>) {
VariableMapping result = resolveTypeVariableInClass(var, (Class<?>) inType, currentMapping);
return result;
} else if (inType instanceof ParameterizedType) {
ParameterizedType inParameterizedType = (ParameterizedType) inType;
VariableMapping result = resolveTypeVariableInParameterizedType(var, inParameterizedType, currentMapping);
return result;
} else {
throw new AssertionError("Unexpected type: " + inType);
}
}
private static GenericType getFieldType(VariableMapping currentMapping, Class<?> startClass, String fieldname) {
Type type = null;
Field field = null; // e.g. BClass<String, Integer> bObject;
{
Class<?> ownerClass = startClass;
findfield: while (true) {
Field[] fields = ownerClass.getDeclaredFields();
for (Field f : fields) {
if (f.getName().equals(fieldname)) {
field = f;
break findfield;
}
}
// field not found so far
// -> search superclass
Type superType = ownerClass.getGenericSuperclass();
if (superType instanceof ParameterizedType) {
ParameterizedType parameterizedSuperType = (ParameterizedType) superType;
currentMapping = new VariableMapping(currentMapping, parameterizedSuperType);
ownerClass = (Class<?>) parameterizedSuperType.getRawType();
} else if (superType instanceof Class<?>) {
ownerClass = (Class<?>) superType;
} else {
throw new IllegalStateException();
}
}
}
type = field.getGenericType(); // e.g. BClass<String, Integer>
return getGenericType(currentMapping, type);
}
private static GenericType getMethodReturnType(VariableMapping currentMapping, Class<?> startClass,
String methodName) {
Type type = null;
Method method = null;
{
Class<?> ownerClass = startClass;
findmethod: while (true) {
Method[] methods = ownerClass.getDeclaredMethods();
// List<Method> ma = toList(methods);
// Collections.reverse(ma);
// methods = ma.toArray(new Method[]{});
for (Method m : methods) {
if (m.getName().equals(methodName)
&& (m.getParameterTypes() == null || m.getParameterTypes().length == 0)) {
if (method == null || isMoreSpecific(m, method)) {
method = m;
}
}
}
if (method != null) {
break findmethod;
}
// method not found so far.
// -> search superclass
Type superType = ownerClass.getGenericSuperclass();
if (superType == null) {
// method not found
throw new IllegalArgumentException(String.format("Method %s() not found in type %s!", methodName,
startClass));
} else if (superType instanceof ParameterizedType) {
ParameterizedType parameterizedSuperType = (ParameterizedType) superType;
currentMapping = new VariableMapping(currentMapping, parameterizedSuperType);
ownerClass = (Class<?>) parameterizedSuperType.getRawType();
} else if (superType instanceof Class<?>) {
ownerClass = (Class<?>) superType;
} else {
throw new IllegalStateException(String.format("%s", superType));
}
}
}
type = method.getGenericReturnType();
return getGenericType(currentMapping, type);
}
private static <T> List<T> toList(T... objs) {
List<T> result = new ArrayList<T>(objs.length);
for (T obj : objs) {
result.add(obj);
}
return result;
}
/**
* Returns <code>true</code> if method1 is more specific than method2.
* <p>
* The informal intuition is that one method is more specific than another
* if any invocation handled by the first method could be passed on to the
* other one without a compile-time type error. More precisely, if the
* parameter types of method1 are M1[1] to M1[n] and parameter types of
* method2 are M2[1] to M2[n] method1 is more specific then method2 if M1[j]
* can be converted to M2[j] for all j from 1 to n by method invocation
* conversion.
*
* @param method1
* @param method2
* @return <code>true</code> if method m is more specific that the other
* method
* @see <pre>
* <a
* href="http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12">The Java Language Specification (JLS) in section 15.12 Method Invocation Expression</a>
* </pre>
*/
private static boolean isMoreSpecific(Method method1, Method method2) {
if (namesAreEqual(method1, method2) && returnTypeIsConvertible(method1, method2)
&& parameterTypesAreConvertible(method1, method2)) {
return true;
} else {
return false;
}
}
private static boolean parameterTypesAreConvertible(Method method1, Method method2) {
Class<?>[] p1 = method1.getParameterTypes();
Class<?>[] p2 = method2.getParameterTypes();
if (p1.length != p2.length) {
// TODO check if we have to allow unequal parameter numbers, e.g.
// when using ellipsis
return false;
}
int len = p1.length;
for (int i = 0; i < len; ++i) {
if (!isConvertible(p1[i], p2[i])) {
return false;
}
}
return true;
}
/**
* Returns <code>true</code> if type1 is convertible to type2.
*
* @param type1
* @param type2
* @return <code>true</code> if type1 is convertible to type2
*/
private static boolean isConvertible(Class<?> type1, Class<?> type2) {
return type2.isAssignableFrom(type1);
}
private static boolean returnTypeIsConvertible(Method method1, Method method2) {
return isConvertible(method1.getReturnType(), method2.getReturnType());
}
private static boolean namesAreEqual(Method method1, Method method2) {
return method1.getName().equals(method2.getName());
}
private static GenericType getGenericType(VariableMapping currentMapping, Type type) {
// try to resolve type
if (type == null) {
throw new IllegalArgumentException("Type must not be null!");
}
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
currentMapping = new VariableMapping(currentMapping, parameterizedType);
// e.g. <X1, X2 extends Number> => <String, Integer>
return new GenericType(currentMapping, parameterizedType);
} else if (type instanceof Class<?>) {
return new GenericType(currentMapping, (Class) type);
} else if (type instanceof TypeVariable<?>) {
return new GenericType(currentMapping, (TypeVariable<?>) type);
} else {
throw new IllegalStateException("Type not supported so far: " + type.getClass().getName());
}
}
}