Skip to content
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
160 changes: 95 additions & 65 deletions src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -290,7 +291,7 @@ public String toString() {
// @formatter:off
private static final AppendableJoiner<Type> AMP_JOINER = AppendableJoiner.<Type>builder()
.setDelimiter(" & ")
.setElementAppender((a, e) -> a.append(toString(e)))
.setElementAppender((a, e) -> a.append(toReferenceString(e)))
.get();
// @formatter:on

Expand All @@ -316,6 +317,18 @@ public String toString() {
.get();
// @formatter:on

/**
* Type arguments joiner.
*/
// @formatter:off
private static final AppendableJoiner<Type> TYPE_ARG_JOINER = AppendableJoiner.<Type>builder()
.setPrefix("<")
.setSuffix(">")
.setDelimiter(", ")
.setElementAppender((a, e) -> a.append(toReferenceString(e)))
.get();
// @formatter:on

/**
* A wildcard instance matching {@code ?}.
*
Expand All @@ -327,17 +340,20 @@ private static <T> 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}.
*
Expand Down Expand Up @@ -387,18 +403,24 @@ 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());
}
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
Expand Down Expand Up @@ -551,17 +573,6 @@ private static Type[] extractTypeArgumentsFrom(final Map<TypeVariable<?>, 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.
Expand All @@ -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()));
}

/**
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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<Set<Type>> 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)";
}

/**
Expand All @@ -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<Type> 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));
}

/**
Expand Down Expand Up @@ -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();
}
Expand Down
Loading
Loading