From e2fae069dcbb1a09e66a37671638c11d20cf14b0 Mon Sep 17 00:00:00 2001 From: MoonFruit Date: Tue, 4 Mar 2025 17:36:49 +0800 Subject: [PATCH] Avoid exception in ConversionService.canConvert() for Enum targets Prior to this commit, ConversionService#canConvert(Class, Class) threw an IllegalArgumentException when invoked with Enum.class as the target type (i.e., `canConvert(String.class, Enum.class)`), because ConverterFactory#getConverter() in StringToEnumConverterFactory and IntegerToEnumConverterFactory eagerly resolved the concrete enum type. To address that, StringToEnumConverterFactory and IntegerToEnumConverterFactory now implement ConditionalConverter so that matches() can reject non-concrete-enum targets before getConverter() is ever invoked. Closes gh-34532 Signed-off-by: MoonFruit Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> --- .../core/convert/support/ConversionUtils.java | 10 +++++++--- .../support/IntegerToEnumConverterFactory.java | 14 ++++++++++++-- .../support/StringToEnumConverterFactory.java | 14 ++++++++++++-- .../support/GenericConversionServiceTests.java | 14 ++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java index e6e0a7c2a29..91da300fdd6 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java @@ -22,7 +22,6 @@ import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -71,12 +70,17 @@ abstract class ConversionUtils { return false; } - public static Class getEnumType(Class targetType) { + /** + * Resolve the enum type for the supplied target type. + * @param targetType the target type for which to resolve the enum type + * @return the resolved enum type, or {@code null} if the supplied target type + * does not refer to an enum + */ + public static @Nullable Class resolveEnumType(Class targetType) { Class enumType = targetType; while (enumType != null && !enumType.isEnum()) { enumType = enumType.getSuperclass(); } - Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum"); return enumType; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java index 306400cdc36..aafde995ca1 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java @@ -18,8 +18,11 @@ package org.springframework.core.convert.support; import org.jspecify.annotations.Nullable; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.Assert; /** * Converts from an Integer to a {@link java.lang.Enum} by calling {@link Class#getEnumConstants()}. @@ -29,11 +32,18 @@ import org.springframework.core.convert.converter.ConverterFactory; * @since 4.3 */ @SuppressWarnings({"rawtypes", "unchecked"}) -final class IntegerToEnumConverterFactory implements ConverterFactory { +final class IntegerToEnumConverterFactory implements ConverterFactory, ConditionalConverter { @Override public Converter getConverter(Class targetType) { - return new IntegerToEnum(ConversionUtils.getEnumType(targetType)); + Class enumType = ConversionUtils.resolveEnumType(targetType); + Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum"); + return new IntegerToEnum(enumType); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (ConversionUtils.resolveEnumType(targetType.getType()) != null); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java index 93c32d6d700..917750ef881 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java @@ -18,8 +18,11 @@ package org.springframework.core.convert.support; import org.jspecify.annotations.Nullable; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.Assert; /** * Converts from a String to a {@link java.lang.Enum} by calling {@link Enum#valueOf(Class, String)}. @@ -29,11 +32,18 @@ import org.springframework.core.convert.converter.ConverterFactory; * @since 3.0 */ @SuppressWarnings({"rawtypes", "unchecked"}) -final class StringToEnumConverterFactory implements ConverterFactory { +final class StringToEnumConverterFactory implements ConverterFactory, ConditionalConverter { @Override public Converter getConverter(Class targetType) { - return new StringToEnum(ConversionUtils.getEnumType(targetType)); + Class enumType = ConversionUtils.resolveEnumType(targetType); + Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum"); + return new StringToEnum(enumType); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (ConversionUtils.resolveEnumType(targetType.getType()) != null); } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index 4d8a21bdf5d..6150edb2c3a 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -503,6 +503,20 @@ class GenericConversionServiceTests { assertThat(conversionService.convert("base1", MyEnum.class)).isEqualTo(MyEnum.A); } + @Test // gh-34532 + void canConvertToEnumDoesNotThrowForNonEnumTargetType() { + conversionService.addConverterFactory(new StringToEnumConverterFactory()); + conversionService.addConverterFactory(new IntegerToEnumConverterFactory()); + + assertThat(conversionService.canConvert(String.class, Enum.class)).isFalse(); + assertThat(conversionService.canConvert(Integer.class, Enum.class)).isFalse(); + + assertThat(conversionService.canConvert(String.class, MyEnum.class)).isTrue(); + assertThat(conversionService.canConvert(Integer.class, MyEnum.class)).isTrue(); + assertThat(conversionService.convert("A", MyEnum.class)).isEqualTo(MyEnum.A); + assertThat(conversionService.convert(0, MyEnum.class)).isEqualTo(MyEnum.A); + } + @Test void convertNullAnnotatedStringToString() throws Exception { String source = null;