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 92dfff7dae1..4c8491efd4a 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -235,8 +236,8 @@ private static final class WildcardTypeImpl implements WildcardType { * @param lowerBounds of this type. */ private WildcardTypeImpl(final Type[] upperBounds, final Type[] lowerBounds) { - this.upperBounds = ObjectUtils.getIfNull(upperBounds, ArrayUtils.EMPTY_TYPE_ARRAY); - this.lowerBounds = ObjectUtils.getIfNull(lowerBounds, ArrayUtils.EMPTY_TYPE_ARRAY); + this.upperBounds = upperBounds != null ? upperBounds.clone() : ArrayUtils.EMPTY_TYPE_ARRAY; + this.lowerBounds = lowerBounds != null ? lowerBounds.clone() : ArrayUtils.EMPTY_TYPE_ARRAY; } /** @@ -290,7 +291,7 @@ public String toString() { // @formatter:off private static final AppendableJoiner AMP_JOINER = AppendableJoiner.builder() .setDelimiter(" & ") - .setElementAppender((a, e) -> a.append(toString(e))) + .setElementAppender((a, e) -> a.append(toReferenceString(e))) .get(); // @formatter:on @@ -316,6 +317,18 @@ public String toString() { .get(); // @formatter:on + /** + * Type arguments joiner. + */ + // @formatter:off + private static final AppendableJoiner TYPE_ARG_JOINER = AppendableJoiner.builder() + .setPrefix("<") + .setSuffix(">") + .setDelimiter(", ") + .setElementAppender((a, e) -> a.append(toReferenceString(e))) + .get(); + // @formatter:on + /** * A wildcard instance matching {@code ?}. * @@ -327,17 +340,20 @@ private static String anyToString(final T object) { return object instanceof Type ? toString((Type) object) : object.toString(); } - private static void appendRecursiveTypes(final StringBuilder builder, final int[] recursiveTypeIndexes, final Type[] argumentTypes) { - for (final Type type : argumentTypes) { - // toString() or you get a SO - GT_JOINER.join(builder, Objects.toString(type)); - } - final Type[] argumentsFiltered = ArrayUtils.removeAll(argumentTypes, recursiveTypeIndexes); - if (argumentsFiltered.length > 0) { - GT_JOINER.join(builder, (Object[]) argumentsFiltered); + /** + * Formats a {@link Type} as a type reference string (type variables are formatted by name only without bounds). + * + * @param type The type to format. + * @return String. + */ + private static String toReferenceString(final Type type) { + if (type instanceof TypeVariable) { + return ((TypeVariable) type).getName(); } + return toString(type); } + /** * Formats a {@link Class} as a {@link String}. * @@ -387,7 +403,17 @@ public static boolean containsTypeVariables(final Type type) { } if (type instanceof WildcardType) { final WildcardType wild = (WildcardType) type; - return containsTypeVariables(getImplicitLowerBounds(wild)[0]) || containsTypeVariables(getImplicitUpperBounds(wild)[0]); + for (final Type bound : getImplicitLowerBounds(wild)) { + if (containsTypeVariables(bound)) { + return true; + } + } + for (final Type bound : getImplicitUpperBounds(wild)) { + if (containsTypeVariables(bound)) { + return true; + } + } + return false; } if (type instanceof GenericArrayType) { return containsTypeVariables(((GenericArrayType) type).getGenericComponentType()); @@ -395,10 +421,6 @@ public static boolean containsTypeVariables(final Type type) { return false; } - private static boolean containsVariableTypeSameParametrizedTypeBound(final TypeVariable typeVariable, final ParameterizedType parameterizedType) { - return ArrayUtils.contains(typeVariable.getBounds(), parameterizedType); - } - /** * Tries to determine the type arguments of a class/interface based on a super parameterized type's type arguments. This method is the inverse of * {@link #getTypeArguments(Type, Class)} which gets a class/interface's type arguments based on a subtype. It is far more limited in determining the type @@ -551,17 +573,6 @@ private static Type[] extractTypeArgumentsFrom(final Map, Type> return result; } - private static int[] findRecursiveTypes(final ParameterizedType parameterizedType) { - final Type[] filteredArgumentTypes = Arrays.copyOf(parameterizedType.getActualTypeArguments(), parameterizedType.getActualTypeArguments().length); - int[] indexesToRemove = {}; - for (int i = 0; i < filteredArgumentTypes.length; i++) { - if (filteredArgumentTypes[i] instanceof TypeVariable - && containsVariableTypeSameParametrizedTypeBound((TypeVariable) filteredArgumentTypes[i], parameterizedType)) { - indexesToRemove = ArrayUtils.add(indexesToRemove, i); - } - } - return indexesToRemove; - } /** * Creates a generic array type instance. @@ -581,7 +592,7 @@ public static GenericArrayType genericArrayType(final Type componentType) { * @return String. */ private static String genericArrayTypeToString(final GenericArrayType genericArrayType) { - return String.format("%s[]", toString(genericArrayType.getGenericComponentType())); + return String.format("%s[]", toReferenceString(genericArrayType.getGenericComponentType())); } /** @@ -1446,15 +1457,13 @@ private static String parameterizedTypeToString(final ParameterizedType paramete if (useOwner instanceof Class) { builder.append(((Class) useOwner).getName()); } else { - builder.append(useOwner); + builder.append(toString(useOwner)); } builder.append('.').append(raw.getSimpleName()); } - final int[] recursiveTypeIndexes = findRecursiveTypes(parameterizedType); - if (recursiveTypeIndexes.length > 0) { - appendRecursiveTypes(builder, recursiveTypeIndexes, parameterizedType.getActualTypeArguments()); - } else { - GT_JOINER.join(builder, (Object[]) parameterizedType.getActualTypeArguments()); + final Type[] typeArguments = parameterizedType.getActualTypeArguments(); + if (typeArguments.length > 0) { + TYPE_ARG_JOINER.join(builder, typeArguments); } return builder.toString(); } @@ -1549,7 +1558,31 @@ public static String toLongString(final TypeVariable typeVariable) { } else { buf.append(d); } - return buf.append(':').append(typeVariableToString(typeVariable)).toString(); + return buf.append(':').append(toString(typeVariable)).toString(); + } + + private static final ThreadLocal> VISITING = ThreadLocal.withInitial(() -> Collections.newSetFromMap(new IdentityHashMap<>())); + + private static String toCyclicString(final Type type) { + if (type instanceof Class) { + return ((Class) type).getSimpleName() + "(cycle)"; + } + if (type instanceof TypeVariable) { + return ((TypeVariable) type).getName() + "(cycle)"; + } + if (type instanceof WildcardType) { + return "? (cycle)"; + } + if (type instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) type; + final Type raw = pt.getRawType(); + final String rawName = raw instanceof Class ? ((Class) raw).getSimpleName() : raw.getTypeName(); + return rawName + "(cycle)"; + } + if (type instanceof GenericArrayType) { + return "(cycle)"; + } + return ObjectUtils.identityToString(type) + "(cycle)"; } /** @@ -1562,22 +1595,33 @@ public static String toLongString(final TypeVariable typeVariable) { */ public static String toString(final Type type) { Objects.requireNonNull(type, "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); + final Set visiting = VISITING.get(); + if (!visiting.add(type)) { + return toCyclicString(type); } - if (type instanceof TypeVariable) { - return typeVariableToString((TypeVariable) type); - } - if (type instanceof GenericArrayType) { - return genericArrayTypeToString((GenericArrayType) type); + try { + 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)); + } finally { + visiting.remove(type); + if (visiting.isEmpty()) { + VISITING.remove(); + } } - throw new IllegalArgumentException(ObjectUtils.identityToString(type)); } /** @@ -1615,22 +1659,8 @@ private static String typeVariableToString(final TypeVariable typeVariable) { final StringBuilder builder = new StringBuilder(typeVariable.getName()); final Type[] bounds = typeVariable.getBounds(); if (bounds.length > 0 && !(bounds.length == 1 && Object.class.equals(bounds[0]))) { - // https://issues.apache.org/jira/projects/LANG/issues/LANG-1698 - // There must be a better way to avoid a stack overflow on Java 17 and up. - // Bounds are different in Java 17 and up where instead of Object you can get an interface like Comparable. - final Type bound = bounds[0]; - boolean append = true; - if (bound instanceof ParameterizedType) { - final Type rawType = ((ParameterizedType) bound).getRawType(); - if (rawType instanceof Class && ((Class) rawType).isInterface()) { - // Avoid recursion and stack overflow on Java 17 and up. - append = false; - } - } - if (append) { - builder.append(" extends "); - AMP_JOINER.join(builder, bounds); - } + builder.append(" extends "); + AMP_JOINER.join(builder, bounds); } return builder.toString(); } 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 5dc6d15f9c8..6c00f820410 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.awt.Insets; @@ -47,10 +48,13 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.TreeSet; +import java.util.function.BiFunction; import java.util.stream.Stream; import org.apache.commons.lang3.AbstractLangTest; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.reflect.testbed.Foo; import org.apache.commons.lang3.reflect.testbed.GenericParent; import org.apache.commons.lang3.reflect.testbed.GenericTypeHolder; @@ -171,6 +175,40 @@ abstract class Test1 { public abstract Map m9(); } +class MySuperClass { + // empty +} + +class MyClass> { + // empty +} + +class MultiBoundClass> { + // empty +} + +class TwoParams, U> { + // empty +} + +class InterfaceBound> { + // empty +} + +class DependentBounds { + // empty +} + +class ParameterizedOwner { + class NonGenericInner { + // empty + } + + public NonGenericInner getInner() { + return null; + } +} + /** * Tests {@link TypeUtils}. * @@ -1296,4 +1334,298 @@ void testWrap() { assertEquals(String.class, TypeUtils.wrap(String.class).getType()); } + @Test + void testRecursiveTypeWildcardBoundClass() { + assertEquals("org.apache.commons.lang3.reflect.MyClass>", + TypeUtils.toString(MyClass.class)); + assertEquals("U extends org.apache.commons.lang3.reflect.MySuperClass", + TypeUtils.toString(MyClass.class.getTypeParameters()[0])); + } + + @Test + void testMultiBoundRecursiveType() { + assertEquals("org.apache.commons.lang3.reflect.MultiBoundClass>", + TypeUtils.toString(MultiBoundClass.class)); + assertEquals("U extends java.lang.Number & java.lang.Comparable", + TypeUtils.toString(MultiBoundClass.class.getTypeParameters()[0])); + } + + @Test + void testInterfaceBoundPreserved() { + assertEquals("T extends java.util.List", + TypeUtils.toString(InterfaceBound.class.getTypeParameters()[0])); + } + + @Test + void testMultiParamRecursiveType() { + final ParameterizedType parameterizedType = TypeUtils.parameterize(TwoParams.class, TwoParams.class.getTypeParameters()); + assertEquals("org.apache.commons.lang3.reflect.TwoParams", + TypeUtils.toString(parameterizedType)); + } + + @Test + void testGClassToString() { + assertEquals("org.apache.commons.lang3.reflect.AClass.GClass " + + "& org.apache.commons.lang3.reflect.AClass.AInterface>>", + TypeUtils.toString(AClass.GClass.class)); + } + + @Test + void testContainsTypeVariablesMultiBoundWildcard() { + final TypeVariable t = getClass().getTypeParameters()[0]; + final WildcardType wtUpper = TypeUtils.wildcardType().withUpperBounds(Integer.class, t).build(); + assertTrue(TypeUtils.containsTypeVariables(wtUpper)); + final WildcardType wtLower = TypeUtils.wildcardType().withLowerBounds(Integer.class, t).build(); + assertTrue(TypeUtils.containsTypeVariables(wtLower)); + final WildcardType wtNone = TypeUtils.wildcardType().withUpperBounds(Integer.class, String.class).build(); + assertFalse(TypeUtils.containsTypeVariables(wtNone)); + } + + @Test + void testWildcardTypeBuilderDefensiveCopy() { + // Upper bounds defensive copying on input array and getter + final Type[] upperBounds = { String.class }; + final WildcardType wildcardUpper = TypeUtils.wildcardType().withUpperBounds(upperBounds).build(); + upperBounds[0] = Integer.class; + assertArrayEquals(new Type[] { String.class }, wildcardUpper.getUpperBounds()); + wildcardUpper.getUpperBounds()[0] = Integer.class; + assertArrayEquals(new Type[] { String.class }, wildcardUpper.getUpperBounds()); + + // Lower bounds defensive copying on input array and getter + final Type[] lowerBounds = { String.class }; + final WildcardType wildcardLower = TypeUtils.wildcardType().withLowerBounds(lowerBounds).build(); + lowerBounds[0] = Integer.class; + assertArrayEquals(new Type[] { String.class }, wildcardLower.getLowerBounds()); + wildcardLower.getLowerBounds()[0] = Integer.class; + assertArrayEquals(new Type[] { String.class }, wildcardLower.getLowerBounds()); + } + + @Test + void testBoundedGenericArrayTypeToString() { + final TypeVariable t = DependentBounds.class.getTypeParameters()[0]; + final GenericArrayType gat = TypeUtils.genericArrayType(t); + assertEquals("T[]", TypeUtils.toString(gat)); + } + + @Test + void testBoundedParameterizedTypeArgumentToString() { + final TypeVariable t = DependentBounds.class.getTypeParameters()[0]; + final ParameterizedType pt = TypeUtils.parameterize(List.class, t); + assertEquals("java.util.List", TypeUtils.toString(pt)); + } + + @Test + void testDependentBoundsClassAndTypeParametersToString() { + assertEquals("org.apache.commons.lang3.reflect.DependentBounds", + TypeUtils.toString(DependentBounds.class)); + assertEquals("T extends java.lang.Number", + TypeUtils.toString(DependentBounds.class.getTypeParameters()[0])); + assertEquals("S extends T", + TypeUtils.toString(DependentBounds.class.getTypeParameters()[1])); + } + + @Test + void testToLongStringBoundedTypeVariable() { + assertEquals("org.apache.commons.lang3.reflect.DependentBounds:T extends java.lang.Number", + TypeUtils.toLongString(DependentBounds.class.getTypeParameters()[0])); + assertEquals("org.apache.commons.lang3.reflect.DependentBounds:S extends T", + TypeUtils.toLongString(DependentBounds.class.getTypeParameters()[1])); + assertEquals("org.apache.commons.lang3.reflect.MultiBoundClass:U extends java.lang.Number & java.lang.Comparable", + TypeUtils.toLongString(MultiBoundClass.class.getTypeParameters()[0])); + assertEquals("org.apache.commons.lang3.reflect.InterfaceBound:T extends java.util.List", + TypeUtils.toLongString(InterfaceBound.class.getTypeParameters()[0])); + assertEquals("org.apache.commons.lang3.reflect.MyClass:U extends org.apache.commons.lang3.reflect.MySuperClass", + TypeUtils.toLongString(MyClass.class.getTypeParameters()[0])); + } + + @Test + void testParameterizedOwnerWithNonGenericInnerClassToString() throws NoSuchMethodException { + final ParameterizedType owner = TypeUtils.parameterize(ParameterizedOwner.class, String.class); + final ParameterizedType nonGenericInner = TypeUtils.parameterizeWithOwner(owner, ParameterizedOwner.NonGenericInner.class); + assertEquals("org.apache.commons.lang3.reflect.ParameterizedOwner.NonGenericInner", + TypeUtils.toString(nonGenericInner)); + + final Type methodReturnType = ParameterizedOwner.class.getMethod("getInner").getGenericReturnType(); + assertEquals("org.apache.commons.lang3.reflect.ParameterizedOwner.NonGenericInner", + TypeUtils.toString(methodReturnType)); + } + + @Test + void testCyclicOwnerParameterizedTypeToString() { + final ParameterizedType[] holder = new ParameterizedType[1]; + final ParameterizedType cyclicOwnerType = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return ArrayUtils.EMPTY_TYPE_ARRAY; + } + + @Override + public Type getRawType() { + return List.class; + } + + @Override + public Type getOwnerType() { + return holder[0]; + } + }; + holder[0] = cyclicOwnerType; + assertEquals("List(cycle).List", TypeUtils.toString(cyclicOwnerType)); + + final ParameterizedType[] holderA = new ParameterizedType[1]; + final ParameterizedType[] holderB = new ParameterizedType[1]; + final ParameterizedType typeA = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return ArrayUtils.EMPTY_TYPE_ARRAY; + } + + @Override + public Type getRawType() { + return Map.class; + } + + @Override + public Type getOwnerType() { + return holderB[0]; + } + }; + final ParameterizedType typeB = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return ArrayUtils.EMPTY_TYPE_ARRAY; + } + + @Override + public Type getRawType() { + return Set.class; + } + + @Override + public Type getOwnerType() { + return holderA[0]; + } + }; + holderA[0] = typeA; + holderB[0] = typeB; + assertEquals("Map(cycle).Set.Map", TypeUtils.toString(typeA)); + } + + @Test + void testRepeatedSiblingReferencesToString() { + final ParameterizedType listString = TypeUtils.parameterize(List.class, String.class); + final ParameterizedType mapType = TypeUtils.parameterize(Map.class, listString, listString); + assertEquals("java.util.Map, java.util.List>", + TypeUtils.toString(mapType)); + + final TypeVariable t = DependentBounds.class.getTypeParameters()[0]; + final ParameterizedType biFunctionType = TypeUtils.parameterize(BiFunction.class, t, t, t); + assertEquals("java.util.function.BiFunction", TypeUtils.toString(biFunctionType)); + + final WildcardType wildcard = TypeUtils.wildcardType().withUpperBounds(listString).build(); + final ParameterizedType mapWildcards = TypeUtils.parameterize(Map.class, wildcard, wildcard); + assertEquals("java.util.Map, ? extends java.util.List>", + TypeUtils.toString(mapWildcards)); + } + + @Test + void testThreadLocalCleanupAfterException() { + final Type unsupportedType = new Type() { + @Override + public String getTypeName() { + return "Unsupported"; + } + }; + assertThrows(IllegalArgumentException.class, () -> TypeUtils.toString(unsupportedType)); + assertEquals("java.lang.String", TypeUtils.toString(String.class)); + + final Type faultyType = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + throw new IllegalStateException("Simulated failure in getActualTypeArguments"); + } + + @Override + public Type getRawType() { + return List.class; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + assertThrows(IllegalStateException.class, () -> TypeUtils.toString(faultyType)); + assertEquals("java.lang.String", TypeUtils.toString(String.class)); + + final ParameterizedType wrapper = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return new Type[] { faultyType }; + } + + @Override + public Type getRawType() { + return Set.class; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + assertThrows(IllegalStateException.class, () -> TypeUtils.toString(wrapper)); + assertEquals("java.util.List", + TypeUtils.toString(TypeUtils.parameterize(List.class, String.class))); + } + + @Test + void testCyclicWildcardTypeToString() { + final WildcardType[] holder = new WildcardType[1]; + final WildcardType cyclicWildcard = new WildcardType() { + @Override + public Type[] getUpperBounds() { + return new Type[] { holder[0] }; + } + + @Override + public Type[] getLowerBounds() { + return ArrayUtils.EMPTY_TYPE_ARRAY; + } + }; + holder[0] = cyclicWildcard; + assertEquals("? extends ? (cycle)", TypeUtils.toString(cyclicWildcard)); + } + + @Test + void testCyclicParameterizedTypeToString() { + final ParameterizedType[] holder = new ParameterizedType[1]; + final ParameterizedType cyclicType = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return new Type[] { holder[0] }; + } + + @Override + public Type getRawType() { + return List.class; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + holder[0] = cyclicType; + assertEquals("java.util.List", TypeUtils.toString(cyclicType)); + } + + @Test + void testCyclicGenericArrayTypeToString() { + final GenericArrayType[] holder = new GenericArrayType[1]; + final GenericArrayType cyclicType = () -> holder[0]; + holder[0] = cyclicType; + assertEquals("(cycle)[]", TypeUtils.toString(cyclicType)); + } + }