From 57b6c557838b01929b8f133a99a74ed0ddc6bd18 Mon Sep 17 00:00:00 2001 From: lucve Date: Tue, 26 Aug 2025 09:40:25 +0200 Subject: [PATCH] Issue#69 | Add ResolvedTypeMapper to convert ResolvedType into Type --- .../classmate/ResolvedTypeMapper.java | 209 ++++++++++++++ .../classmate/TestResolvedTypeMapper.java | 267 ++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 src/main/java/com/fasterxml/classmate/ResolvedTypeMapper.java create mode 100644 src/test/java/com/fasterxml/classmate/TestResolvedTypeMapper.java diff --git a/src/main/java/com/fasterxml/classmate/ResolvedTypeMapper.java b/src/main/java/com/fasterxml/classmate/ResolvedTypeMapper.java new file mode 100644 index 0000000..db66d33 --- /dev/null +++ b/src/main/java/com/fasterxml/classmate/ResolvedTypeMapper.java @@ -0,0 +1,209 @@ +package com.fasterxml.classmate; + +import com.fasterxml.classmate.types.ResolvedArrayType; +import com.fasterxml.classmate.types.ResolvedRecursiveType; + +import java.io.Serializable; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + + +/** + * Utility for converting a {@link ResolvedType} into a standard Java {@link Type} + * that can be used with the reflection API. + * + *

Limitations:

+ * + * + *

+ * For further discussion, see + * issue #69. + *

+ */ + +@SuppressWarnings("serial") +public class ResolvedTypeMapper implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Method for mapping {@link ResolvedType} to java types. + + * @param resolvedType The resolved type to map to a java {@link Type} + * @return The type with all generics resolved, OR with the generics replaced with the upper bounds AND with all wildcards resolved to the upper bound. + */ + public Type map(ResolvedType resolvedType) { + if (resolvedType instanceof ResolvedArrayType) { + ResolvedArrayType arrayType = (ResolvedArrayType) resolvedType; + ResolvedType arrayElementType = arrayType.getArrayElementType(); + if (arrayElementType == null) { + throw new IllegalStateException("Missing array element type"); + } + Type elementType = map(arrayElementType); + //GenericArrayType is only used for parameterized types or TypeVariables, but we don't have TypeVariables + if (elementType instanceof ParameterizedType) { + return new GenericArrayTypeImpl(elementType); + } + return arrayType.getErasedType(); + } + // Extract recursive type + if (resolvedType instanceof ResolvedRecursiveType) { + ResolvedRecursiveType recursiveType = (ResolvedRecursiveType) resolvedType; + ResolvedType selfReferencedType = recursiveType.getSelfReferencedType(); + if (selfReferencedType == null) { + throw new IllegalStateException("Missing self referenced type"); + } + return map(selfReferencedType); + } + // no generics present, so the erased type is equal to the real type + if (resolvedType.getTypeParameters() == null || resolvedType.getTypeParameters().isEmpty()) { + return resolvedType.getErasedType(); + } + // Parameters are present, so we need to create a parameterized + // Wildcard types are not supported, since we store the upperbound + return _mapParameterizedType(resolvedType); + } + + private ParameterizedTypeImpl _mapParameterizedType(ResolvedType objectType) { + Class erasedType = objectType.getErasedType(); + List list = new ArrayList<>(); + for (ResolvedType resolvedType : objectType.getTypeParameters()) { + Type mappedArrayType = map(resolvedType); + list.add(mappedArrayType); + } + return new ParameterizedTypeImpl(erasedType, list.toArray(new Type[0]), erasedType.getEnclosingClass()); + } + + /** + * Implementation of ParameterizedType for classmate type creation + */ + static final class ParameterizedTypeImpl implements ParameterizedType { + private final Type rawType; + private final Type[] actualTypeArguments; + private final Type ownerType; + + private ParameterizedTypeImpl(Type rawType, Type[] actualTypeArguments, Type ownerType) { + this.rawType = rawType; + this.actualTypeArguments = actualTypeArguments; + this.ownerType = ownerType; + } + + /** + * {@inheritDoc} + */ + @Override + public Type[] getActualTypeArguments() { + return actualTypeArguments; + } + + /** + * {@inheritDoc} + */ + @Override + public Type getRawType() { + return rawType; + } + + /** + * {@inheritDoc} + */ + @Override + public Type getOwnerType() { + return ownerType; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof ParameterizedType)) { + return false; + } + ParameterizedType that = (ParameterizedType) o; + return Objects.equals(rawType, that.getRawType()) && Objects.deepEquals(actualTypeArguments, that.getActualTypeArguments()) && Objects.equals(ownerType, that.getOwnerType()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(rawType, Arrays.hashCode(actualTypeArguments), ownerType); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return rawType + "<" + Arrays.stream(actualTypeArguments).map(Type::getTypeName).collect(Collectors.joining(", ")) + ">"; + } + } + + /** + * Implementation of GenericArrayType for classmate type creation + */ + static final class GenericArrayTypeImpl implements GenericArrayType { + private final Type genericComponentType; + + private GenericArrayTypeImpl(Type genericComponentType) { + this.genericComponentType = genericComponentType; + } + + /** + * {@inheritDoc} + */ + @Override + public Type getGenericComponentType() { + return genericComponentType; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof GenericArrayType)) { + return false; + } + GenericArrayType that = (GenericArrayType) o; + return Objects.equals(genericComponentType, that.getGenericComponentType()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(genericComponentType); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return genericComponentType.getTypeName() + "[]"; + } + } +} diff --git a/src/test/java/com/fasterxml/classmate/TestResolvedTypeMapper.java b/src/test/java/com/fasterxml/classmate/TestResolvedTypeMapper.java new file mode 100644 index 0000000..f1ebda8 --- /dev/null +++ b/src/test/java/com/fasterxml/classmate/TestResolvedTypeMapper.java @@ -0,0 +1,267 @@ +package com.fasterxml.classmate; + +import com.fasterxml.classmate.types.ResolvedArrayType; +import com.fasterxml.classmate.types.ResolvedPrimitiveType; +import com.fasterxml.classmate.types.ResolvedRecursiveType; + +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.util.List; +import java.util.Map; +import java.util.TreeMap; + +@SuppressWarnings("serial") +public class TestResolvedTypeMapper extends BaseTest { + /* + /********************************************************************** + /* Helper types + /********************************************************************** + */ + + // // Multi-level resolution needed + + static class MyStringLongMap extends MyStringKeyMap { + public Map expectedType; + } + + static class MyStringKeyMap extends TreeMap { + } + + // // For verifying "jdk type" resolution + + static class ListWrapper { + public List wrap() { + return null; + } + } + + // Since using the regular java API we cannot get a parameterized type for List, add the expected type to compare the returntype of the wrap method against + static class StringListWrapper extends ListWrapper { + public long field; + + public List expectedType; + } + + // and recursive types... + static abstract class SelfRefType implements Comparable { + } + + // And arrays (ResolvedType resolves TypeVariables to the upperbound, in this case Object[] and List[]) + static abstract class GenericArray> { + public E[] field; + public Object[] expectedTypeForField; + public T[] boundedField; + public List[] expectedTypeForBoundedField; + public List[] wildcardAndSuperField; + } + + // When using concrete types, we can resolve stricter bounds for the fields in the superclass + static class StringArray extends GenericArray> { + public String[] expectedType; + public List[] expectedBoundedType; + public List[] expectedWildcardSuperBoundedType; + } + + static class StringMapArray { + public Map[] field; + } + + /* + /********************************************************************** + /* setup + /********************************************************************** + */ + + // Let's use a single instance for all tests, to increase chance of seeing failures + protected final ResolvedTypeMapper mapper = new ResolvedTypeMapper(); + + // Let's use the type resolver to construct the ResolvedType used in the tests + protected final TypeResolver resolver = new TypeResolver(); + + /* + /********************************************************************** + /* Unit tests, normal operation + /********************************************************************** + */ + + public void testMapWithRawType() { + ResolvedType resolvedType = resolver.resolve(List.class); + assertEquals(List.class, mapper.map(resolvedType)); + } + + public void testPrimitiveTypes() { + for (ResolvedPrimitiveType primitiveType : ResolvedPrimitiveType.all()) { + assertEquals(primitiveType._erasedType, mapper.map(primitiveType)); + } + } + + public void testPrimitiveArrayTypes() { + ResolvedType resolvedType = resolver.resolve(int[].class); + assertEquals(int[].class, mapper.map(resolvedType)); + } + + public void testGenericUnboundedArrayTypes() throws NoSuchFieldException { + Field f = GenericArray.class.getDeclaredField("field"); + ResolvedType genericArrayType = resolver.resolve(GenericArray.class); + ResolvedType fieldType = resolver.resolve(genericArrayType.getTypeBindings(), f.getGenericType()); + Type mappedArrayType = mapper.map(fieldType); + // Check it is a Object[] + assertEquals(GenericArray.class.getDeclaredField("expectedTypeForField").getGenericType(), mappedArrayType); + assertTrue(mappedArrayType instanceof Class); + } + + public void testGenericBoundedArrayTypes() throws NoSuchFieldException { + Field f = GenericArray.class.getDeclaredField("boundedField"); + ResolvedType genericArrayType = resolver.resolve(GenericArray.class); + ResolvedType fieldType = resolver.resolve(genericArrayType.getTypeBindings(), f.getGenericType()); + Type mappedArrayType = mapper.map(fieldType); + // Check it is a List[] + Type expectedGenericType = GenericArray.class.getDeclaredField("expectedTypeForBoundedField").getGenericType(); + assertEquals(expectedGenericType, mappedArrayType); + assertTrue(mappedArrayType instanceof GenericArrayType); + //Test equals is symmetric + assertEquals(mappedArrayType, expectedGenericType); + } + + public void testGenericWildcardWithSuperArrayTypes() throws NoSuchFieldException { + Field f = GenericArray.class.getDeclaredField("wildcardAndSuperField"); + ResolvedType genericArrayType = resolver.resolve(GenericArray.class); + ResolvedType fieldType = resolver.resolve(genericArrayType.getTypeBindings(), f.getGenericType()); + Type mappedArrayType = mapper.map(fieldType); + // Check it is a List[] + assertEquals(GenericArray.class.getDeclaredField("expectedTypeForBoundedField").getGenericType(), mappedArrayType); + assertTrue(mappedArrayType instanceof GenericArrayType); + + // Even after resolving E, we would still get the same upper bound, since we are using super + ResolvedType stringArrayType = resolver.resolve(StringArray.class); + assertEquals(StringArray.class, mapper.map(stringArrayType)); + ResolvedType resolvedType = resolver.resolve(stringArrayType.getParentClass().getTypeBindings(), f.getGenericType()); + // Check it is a List[] + assertEquals(StringArray.class.getDeclaredField("expectedWildcardSuperBoundedType").getGenericType(), mapper.map(resolvedType)); + } + + public void testConcreteArrayTypes() throws NoSuchFieldException { + Field f = GenericArray.class.getDeclaredField("field"); + ResolvedType stringArrayType = resolver.resolve(StringArray.class); + assertEquals(StringArray.class, mapper.map(stringArrayType)); + + ResolvedType resolvedType = resolver.resolve(stringArrayType.getParentClass().getTypeBindings(), f.getGenericType()); + Type mappedArrayType = mapper.map(resolvedType); + // Check it is a String[] + assertEquals(StringArray.class.getDeclaredField("expectedType").getGenericType(), mappedArrayType); + assertTrue(mappedArrayType instanceof Class); + } + + public void testConcreteParameterArrayTypes() throws NoSuchFieldException { + Field f = GenericArray.class.getDeclaredField("boundedField"); + ResolvedType stringArrayType = resolver.resolve(StringArray.class); + assertEquals(StringArray.class, mapper.map(stringArrayType)); + + ResolvedType resolvedType = resolver.resolve(stringArrayType.getParentClass().getTypeBindings(), f.getGenericType()); + Type mappedArrayType = mapper.map(resolvedType); + // Check it is a List[] + assertEquals(StringArray.class.getDeclaredField("expectedBoundedType").getGenericType(), mappedArrayType); + assertTrue(mappedArrayType instanceof GenericArrayType); + } + + public void testParameterizedArrayTypes() throws NoSuchFieldException { + Field f = StringMapArray.class.getDeclaredField("field"); + ResolvedType stringMapArrayType = resolver.resolve(StringMapArray.class); + assertEquals(StringMapArray.class, mapper.map(stringMapArrayType)); + + ResolvedType resolvedType = resolver.resolve(stringMapArrayType.getTypeBindings(), f.getGenericType()); + // Check it is a Map[] + Type mappedArrayType = mapper.map(resolvedType); + assertEquals(f.getGenericType(), mappedArrayType); + assertTrue(mappedArrayType instanceof GenericArrayType); + + } + + public void testGenericMap() { + // First, direct ref + GenericType mapInput = new GenericType>() { + }; + ResolvedType mapType = resolver.resolve(mapInput); + + //Parameterized map type, since GenericType is skipped + ParameterizedType parameterizedMapType = (ParameterizedType) ((ParameterizedType) mapInput.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + assertEquals(parameterizedMapType, mapper.map(mapType)); + // Test equals is symmetric for ParameterizedType + assertEquals(mapper.map(mapType), parameterizedMapType); + + //String + Type mappedString = mapper.map(mapType.getTypeParameters().get(0)); + assertEquals(parameterizedMapType.getActualTypeArguments()[0], mappedString); + assertEquals(String.class, mappedString); + //Long + Type mappedLong = mapper.map(mapType.getTypeParameters().get(1)); + assertEquals(parameterizedMapType.getActualTypeArguments()[1], mappedLong); + assertEquals(Long.class, mappedLong); + } + + public void testParametricMap() throws NoSuchFieldException { + ResolvedType mapType = resolver.resolve(MyStringLongMap.class); + + assertEquals(MyStringLongMap.class, mapper.map(mapType)); + // Ensure we can find parameters for Map + ResolvedType mapSuperType = mapType.findSupertype(Map.class); + // Check if it is a Map + assertEquals(MyStringLongMap.class.getDeclaredField("expectedType").getGenericType(), mapper.map(mapSuperType)); + } + + public void testJdkType() throws Exception { + ResolvedType wrapperType = resolver.resolve(StringListWrapper.class); + Field f = StringListWrapper.class.getDeclaredField("field"); + // first; field has no generic stuff, should be simple + ResolvedType fieldType = resolver.resolve(wrapperType.getTypeBindings(), f.getGenericType()); + assertEquals(Long.TYPE, mapper.map(fieldType)); + + // but method return type is templatized; and MUST be given correct type bindings! + Method m = ListWrapper.class.getDeclaredMethod("wrap"); + ResolvedType superType = wrapperType.getParentClass(); + ResolvedType methodReturnType = resolver.resolve(superType.getTypeBindings(), + m.getGenericReturnType()); + // should be List, but we cannot access that from the method, so we compare it to a typed field from the java API + Type expectedType = StringListWrapper.class.getField("expectedType").getGenericType(); + assertEquals(expectedType, mapper.map(methodReturnType)); + } + + public void testSimpleSelfRef() { + ResolvedType type = resolver.resolve(SelfRefType.class); + List interfaces = type.getImplementedInterfaces(); + assertEquals(1, interfaces.size()); + ResolvedType compType = interfaces.get(0); + assertEquals(SelfRefType.class.getGenericInterfaces()[0], mapper.map(compType)); + } + + /* + /********************************************************************** + /* Unit tests, error cases + /********************************************************************** + */ + + public void testNullabilityInResolvedRecursiveType() { + ResolvedRecursiveType resolvedType = new ResolvedRecursiveType(Integer.class, TypeBindings.emptyBindings()); + //empty reference + try { + mapper.map(resolvedType); + fail("Expected failure"); + } catch (IllegalStateException e) { + verifyException(e, "Missing self referenced type"); + } + } + + public void testNullabilityInResolvedArrayType() { + ResolvedArrayType resolvedType = new ResolvedArrayType(Integer.class, TypeBindings.emptyBindings(), null); + //empty reference + try { + mapper.map(resolvedType); + fail("Expected failure"); + } catch (IllegalStateException e) { + verifyException(e, "Missing array element type"); + } + } +}