From 326c78e4622433d30a173e770d39be2c5809b20f Mon Sep 17 00:00:00 2001 From: Stephen Colebourne Date: Thu, 2 Jan 2025 16:53:39 +0000 Subject: [PATCH] Convert ResolvedTyoe to Type * Allow a `ResolvedType` to be converted to a `Type` * Rename methods and add Javadoc for clarity * Fix uses of resolved type in Binary/JSON serialization --- .../java/org/joda/beans/ResolvedType.java | 262 +++++++++++++++--- .../ser/bin/JodaBeanStandardBinWriter.java | 10 +- .../beans/ser/json/JodaBeanJsonWriter.java | 12 +- .../java/org/joda/beans/TestResolvedType.java | 77 ++++- 4 files changed, 304 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/joda/beans/ResolvedType.java b/src/main/java/org/joda/beans/ResolvedType.java index c0d78c4f..2d6b1d49 100644 --- a/src/main/java/org/joda/beans/ResolvedType.java +++ b/src/main/java/org/joda/beans/ResolvedType.java @@ -23,6 +23,7 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -48,12 +49,18 @@ * The process of resolving uses a context class to resolve type variables like {@code }. *

* Where type variables cannot be resolved, the type parameter upper bound will be used. - * For example, {@code ResolvedType.of(List.class)} returns {@code List} unless a context + * For example, {@code ResolvedType.from(List.class)} returns {@code List} unless a context * class is passed in that extends {@code List} and constrains the type, such as an imaginary class * {@code StringList implements List}. *

* Note that special cases like anonymous classes, hidden classes and specialized enum subclasses * are not resolved. + *

+ * The state of this class consists of two parts. + * Firstly, there is a {@code Class} representing the raw type, which may be an array. + * Secondaly, there is a list of {@code ResolvedType} representing the type arguments. + * For example, {@code List[]} will be represented by a {@code Class} of {@code List[]} and + * a list of type arguments representing {@code String}. * * @since 3.0.0 */ @@ -322,7 +329,7 @@ public static ResolvedType ofFlat(Class rawType, Class... arguments) { //------------------------------------------------------------------------- /** - * Obtains an instance from a raw type, defaulting any type parameters. + * Obtains an instance from a raw type, providing a default type argument for where the argument is missing. *

* If the input class has generic type parameters, they will be resolved to their upper bound, * typically {@code Object.class}. Use {@link #of(Class)} if you simply want to obtain a wrapper around the raw type. @@ -335,31 +342,54 @@ public static ResolvedType ofFlat(Class rawType, Class... arguments) { */ public static ResolvedType from(Class rawType) { Objects.requireNonNull(rawType, "rawType must not be null"); - return resolveClass(rawType, Object.class); + return resolveClass(rawType, Object.class, false); + } + + /** + * Obtains an instance from a type and context class, providing a default type argument for where the argument is missing. + *

+ * The type is typically obtained from reflection, such as from {@link MetaProperty#propertyGenericType()}. + * The context class represents the {@code Class} associated with the object being queried, + * which is used to resolve type variables like {@code }. + * + * @param javaType the Java type to resolve, not null + * @param contextClass the context class to evaluate against, not null + * + * @return the resolved type + * @throws NullPointerException if null is passed in + */ + public static ResolvedType from(Type javaType, Class contextClass) { + return from(javaType, contextClass, false); } /** - * Obtains an instance from a type and context class. + * Obtains an instance from a type and context class, where missing type arguments are not defaulted. *

* The type is typically obtained from reflection, such as from {@link MetaProperty#propertyGenericType()}. * The context class represents the {@code Class} associated with the object being queried, * which is used to resolve type variables like {@code }. * - * @param type the type to resolve, not null + * @param javaType the Java type to resolve, not null * @param contextClass the context class to evaluate against, not null + * * @return the resolved type * @throws NullPointerException if null is passed in */ - public static ResolvedType from(Type type, Class contextClass) { - Objects.requireNonNull(type, "type must not be null"); + public static ResolvedType fromAllowRaw(Type javaType, Class contextClass) { + return from(javaType, contextClass, true); + } + + // resolves from the Java Type, optionally resolving raw types + private static ResolvedType from(Type javaType, Class contextClass, boolean allowRaw) { + Objects.requireNonNull(javaType, "type must not be null"); Objects.requireNonNull(contextClass, "contextClass must not be null"); - return switch (type) { - case Class cls -> resolveClass(cls, contextClass); - case ParameterizedType parameterizedType -> resolveParameterizedType(parameterizedType, contextClass); - case GenericArrayType arrType -> resolveGenericArrayType(arrType, contextClass); - case TypeVariable tvar -> resolveTypeVariable(tvar, contextClass); - case WildcardType wild -> resolveWildcard(wild, contextClass); - default -> throw unknownGenericTypeClass(type); + return switch (javaType) { + case Class cls -> resolveClass(cls, contextClass, allowRaw); + case ParameterizedType parameterizedType -> resolveParameterizedType(parameterizedType, contextClass, allowRaw); + case GenericArrayType arrType -> resolveGenericArrayType(arrType, contextClass, allowRaw); + case TypeVariable tvar -> resolveTypeVariable(tvar, contextClass, allowRaw); + case WildcardType wild -> resolveWildcard(wild, contextClass, allowRaw); + default -> throw unknownGenericTypeClass(javaType); }; } @@ -368,77 +398,84 @@ private static IllegalArgumentException unknownGenericTypeClass(Type type) { return new IllegalArgumentException("Unknown generic type class: " + type); } - // resolve a Class - private static ResolvedType resolveClass(Class cls, Class contextClass) { + // resolve a Class, either returning the raw type, or selecting the best available type arguments + private static ResolvedType resolveClass(Class cls, Class contextClass, boolean allowRaw) { + if (allowRaw) { + return new ResolvedType(cls); + } var baseType = extractBaseComponentType(cls); var typeVariables = baseType.getTypeParameters(); var typeArguments = new ResolvedType[typeVariables.length]; for (var i = 0; i < typeArguments.length; i++) { - typeArguments[i] = resolveTypeVariable(typeVariables[i], contextClass); + typeArguments[i] = resolveTypeVariable(typeVariables[i], contextClass, allowRaw); } return new ResolvedType(cls, List.of(typeArguments)); } // resolve things like List - private static ResolvedType resolveParameterizedType(ParameterizedType parameterizedType, Class contextClass) { + private static ResolvedType resolveParameterizedType(ParameterizedType parameterizedType, Class contextClass, boolean allowRaw) { var actualTypeArguments = parameterizedType.getActualTypeArguments(); var typeArguments = new ResolvedType[actualTypeArguments.length]; for (var i = 0; i < typeArguments.length; i++) { - typeArguments[i] = from(actualTypeArguments[i], contextClass); + typeArguments[i] = from(actualTypeArguments[i], contextClass, allowRaw); } // all known instances of ParameterizedType return Class return new ResolvedType((Class) parameterizedType.getRawType(), List.of(typeArguments)); } // resolve things like Optional[] - private static ResolvedType resolveGenericArrayType(GenericArrayType arrType, Class contextClass) { + private static ResolvedType resolveGenericArrayType(GenericArrayType arrType, Class contextClass, boolean allowRaw) { var componentType = arrType.getGenericComponentType(); // Optional - var componentResolvedType = from(componentType, contextClass); // Optional + var componentResolvedType = from(componentType, contextClass, allowRaw); // Optional var rawType = componentResolvedType.getRawType(); // Optional return new ResolvedType(rawType.arrayType(), componentResolvedType.getArguments()); } // resolve things like > - private static ResolvedType resolveTypeVariable(TypeVariable tvar, Class contextClass) { + private static ResolvedType resolveTypeVariable(TypeVariable tvar, Class contextClass, boolean allowRaw) { var resolved = JodaBeanUtils.resolveGenerics(tvar, contextClass); if (resolved instanceof TypeVariable unresolved) { var bounds = unresolved.getBounds(); - return bounds.length > 0 ? resolveGenericBound(bounds[0]) : OBJECT; + return bounds.length > 0 ? resolveGenericBound(bounds[0], allowRaw) : OBJECT; } - return from(resolved, contextClass); + return from(resolved, contextClass, allowRaw); } // resolve things like > - private static ResolvedType resolveGenericBound(Type bound) { + private static ResolvedType resolveGenericBound(Type bound, boolean allowRaw) { return switch (bound) { - case Class cls -> resolveClass(cls, Object.class); + case Class cls -> resolveClass(cls, Object.class, allowRaw); case ParameterizedType pt -> { var rawType = JodaBeanUtils.eraseToClass(pt.getRawType()); var typeArgs = pt.getActualTypeArguments(); var resolvedTypeArgs = new ResolvedType[typeArgs.length]; if (typeArgs.length == 0) { - yield resolveClass(rawType, Object.class); // ignore weird situations + yield resolveClass(rawType, Object.class, allowRaw); // ignore weird situations } for (var i = 0; i < typeArgs.length; i++) { resolvedTypeArgs[i] = typeArgs[i] instanceof TypeVariable ? OBJECT : // resolve > into Comparable - resolveGenericBound(typeArgs[i]); + resolveGenericBound(typeArgs[i], allowRaw); } yield of(rawType, resolvedTypeArgs); } - default -> resolveClass(JodaBeanUtils.eraseToClass(bound), Object.class); // ignore weird situations + default -> resolveClass(JodaBeanUtils.eraseToClass(bound), Object.class, allowRaw); // ignore weird situations }; } // resolves a wildcard - private static ResolvedType resolveWildcard(WildcardType wild, Class contextClass) { + private static ResolvedType resolveWildcard(WildcardType wild, Class contextClass, boolean allowRaw) { var bounds = wild.getUpperBounds(); - return bounds.length == 0 ? OBJECT : from(bounds[0], contextClass); + return bounds.length == 0 ? OBJECT : from(bounds[0], contextClass, allowRaw); } //------------------------------------------------------------------------- /** * Gets the raw type. + *

+ * Gets the {@code Class} object representing the resolved type without generic type arguments. + * For example, {@code Optional} will return {@code Optional}, + * and {@code List[]} will return {@code List[]}. * * @return the raw type, may be a primitive type or an array type, not null */ @@ -475,7 +512,9 @@ public ResolvedType getArgumentOrDefault(int index) { /** * Checks whether this is a parameterized generic type, irrespective of whether the type arguments are known. *

- * For example, "List" and "List<String>" return true, while "String" returns false. + * For example, {@code List} and {@code List} return true, while {@code String} returns false. + *

+ * An array type will return true or false based on the raw type. * * @return true if this is a type with generic type parameters */ @@ -484,16 +523,21 @@ public boolean isParameterized() { } /** - * Checks whether this is a raw type. + * Checks whether this type is raw, which is a type that has missing type arguments. + *

+ * An array type will return true or false based on the raw type. * * @return true if this is a parameterized generic type but the type arguments are empty */ public boolean isRaw() { - return arguments.isEmpty() && isParameterized(); + return arguments.isEmpty() && extractBaseComponentType(rawType).getTypeParameters().length != 0; } /** - * Checks whether this is a primitive type. + * Checks whether this type is a primitive type. + *

+ * An array of primitives will return false. + * Consider calling {@link #toBaseType()} first to check for primitive arrays. * * @return true if this is one of the 8 primitive types or void */ @@ -502,7 +546,7 @@ public boolean isPrimitive() { } /** - * Checks whether this is an array type. + * Checks whether this type is an array type. * * @return true if this is an array type */ @@ -512,16 +556,19 @@ public boolean isArray() { //------------------------------------------------------------------------- /** - * Gets the raw type, effectively dropping the generics. + * Converts this type to be raw, effectively dropping the generics. + *

+ * This operates on all kinds of {@code ResolvedType}, including arrays and primitives. + * If this type has type arguments, the result does not. * * @return the underlying raw type, as a {@code ResolvedType} */ - public ResolvedType toRawType() { + public ResolvedType toRaw() { return arguments.isEmpty() ? this : new ResolvedType(rawType); } /** - * Returns the boxed type equivalent to this type. + * Converts this type to the boxed equivalent. *

* If this type is one of the nine primitive types, the equivalent box is returned. * Otherwise, {@code this} is returned. @@ -555,7 +602,28 @@ private ResolvedType boxed() { } /** - * Gets the component type if the raw type is an array. + * Converts this type to the equivalent Java {@code Type}. + * + * @return the equivalent Java type + */ + public Type toJavaType() { + if (arguments.isEmpty()) { + return rawType; + } + if (rawType.isArray()) { + var componentType = toComponentType().toJavaType(); + return new DynamicGenericArrayType(componentType); + } else { + var argumentList = arguments.stream() + .map(ResolvedType::toJavaType) + .toArray(Type[]::new); + return new DynamicParameterizedType(rawType, argumentList); + } + } + + //------------------------------------------------------------------------- + /** + * Returns the component type if the raw type is an array. *

* Note that the component type may be an array type if this type is a higher-dimension array. * @@ -575,7 +643,7 @@ private IllegalStateException invalidArrayType() { } /** - * Gets the component type if the raw type is an array, defaulting to {@code Object} if the type is not an array. + * Returns the component type if the raw type is an array, defaulting to {@code Object} if the type is not an array. *

* Note that the component type may be an array type if this type is a higher-dimension array. * @@ -599,6 +667,20 @@ public ResolvedType toArrayType() { return new ResolvedType(rawType.arrayType(), arguments); } + /** + * Returns the base type, effectively dropping any array part. + *

+ * The result of this method consists of the base component type of the raw type, and the same type arguments. + * For example, {@code List[][]} will be returned as {@code List}. + * + * @return the component type + * @throws IllegalStateException if the type is not an array + */ + public ResolvedType toBaseType() { + var baseComponentType = extractBaseComponentType(rawType); + return new ResolvedType(baseComponentType, arguments); + } + //------------------------------------------------------------------------- private static Class extractBaseComponentType(Class cls) { var nonArrayType = cls; @@ -676,4 +758,102 @@ private String shortenedClassName(Class cls) { return name; } + //------------------------------------------------------------------------- + // the JDK should have a public implementation for this + private static final class DynamicParameterizedType implements ParameterizedType { + + private final Class rawClass; + private final Type[] argumentsList; + + private DynamicParameterizedType(Class rawClass, Type[] typeArguments) { + this.rawClass = Objects.requireNonNull(rawClass); + this.argumentsList = Objects.requireNonNull(typeArguments).clone(); + for (var type : typeArguments) { + Objects.requireNonNull(type); + } + if (typeArguments.length != rawClass.getTypeParameters().length && typeArguments.length != 0) { + throw new IllegalArgumentException("Type parameters do not match"); + } + } + + @Override + public Type[] getActualTypeArguments() { + return argumentsList.clone(); + } + + @Override + public Type getRawType() { + return rawClass; + } + + @Override + public Type getOwnerType() { + if (rawClass.isLocalClass()) { + return null; + } else { + return rawClass.getEnclosingClass(); + } + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ParameterizedType other && + getRawType().equals(other.getRawType()) && + Objects.equals(getOwnerType(), other.getOwnerType()) && + Arrays.equals(getActualTypeArguments(), other.getActualTypeArguments()); + } + + @Override + public int hashCode() { + return Objects.hash(rawClass, Arrays.hashCode(argumentsList)); + } + + @Override + public String toString() { + var builder = new StringBuilder(rawClass.getName()).append('<'); + for (var i = 0; i < argumentsList.length; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(typeToString(argumentsList[i])); + } + return builder.append('>').toString(); + } + } + + //------------------------------------------------------------------------- + // the JDK should have a public implementation for this + private static final class DynamicGenericArrayType implements GenericArrayType { + + private final Type componentType; + + private DynamicGenericArrayType(Type componentType) { + this.componentType = Objects.requireNonNull(componentType); + } + + @Override + public Type getGenericComponentType() { + return componentType; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof GenericArrayType other && + Objects.equals(getGenericComponentType(), other.getGenericComponentType()); + } + + @Override + public int hashCode() { + return Objects.hashCode(componentType); + } + + @Override + public String toString() { + return typeToString(componentType) + "[]"; + } + } + + private static String typeToString(Type type) { + return type instanceof Class cls ? cls.getName() : type.toString(); + } } diff --git a/src/main/java/org/joda/beans/ser/bin/JodaBeanStandardBinWriter.java b/src/main/java/org/joda/beans/ser/bin/JodaBeanStandardBinWriter.java index 3f0ce836..c5cd0c07 100644 --- a/src/main/java/org/joda/beans/ser/bin/JodaBeanStandardBinWriter.java +++ b/src/main/java/org/joda/beans/ser/bin/JodaBeanStandardBinWriter.java @@ -269,7 +269,7 @@ private static ResolvedType toWeakenedType(ResolvedType base) { for (var arg : base.getArguments()) { var rawType = arg.getRawType(); if (LOOKUP.get(rawType).isCollection(rawType)) { - return base.toRawType(); + return base.toRaw(); } } return base; @@ -667,7 +667,7 @@ public void handle( if (!Optional.class.isAssignableFrom(declaredType.getRawType())) { writeMetaType(writer, "Optional"); } - var valueType = declaredType.getArgumentOrDefault(0).toRawType(); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.writeObject(valueType, "", opt.orElse(null)); } @@ -681,7 +681,7 @@ public PropertyHandler handleProperty( return opt .map(value -> (PropertyHandler) () -> { - var valueType = declaredType.getArgumentOrDefault(0).toRawType(); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.output.writeString(propertyName); writer.writeObject(valueType, propertyName, value); }) @@ -705,7 +705,7 @@ public void handle( writeMetaType(writer, "GuavaOptional"); } // write content - var valueType = declaredType.getArgumentOrDefault(0).toRawType(); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.writeObject(valueType, "", opt.orNull()); } @@ -719,7 +719,7 @@ public PropertyHandler handleProperty( return opt .transform(value -> (PropertyHandler) () -> { - var valueType = declaredType.getArgumentOrDefault(0).toRawType(); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.output.writeString(propertyName); writer.writeObject(valueType, propertyName, value); }) diff --git a/src/main/java/org/joda/beans/ser/json/JodaBeanJsonWriter.java b/src/main/java/org/joda/beans/ser/json/JodaBeanJsonWriter.java index 568dc167..beb2fb19 100644 --- a/src/main/java/org/joda/beans/ser/json/JodaBeanJsonWriter.java +++ b/src/main/java/org/joda/beans/ser/json/JodaBeanJsonWriter.java @@ -273,7 +273,7 @@ private void writeBeanProperties(ResolvedType declaredType, Bean bean) throws IO if (settings.isSerialized(metaProperty)) { var value = metaProperty.get(bean); if (value != null) { - var resolvedType = ResolvedType.from(metaProperty.propertyGenericType(), bean.getClass()); + var resolvedType = metaProperty.propertyResolvedType(bean.getClass()); var handler = LOOKUP.get(value.getClass()); handler.handleProperty(this, resolvedType, metaProperty.name(), value); } @@ -478,7 +478,7 @@ static ResolvedType toWeakenedType(ResolvedType base) { for (var arg : base.getArguments()) { var rawType = arg.getRawType(); if (LOOKUP.get(rawType).isCollection()) { - return base.toRawType(); + return base.toRaw(); } } return base; @@ -895,7 +895,7 @@ public void handle( String propertyName, Optional opt) throws IOException { - var valueType = declaredType.getArgumentOrDefault(0); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.writeObject(valueType, "", opt.orElse(null)); } @@ -909,7 +909,7 @@ public void handleProperty( var value = opt.orElse(null); if (value != null) { - var valueType = declaredType.getArgumentOrDefault(0); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.output.writeObjectKey(propertyName); writer.writeObject(valueType, propertyName, value); } @@ -928,7 +928,7 @@ public void handle( String propertyName, com.google.common.base.Optional opt) throws IOException { - var valueType = declaredType.getArgumentOrDefault(0); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.writeObject(valueType, "", opt.orNull()); } @@ -942,7 +942,7 @@ public void handleProperty( var value = opt.orNull(); if (value != null) { - var valueType = declaredType.getArgumentOrDefault(0); + var valueType = declaredType.getArgumentOrDefault(0).toRaw(); writer.output.writeObjectKey(propertyName); writer.writeObject(valueType, propertyName, value); } diff --git a/src/test/java/org/joda/beans/TestResolvedType.java b/src/test/java/org/joda/beans/TestResolvedType.java index aa89506e..70c3b1d9 100644 --- a/src/test/java/org/joda/beans/TestResolvedType.java +++ b/src/test/java/org/joda/beans/TestResolvedType.java @@ -20,9 +20,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import java.io.Serializable; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; import org.joda.beans.sample.AbstractResult; @@ -60,15 +62,14 @@ */ class TestResolvedType { - @SuppressWarnings("serial") static Object[][] data_resolvedTypes() { return new Object[][] { {ResolvedType.of(String.class), String.class, List.of(), "String"}, {ResolvedType.of(List.class), - List.class, List.of(), - "List"}, + List.class, List.of(), + "List"}, {ResolvedType.from(String.class), String.class, List.of(), "String"}, @@ -90,6 +91,9 @@ static Object[][] data_resolvedTypes() { {ResolvedType.of(String[].class), String[].class, List.of(), "String[]"}, + {ResolvedType.of(List[].class), + List[].class, List.of(), + "List[]"}, {ResolvedType.ofFlat(List[].class, String.class), List[].class, List.of(ResolvedType.of(String.class)), "List[]"}, @@ -238,9 +242,16 @@ void test_queries( List expectedArgTypes, String expectedToString) { + var expectedRawTypeWithoutArray = expectedRawType; + if (expectedRawTypeWithoutArray.isArray()) { + expectedRawTypeWithoutArray = expectedRawTypeWithoutArray.getComponentType(); + if (expectedRawTypeWithoutArray.isArray()) { + expectedRawTypeWithoutArray = expectedRawTypeWithoutArray.getComponentType(); + } + } if (expectedArgTypes.isEmpty()) { assertThat(test.getArgumentOrDefault(0)).isEqualTo(ResolvedType.OBJECT); - assertThat(test.isRaw()).isEqualTo(expectedRawType.getTypeParameters().length != 0); + assertThat(test.isRaw()).isEqualTo(expectedRawTypeWithoutArray.getTypeParameters().length != 0); } else if (expectedArgTypes.size() == 1) { assertThat(test.getArgumentOrDefault(0)).isEqualTo(expectedArgTypes.get(0)); assertThat(test.getArgumentOrDefault(1)).isEqualTo(ResolvedType.OBJECT); @@ -265,10 +276,26 @@ void test_queries( assertThat(test.toComponentTypeOrDefault()).isEqualTo(ResolvedType.OBJECT); } assertThat(test.toArrayType().toComponentType()).isEqualTo(test); + assertThat(test.toBaseType().getRawType()).isEqualTo(expectedRawTypeWithoutArray); + assertThat(test.toBaseType().getArguments()).isEqualTo(test.getArguments()); assertThat(test.isPrimitive()).isEqualTo(expectedRawType.isPrimitive()); assertThat(test.isParameterized()).isEqualTo(expectedRawType.getTypeParameters().length > 0); } + @ParameterizedTest + @MethodSource("data_resolvedTypes") + void test_javaType( + ResolvedType test, + Class expectedRawType, + List expectedArgTypes, + String expectedToString) { + + if (test.getArguments().isEmpty()) { + assertThat(test.toJavaType()).isEqualTo(expectedRawType); + } + assertThat(ResolvedType.fromAllowRaw(test.toJavaType(), Object.class)).isEqualTo(test); + } + @ParameterizedTest @MethodSource("data_resolvedTypes") void test_jodaConvert( @@ -309,7 +336,6 @@ void test_boxed(Class primitive, Class box) { } //------------------------------------------------------------------------- - @SuppressWarnings("serial") static Object[][] data_invalidParse() { return new Object[][] { {"String,"}, @@ -346,4 +372,45 @@ void test_enumSubclass() { var test = ResolvedType.of(RiskLevel.HIGH.getClass()); assertThat(test.getRawType().getSuperclass()).isEqualTo(RiskLevel.class); } + + @SuppressWarnings("unused") + private final List field1 = null; + @SuppressWarnings("unused") + private final Map> field2 = null; + @SuppressWarnings("unused") + private final Map[]>[][] field3 = null; + + @SuppressWarnings("unused") + private final Optional field0 = null; + @SuppressWarnings("unused") + private final List field1b = null; + @SuppressWarnings("unused") + private final Map[]>[][] field3b = null; + + //------------------------------------------------------------------------- + static Object[][] data_dynamicTypes() throws Exception { + return new Object[][] { + {String.class}, + {String[].class}, + {List[].class}, + {List[][].class}, + {TestResolvedType.class.getDeclaredField("field1").getGenericType()}, + {TestResolvedType.class.getDeclaredField("field2").getGenericType()}, + {TestResolvedType.class.getDeclaredField("field3").getGenericType()}, + }; + } + + @ParameterizedTest + @MethodSource("data_dynamicTypes") + void test_dynamicTypes(Type type) throws Exception { + var test = ResolvedType.fromAllowRaw(type, Object.class); + assertThat(test.toJavaType()) + .isEqualTo(type) + .isNotEqualTo("") + .isNotEqualTo(Object.class) + .isNotEqualTo(TestResolvedType.class.getDeclaredField("field0").getGenericType()) + .isNotEqualTo(TestResolvedType.class.getDeclaredField("field1b").getGenericType()) + .isNotEqualTo(TestResolvedType.class.getDeclaredField("field3b").getGenericType()); + assertThat(test.toJavaType().toString()).isEqualTo(type.toString()); + } }