diff --git a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java index 58eef3c7c..3c3517121 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java @@ -18,6 +18,7 @@ import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; @@ -29,7 +30,11 @@ import java.util.Map; import java.util.Set; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.Builder; /** *

Utility methods focusing on type inspection, particularly with regard to @@ -40,6 +45,156 @@ */ public class TypeUtils { + /** + * {@link WildcardType} builder. + */ + public static class WildcardTypeBuilder implements Builder { + private WildcardTypeBuilder() { + } + + private Type[] upperBounds; + private Type[] lowerBounds; + + public WildcardTypeBuilder withUpperBounds(Type... bounds) { + this.upperBounds = bounds; + return this; + } + + public WildcardTypeBuilder withLowerBounds(Type... bounds) { + this.lowerBounds = bounds; + return this; + } + + @Override + public WildcardType build() { + return new WildcardTypeImpl(upperBounds, lowerBounds); + } + } + + private static final class GenericArrayTypeImpl implements GenericArrayType { + private final Type componentType; + + private GenericArrayTypeImpl(Type componentType) { + this.componentType = componentType; + } + + @Override + public Type getGenericComponentType() { + return componentType; + } + + @Override + public String toString() { + return TypeUtils.toString(this); + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj instanceof GenericArrayType && TypeUtils.equals(this, (GenericArrayType) obj); + } + + @Override + public int hashCode() { + int result = 67 << 4; + result |= componentType.hashCode(); + return result; + } + } + + private static final class ParameterizedTypeImpl implements ParameterizedType { + private final Class raw; + private final Type useOwner; + private final Type[] typeArguments; + + private ParameterizedTypeImpl(Class raw, Type useOwner, Type[] typeArguments) { + this.raw = raw; + this.useOwner = useOwner; + this.typeArguments = typeArguments; + } + + @Override + public Type getRawType() { + return raw; + } + + @Override + public Type getOwnerType() { + return useOwner; + } + + @Override + public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + @Override + public String toString() { + return TypeUtils.toString(this); + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj instanceof ParameterizedType && TypeUtils.equals(this, ((ParameterizedType) obj)); + } + + @Override + public int hashCode() { + int result = 71 << 4; + result |= raw.hashCode(); + result <<= 4; + result |= ObjectUtils.hashCode(useOwner); + result <<= 8; + result |= Arrays.hashCode(typeArguments); + return result; + } + } + + private static final class WildcardTypeImpl implements WildcardType { + private static final Type[] EMPTY_BOUNDS = new Type[0]; + + private final Type[] upperBounds; + private final Type[] lowerBounds; + + private WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { + this.upperBounds = ObjectUtils.defaultIfNull(upperBounds, EMPTY_BOUNDS); + this.lowerBounds = ObjectUtils.defaultIfNull(lowerBounds, EMPTY_BOUNDS); + } + + @Override + public Type[] getUpperBounds() { + return upperBounds.clone(); + } + + @Override + public Type[] getLowerBounds() { + return lowerBounds.clone(); + } + + @Override + public String toString() { + return TypeUtils.toString(this); + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj instanceof WildcardType && TypeUtils.equals(this, (WildcardType) obj); + } + + @Override + public int hashCode() { + int result = 73 << 8; + result |= Arrays.hashCode(upperBounds); + result <<= 8; + result |= Arrays.hashCode(lowerBounds); + return result; + } + } + + /** + * A wildcard instance matching {@code ?}. + */ + public static final WildcardType WILDCARD_ALL = wildcardType().withUpperBounds(Object.class).build(); + /** *

TypeUtils instances should NOT be constructed in standard * programming. Instead, the class should be used as @@ -1098,4 +1253,365 @@ public static Type getArrayComponentType(final Type type) { return null; } + /** + * Get a type representing {@code type} with variable assignments "unrolled." + * + * @param typeArguments as from {@link TypeUtils#getTypeArguments(Type, Class)} + * @param type + * @return Type + */ + public static Type unrollVariables(Map, Type> typeArguments, final Type type) { + if (containsTypeVariables(type)) { + if (type instanceof TypeVariable) { + return unrollVariables(typeArguments, typeArguments.get(type)); + } + if (type instanceof ParameterizedType) { + final ParameterizedType p = (ParameterizedType) type; + final Map, Type> parameterizedTypeArguments; + if (p.getOwnerType() == null) { + parameterizedTypeArguments = typeArguments; + } else { + parameterizedTypeArguments = new HashMap, Type>(typeArguments); + parameterizedTypeArguments.putAll(TypeUtils.getTypeArguments(p)); + } + final Type[] args = p.getActualTypeArguments(); + for (int i = 0; i < args.length; i++) { + final Type unrolled = unrollVariables(parameterizedTypeArguments, args[i]); + if (unrolled != null) { + args[i] = unrolled; + } + } + return parameterizeWithOwner(p.getOwnerType(), (Class) p.getRawType(), args); + } + if (type instanceof WildcardType) { + final WildcardType wild = (WildcardType) type; + return wildcardType().withUpperBounds(unrollBounds(typeArguments, wild.getUpperBounds())) + .withLowerBounds(unrollBounds(typeArguments, wild.getLowerBounds())).build(); + } + } + return type; + } + + private static Type[] unrollBounds(final Map, Type> typeArguments, final Type[] bounds) { + Type[] result = bounds; + int i = 0; + for (; i < result.length; i++) { + final Type unrolled = unrollVariables(typeArguments, result[i]); + if (unrolled == null) { + result = ArrayUtils.remove(result, i--); + } else { + result[i] = unrolled; + } + } + return result; + } + + /** + * Learn, recursively, whether any of the type parameters associated with {@code type} are bound to variables. + * + * @param type + * @return boolean + */ + public static boolean containsTypeVariables(Type type) { + if (type instanceof TypeVariable) { + return true; + } + if (type instanceof Class) { + return ((Class) type).getTypeParameters().length > 0; + } + if (type instanceof ParameterizedType) { + for (Type arg : ((ParameterizedType) type).getActualTypeArguments()) { + if (containsTypeVariables(arg)) { + return true; + } + } + return false; + } + if (type instanceof WildcardType) { + WildcardType wild = (WildcardType) type; + return containsTypeVariables(TypeUtils.getImplicitLowerBounds(wild)[0]) + || containsTypeVariables(TypeUtils.getImplicitUpperBounds(wild)[0]); + } + return false; + } + + /** + * Create a parameterized type instance. + * + * @param raw + * @param typeArguments + * @return {@link ParameterizedType} + */ + public static final ParameterizedType parameterize(final Class raw, final Type... typeArguments) { + return parameterizeWithOwner(null, raw, typeArguments); + } + + /** + * Create a parameterized type instance. + * + * @param raw + * @param typeArgMappings + * @return {@link ParameterizedType} + */ + public static final ParameterizedType parameterize(final Class raw, + final Map, Type> typeArgMappings) { + return parameterizeWithOwner(null, raw, extractTypeArgumentsFrom(typeArgMappings, raw.getTypeParameters())); + } + + /** + * Create a parameterized type instance. + * + * @param owner + * @param raw + * @param typeArguments + * + * @return {@link ParameterizedType} + */ + public static final ParameterizedType parameterizeWithOwner(final Type owner, final Class raw, + final Type... typeArguments) { + Validate.notNull(raw, "raw class"); + final Type useOwner; + if (raw.getEnclosingClass() == null) { + Validate.isTrue(owner == null, "no owner allowed for top-level %s", raw); + useOwner = null; + } else if (owner == null) { + useOwner = raw.getEnclosingClass(); + } else { + Validate.isTrue(TypeUtils.isAssignable(owner, raw.getEnclosingClass()), + "%s is invalid owner type for parameterized %s", owner, raw); + useOwner = owner; + } + Validate.isTrue(raw.getTypeParameters().length == Validate.noNullElements(typeArguments, + "null type argument at index %s").length); + + return new ParameterizedTypeImpl(raw, useOwner, typeArguments); + } + + /** + * Create a parameterized type instance. + * + * @param owner + * @param raw + * @param typeArgMappings + * @return {@link ParameterizedType} + */ + public static final ParameterizedType parameterizeWithOwner(final Type owner, final Class raw, + final Map, Type> typeArgMappings) { + return parameterizeWithOwner(owner, raw, extractTypeArgumentsFrom(typeArgMappings, raw.getTypeParameters())); + } + + private static Type[] extractTypeArgumentsFrom(Map, Type> mappings, TypeVariable[] variables) { + final Type[] result = new Type[variables.length]; + int index = 0; + for (TypeVariable var : variables) { + Validate.isTrue(mappings.containsKey(var), "missing argument mapping for %s", toString(var)); + result[index++] = mappings.get(var); + } + return result; + } + + /** + * Get a {@link WildcardTypeBuilder}. + * @return {@link WildcardTypeBuilder} + */ + public static WildcardTypeBuilder wildcardType() { + return new WildcardTypeBuilder(); + } + + /** + * Create a generic array type instance. + * + * @param componentType + * @return {@link GenericArrayType} + */ + public static GenericArrayType genericArrayType(final Type componentType) { + return new GenericArrayTypeImpl(componentType); + } + + /** + * Check equality of types. + * + * @param t1 + * @param t2 + * @return boolean + */ + public static boolean equals(Type t1, Type t2) { + if (ObjectUtils.equals(t1, t2)) { + return true; + } + if (t1 instanceof ParameterizedType) { + return equals((ParameterizedType) t1, t2); + } + if (t1 instanceof GenericArrayType) { + return equals((GenericArrayType) t1, t2); + } + if (t1 instanceof WildcardType) { + return equals((WildcardType) t1, t2); + } + return false; + } + + private static boolean equals(ParameterizedType p, Type t) { + if (t instanceof ParameterizedType) { + final ParameterizedType other = (ParameterizedType) t; + if (equals(p.getRawType(), other.getRawType()) && equals(p.getOwnerType(), other.getOwnerType())) { + return equals(p.getActualTypeArguments(), other.getActualTypeArguments()); + } + } + return false; + } + + private static boolean equals(GenericArrayType a, Type t) { + return t instanceof GenericArrayType + && equals(a.getGenericComponentType(), ((GenericArrayType) t).getGenericComponentType()); + } + + private static boolean equals(WildcardType w, Type t) { + if (t instanceof WildcardType) { + final WildcardType other = (WildcardType) t; + return equals(w.getLowerBounds(), other.getLowerBounds()) + && equals(TypeUtils.getImplicitUpperBounds(w), TypeUtils.getImplicitUpperBounds(other)); + } + return true; + } + + private static boolean equals(Type[] t1, Type[] t2) { + if (t1.length == t2.length) { + for (int i = 0; i < t1.length; i++) { + if (!equals(t1[i], t2[i])) { + return false; + } + } + return true; + } + return false; + } + + /** + * Present a given type as a Java-esque String. + * + * @param type + * @return String + */ + public static String toString(Type type) { + Validate.notNull(type); + if (type instanceof Class) { + return classToString((Class) type); + } + if (type instanceof ParameterizedType) { + return parameterizedTypeToString((ParameterizedType) type); + } + if (type instanceof WildcardType) { + return wildcardTypeToString((WildcardType) type); + } + if (type instanceof TypeVariable) { + return typeVariableToString((TypeVariable) type); + } + if (type instanceof GenericArrayType) { + return genericArrayTypeToString((GenericArrayType) type); + } + throw new IllegalArgumentException(ObjectUtils.identityToString(type)); + } + + /** + * Format a {@link TypeVariable} including its {@link GenericDeclaration}. + * + * @param var + * @return String + */ + public static String toLongString(TypeVariable var) { + final StringBuilder buf = new StringBuilder(); + final GenericDeclaration d = ((TypeVariable) var).getGenericDeclaration(); + if (d instanceof Class) { + Class c = (Class) d; + while (true) { + if (c.getEnclosingClass() == null) { + buf.insert(0, c.getName()); + break; + } + buf.insert(0, c.getSimpleName()).insert(0, '.'); + c = c.getEnclosingClass(); + } + } else if (d instanceof Type) {// not possible as of now + buf.append(toString((Type) d)); + } else { + buf.append(d); + } + return buf.append(':').append(typeVariableToString(var)).toString(); + } + + private static String classToString(Class c) { + final StringBuilder buf = new StringBuilder(); + + if (c.getEnclosingClass() != null) { + buf.append(classToString(c.getEnclosingClass())).append('.').append(c.getSimpleName()); + } else { + buf.append(c.getName()); + } + if (c.getTypeParameters().length > 0) { + buf.append('<'); + appendAllTo(buf, ", ", c.getTypeParameters()); + buf.append('>'); + } + return buf.toString(); + } + + private static String typeVariableToString(TypeVariable v) { + final StringBuilder buf = new StringBuilder(v.getName()); + final Type[] bounds = v.getBounds(); + if (bounds.length > 0 && !(bounds.length == 1 && Object.class.equals(bounds[0]))) { + buf.append(" extends "); + appendAllTo(buf, " & ", v.getBounds()); + } + return buf.toString(); + } + + private static String parameterizedTypeToString(ParameterizedType p) { + final StringBuilder buf = new StringBuilder(); + + final Type useOwner = p.getOwnerType(); + final Class raw = (Class) p.getRawType(); + final Type[] typeArguments = p.getActualTypeArguments(); + if (useOwner == null) { + buf.append(raw.getName()); + } else { + if (useOwner instanceof Class) { + buf.append(((Class) useOwner).getName()); + } else { + buf.append(useOwner.toString()); + } + buf.append('.').append(raw.getSimpleName()); + } + + appendAllTo(buf.append('<'), ", ", typeArguments).append('>'); + return buf.toString(); + } + + private static String wildcardTypeToString(WildcardType w) { + final StringBuilder buf = new StringBuilder().append('?'); + final Type[] lowerBounds = w.getLowerBounds(); + final Type[] upperBounds = w.getUpperBounds(); + if (lowerBounds.length > 0) { + appendAllTo(buf.append(" super "), " & ", lowerBounds); + } else if (!(upperBounds.length == 1 && Object.class.equals(upperBounds[0]))) { + appendAllTo(buf.append(" extends "), " & ", upperBounds); + } + return buf.toString(); + } + + private static String genericArrayTypeToString(GenericArrayType g) { + return String.format("%s[]", toString(g.getGenericComponentType())); + } + + private static StringBuilder appendAllTo(StringBuilder buf, String sep, Type... types) { + Validate.notEmpty(Validate.noNullElements(types)); + if (types.length > 0) { + buf.append(toString(types[0])); + for (int i = 1; i < types.length; i++) { + buf.append(sep).append(toString(types[i])); + } + } + return buf; + } + } diff --git a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java index e65ba73f4..dc8de61e0 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java @@ -17,19 +17,25 @@ package org.apache.commons.lang3.reflect; import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; 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.net.URI; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeSet; import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.reflect.TypeUtils.WildcardTypeBuilder; import org.apache.commons.lang3.reflect.testbed.Foo; import org.apache.commons.lang3.reflect.testbed.GenericParent; import org.apache.commons.lang3.reflect.testbed.GenericTypeHolder; @@ -41,7 +47,7 @@ * Test TypeUtils * @version $Id$ */ -@SuppressWarnings({ "unchecked", "unused" , "rawtypes", "null"}) +@SuppressWarnings({ "unchecked", "unused" , "rawtypes" }) //raw types, where used, are used purposely public class TypeUtilsTest { @@ -672,6 +678,44 @@ public void testLang820() throws Exception { Assert.assertArrayEquals(expectedArray, TypeUtils.normalizeUpperBounds(typeArray)); } + @Test + public void testParameterize() throws Exception { + final ParameterizedType stringComparableType = TypeUtils.parameterize(Comparable.class, String.class); + Assert.assertTrue(TypeUtils.equals(getClass().getField("stringComparable").getGenericType(), + stringComparableType)); + Assert.assertEquals("java.lang.Comparable", stringComparableType.toString()); + } + + @Test + public void testParameterizeWithOwner() throws Exception { + final Type owner = TypeUtils.parameterize(TypeUtilsTest.class, String.class); + final ParameterizedType dat2Type = TypeUtils.parameterizeWithOwner(owner, That.class, String.class, String.class); + Assert.assertTrue(TypeUtils.equals(getClass().getField("dat2").getGenericType(), dat2Type)); + } + + @Test + public void testWildcardType() throws Exception { + final WildcardType simpleWildcard = TypeUtils.wildcardType().withUpperBounds(String.class).build(); + final Field cClass = AClass.class.getField("cClass"); + Assert.assertTrue(TypeUtils.equals(((ParameterizedType) cClass.getGenericType()).getActualTypeArguments()[0], + simpleWildcard)); + } + + @Test + public void testGenericArrayType() throws Exception { + final Type expected = getClass().getField("intWildcardComparable").getGenericType(); + final GenericArrayType actual = + TypeUtils.genericArrayType(TypeUtils.parameterize(Comparable.class, TypeUtils.wildcardType() + .withUpperBounds(Integer.class).build())); + Assert.assertTrue(TypeUtils.equals(expected, actual)); + Assert.assertEquals("java.lang.Comparable[]", actual.toString()); + } + + @Test + public void testToLongString() { + Assert.assertEquals(getClass().getName() + ":B", TypeUtils.toLongString(getClass().getTypeParameters()[0])); + } + public Iterable>> iterable; public static > G stub() {