From 2921b1428ed4b7c3faf04e19a5224ee1da7fc1d0 Mon Sep 17 00:00:00 2001 From: viviel Date: Sat, 4 Dec 2021 19:51:58 +0800 Subject: [PATCH 1/2] Support lambda based converters via bean method signature generics Update `ApplicationConversionService` to support beans that are implemented using lambdas. The updated code now uses the result of `beanDefinition.getResolvableType()` if the type itself has unresolvable generics. See gh-22885 --- .../convert/ApplicationConversionService.java | 330 +++++++++++++++++- .../ApplicationConversionServiceTests.java | 90 +++++ 2 files changed, 404 insertions(+), 16 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java index a5ff4ce680b..e02e26281b9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java @@ -17,12 +17,23 @@ package org.springframework.boot.convert; import java.lang.annotation.Annotation; +import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; import java.util.Set; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.ConverterRegistry; @@ -37,6 +48,7 @@ import org.springframework.format.Parser; import org.springframework.format.Printer; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; /** @@ -49,6 +61,7 @@ import org.springframework.util.StringValueResolver; * against registry instance. * * @author Phillip Webb + * @author Shixiong Guo(viviel) * @since 2.0.0 */ public class ApplicationConversionService extends FormattingConversionService { @@ -272,28 +285,313 @@ public class ApplicationConversionService extends FormattingConversionService { * @since 2.2.0 */ public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) { - Set beans = new LinkedHashSet<>(); - beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values()); - beans.addAll(beanFactory.getBeansOfType(Converter.class).values()); - beans.addAll(beanFactory.getBeansOfType(Printer.class).values()); - beans.addAll(beanFactory.getBeansOfType(Parser.class).values()); - for (Object bean : beans) { - if (bean instanceof GenericConverter genericConverter) { - registry.addConverter(genericConverter); + Set> entries = new LinkedHashSet<>(); + entries.addAll(beanFactory.getBeansOfType(GenericConverter.class).entrySet()); + entries.addAll(beanFactory.getBeansOfType(Converter.class).entrySet()); + entries.addAll(beanFactory.getBeansOfType(Printer.class).entrySet()); + entries.addAll(beanFactory.getBeansOfType(Parser.class).entrySet()); + for (Map.Entry e : entries) { + String beanName = e.getKey(); + Object bean = e.getValue(); + try { + doAddBean(registry, bean); } - else if (bean instanceof Converter converter) { - registry.addConverter(converter); + catch (IllegalArgumentException ex) { + if (!tryAddFactoryMethodBean(registry, beanFactory, beanName, bean)) { + throw ex; + } + } + } + } + + private static void doAddBean(FormatterRegistry registry, Object bean) { + if (bean instanceof GenericConverter) { + registry.addConverter((GenericConverter) bean); + } + else if (bean instanceof Converter) { + registry.addConverter((Converter) bean); + } + else if (bean instanceof Formatter) { + registry.addFormatter((Formatter) bean); + } + else if (bean instanceof Printer) { + registry.addPrinter((Printer) bean); + } + else if (bean instanceof Parser) { + registry.addParser((Parser) bean); + } + } + + private static boolean tryAddFactoryMethodBean(FormatterRegistry registry, ListableBeanFactory beanFactory, + String beanName, Object bean) { + ConfigurableListableBeanFactory clbf = getConfigurableListableBeanFactory(beanFactory); + if (clbf == null) { + return false; + } + if (!isFactoryMethod(clbf, beanName)) { + return false; + } + if (bean instanceof Converter) { + return addConverter(registry, clbf, beanName, (Converter) bean); + } + else if (bean instanceof Printer) { + return addPrinter(registry, clbf, beanName, (Printer) bean); + } + else if (bean instanceof Parser) { + return addParser(registry, clbf, beanName, (Parser) bean); + } + return false; + } + + private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) { + ListableBeanFactory bf = beanFactory; + if (bf instanceof ConfigurableApplicationContext) { + bf = ((ConfigurableApplicationContext) bf).getBeanFactory(); + } + if (bf instanceof ConfigurableListableBeanFactory) { + return (ConfigurableListableBeanFactory) bf; + } + return null; + } + + private static boolean isFactoryMethod(ConfigurableListableBeanFactory clbf, String beanName) { + BeanDefinition bd = clbf.getMergedBeanDefinition(beanName); + return bd.getFactoryMethodName() != null; + } + + private static boolean addConverter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, + String beanName, Converter converter) { + ConverterAdapter adapter = getConverterAdapter(beanFactory, beanName, converter); + if (adapter == null) { + return false; + } + registry.addConverter(adapter); + return true; + } + + private static ConverterAdapter getConverterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, + Converter converter) { + ResolvableType[] types = getResolvableType(beanFactory, beanName); + if (types.length < 2) { + return null; + } + return new ConverterAdapter(converter, types[0], types[1]); + } + + private static ResolvableType[] getResolvableType(ConfigurableListableBeanFactory beanFactory, String beanName) { + BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); + ResolvableType resolvableType = beanDefinition.getResolvableType(); + return resolvableType.getGenerics(); + } + + private static boolean addPrinter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, + String beanName, Printer printer) { + PrinterAdapter adapter = getPrinterAdapter(beanFactory, beanName, printer); + if (adapter == null) { + return false; + } + registry.addConverter(adapter); + return true; + } + + private static PrinterAdapter getPrinterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, + Printer printer) { + ResolvableType[] types = getResolvableType(beanFactory, beanName); + if (types.length < 1) { + return null; + } + ConversionService conversionService = beanFactory.getBean(ConversionService.class); + return new PrinterAdapter(types[0].resolve(), printer, conversionService); + } + + private static boolean addParser(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, + String beanName, Parser parser) { + ParserAdapter adapter = getParserAdapter(beanFactory, beanName, parser); + if (adapter == null) { + return false; + } + registry.addConverter(adapter); + return true; + } + + private static ParserAdapter getParserAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, + Parser parser) { + ResolvableType[] types = getResolvableType(beanFactory, beanName); + if (types.length < 1) { + return null; + } + ConversionService conversionService = beanFactory.getBean(ConversionService.class); + return new ParserAdapter(types[0].resolve(), parser, conversionService); + } + + /** + * Adapts a {@link Converter} to a {@link GenericConverter}. + *

+ * Reference from + * {@link org.springframework.core.convert.support.GenericConversionService.ConverterAdapter} + */ + @SuppressWarnings("unchecked") + private static final class ConverterAdapter implements ConditionalGenericConverter { + + private final Converter converter; + + private final ConvertiblePair typeInfo; + + private final ResolvableType targetType; + + ConverterAdapter(Converter converter, ResolvableType sourceType, ResolvableType targetType) { + this.converter = (Converter) converter; + this.typeInfo = new ConvertiblePair(sourceType.toClass(), targetType.toClass()); + this.targetType = targetType; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(this.typeInfo); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + // Check raw type first... + if (this.typeInfo.getTargetType() != targetType.getObjectType()) { + return false; } - else if (bean instanceof Formatter formatter) { - registry.addFormatter(formatter); + // Full check for complex generic type match required? + ResolvableType rt = targetType.getResolvableType(); + if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType) + && !this.targetType.hasUnresolvableGenerics()) { + return false; } - else if (bean instanceof Printer printer) { - registry.addPrinter(printer); + return !(this.converter instanceof ConditionalConverter) + || ((ConditionalConverter) this.converter).matches(sourceType, targetType); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return convertNullSource(sourceType, targetType); } - else if (bean instanceof Parser parser) { - registry.addParser(parser); + return this.converter.convert(source); + } + + @Override + public String toString() { + return (this.typeInfo + " : " + this.converter); + } + + /** + * Template method to convert a {@code null} source. + *

+ * The default implementation returns {@code null} or the Java 8 + * {@link java.util.Optional#empty()} instance if the target type is + * {@code java.util.Optional}. Subclasses may override this to return custom + * {@code null} objects for specific target types. + * @param sourceType the source type to convert from + * @param targetType the target type to convert to + * @return the converted null object + */ + private Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.getObjectType() == Optional.class) { + return Optional.empty(); } + return null; + } + + } + + private static class PrinterAdapter implements GenericConverter { + + private final Class fieldType; + + private final TypeDescriptor printerObjectType; + + @SuppressWarnings("rawtypes") + private final Printer printer; + + private final ConversionService conversionService; + + PrinterAdapter(Class fieldType, Printer printer, ConversionService conversionService) { + this.fieldType = fieldType; + this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer)); + this.printer = printer; + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(this.fieldType, String.class)); } + + @Override + @SuppressWarnings("unchecked") + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (!sourceType.isAssignableTo(this.printerObjectType)) { + source = this.conversionService.convert(source, sourceType, this.printerObjectType); + } + if (source == null) { + return ""; + } + return this.printer.print(source, LocaleContextHolder.getLocale()); + } + + private Class resolvePrinterObjectType(Printer printer) { + return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class); + } + + @Override + public String toString() { + return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer); + } + + } + + private static class ParserAdapter implements GenericConverter { + + private final Class fieldType; + + private final Parser parser; + + private final ConversionService conversionService; + + ParserAdapter(Class fieldType, Parser parser, ConversionService conversionService) { + this.fieldType = fieldType; + this.parser = parser; + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, this.fieldType)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + String text = (String) source; + if (!StringUtils.hasText(text)) { + return null; + } + Object result; + try { + result = this.parser.parse(text, LocaleContextHolder.getLocale()); + } + catch (IllegalArgumentException ex) { + throw ex; + } + catch (Throwable ex) { + throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex); + } + TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass()); + if (!resultType.isAssignableTo(targetType)) { + result = this.conversionService.convert(result, resultType, targetType); + } + return result; + } + + @Override + public String toString() { + return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser); + } + } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java index a0b5ae1fdda..eb62ca85c3d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java @@ -26,6 +26,9 @@ import org.junit.jupiter.api.Test; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; @@ -36,13 +39,18 @@ import org.springframework.format.Printer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link ApplicationConversionService}. * * @author Phillip Webb + * @author Shixiong Guo(viviel) */ class ApplicationConversionServiceTests { @@ -94,6 +102,48 @@ class ApplicationConversionServiceTests { } } + @SuppressWarnings("unchecked") + @Test + void addBeansWhenHasFactoryMethodConverterBeanAddConverter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + FactoryMethodConverter.class)) { + Converter converter = (Converter) context.getBean("converter"); + willThrow(IllegalArgumentException.class).given(this.registry).addConverter(converter); + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addConverter(converter); + verify(this.registry).addConverter(any(GenericConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @SuppressWarnings("unchecked") + @Test + void addBeansWhenHasFactoryMethodPrinterBeanAddPrinter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + FactoryMethodPrinter.class)) { + Printer printer = (Printer) context.getBean("printer"); + willThrow(IllegalArgumentException.class).given(this.registry).addPrinter(printer); + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addPrinter(printer); + verify(this.registry).addConverter(any(GenericConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @SuppressWarnings("unchecked") + @Test + void addBeansWhenHasFactoryMethodParserBeanAddParser() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + FactoryMethodParser.class)) { + Parser parser = (Parser) context.getBean("parser"); + willThrow(IllegalArgumentException.class).given(this.registry).addParser(parser); + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addParser(parser); + verify(this.registry).addConverter(any(GenericConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + @Test void isConvertViaObjectSourceTypeWhenObjectSourceReturnsTrue() { // Uses ObjectToCollectionConverter @@ -191,4 +241,44 @@ class ApplicationConversionServiceTests { } + @Configuration + static class FactoryMethodConverter { + + @Bean + Converter converter() { + return Integer::valueOf; + } + + } + + @Configuration + static class FactoryMethodPrinter { + + @Bean + Printer printer() { + return (object, locale) -> object.toString(); + } + + @Bean + ConversionService conversionService() { + return mock(ConversionService.class); + } + + } + + @Configuration + static class FactoryMethodParser { + + @Bean + Parser parser() { + return (text, locale) -> Integer.valueOf(text); + } + + @Bean + ConversionService conversionService() { + return mock(ConversionService.class); + } + + } + } From dcb2dd597e6593b07ba31264699058d0593c8057 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 29 Jan 2025 18:43:34 -0800 Subject: [PATCH 2/2] Polish 'Support lambda based converters via bean method signature generics' See gh-22885 --- .../convert/ApplicationConversionService.java | 432 ++++++++---------- .../ApplicationConversionServiceTests.java | 209 +++++++-- 2 files changed, 375 insertions(+), 266 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java index e02e26281b9..92e4479b00a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,18 +17,19 @@ package org.springframework.boot.convert; import java.lang.annotation.Annotation; -import java.util.Collections; -import java.util.LinkedHashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.GenericTypeResolver; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -48,6 +49,7 @@ import org.springframework.format.Parser; import org.springframework.format.Printer; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -61,11 +63,13 @@ import org.springframework.util.StringValueResolver; * against registry instance. * * @author Phillip Webb - * @author Shixiong Guo(viviel) + * @author Shixiong Guo * @since 2.0.0 */ public class ApplicationConversionService extends FormattingConversionService { + private static final ResolvableType STRING = ResolvableType.forClass(String.class); + private static volatile ApplicationConversionService sharedInstance; private final boolean unmodifiable; @@ -278,301 +282,205 @@ public class ApplicationConversionService extends FormattingConversionService { } /** - * Add {@link GenericConverter}, {@link Converter}, {@link Printer}, {@link Parser} - * and {@link Formatter} beans from the specified context. + * Add {@link Printer}, {@link Parser}, {@link Formatter}, {@link Converter}, + * {@link ConverterFactory}, {@link GenericConverter}, and beans from the specified + * bean factory. * @param registry the service to register beans with * @param beanFactory the bean factory to get the beans from * @since 2.2.0 */ public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) { - Set> entries = new LinkedHashSet<>(); - entries.addAll(beanFactory.getBeansOfType(GenericConverter.class).entrySet()); - entries.addAll(beanFactory.getBeansOfType(Converter.class).entrySet()); - entries.addAll(beanFactory.getBeansOfType(Printer.class).entrySet()); - entries.addAll(beanFactory.getBeansOfType(Parser.class).entrySet()); - for (Map.Entry e : entries) { - String beanName = e.getKey(); - Object bean = e.getValue(); - try { - doAddBean(registry, bean); - } - catch (IllegalArgumentException ex) { - if (!tryAddFactoryMethodBean(registry, beanFactory, beanName, bean)) { - throw ex; - } - } - } - } - - private static void doAddBean(FormatterRegistry registry, Object bean) { - if (bean instanceof GenericConverter) { - registry.addConverter((GenericConverter) bean); - } - else if (bean instanceof Converter) { - registry.addConverter((Converter) bean); - } - else if (bean instanceof Formatter) { - registry.addFormatter((Formatter) bean); - } - else if (bean instanceof Printer) { - registry.addPrinter((Printer) bean); - } - else if (bean instanceof Parser) { - registry.addParser((Parser) bean); - } - } - - private static boolean tryAddFactoryMethodBean(FormatterRegistry registry, ListableBeanFactory beanFactory, - String beanName, Object bean) { - ConfigurableListableBeanFactory clbf = getConfigurableListableBeanFactory(beanFactory); - if (clbf == null) { - return false; - } - if (!isFactoryMethod(clbf, beanName)) { - return false; - } - if (bean instanceof Converter) { - return addConverter(registry, clbf, beanName, (Converter) bean); - } - else if (bean instanceof Printer) { - return addPrinter(registry, clbf, beanName, (Printer) bean); - } - else if (bean instanceof Parser) { - return addParser(registry, clbf, beanName, (Parser) bean); - } - return false; + ConfigurableListableBeanFactory configurableBeanFactory = getConfigurableListableBeanFactory(beanFactory); + getBeans(beanFactory).forEach((beanName, bean) -> { + BeanDefinition beanDefinition = (configurableBeanFactory != null) + ? configurableBeanFactory.getMergedBeanDefinition(beanName) : null; + ResolvableType type = (beanDefinition != null) ? beanDefinition.getResolvableType() : null; + addBean(registry, bean, type); + }); } private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) { - ListableBeanFactory bf = beanFactory; - if (bf instanceof ConfigurableApplicationContext) { - bf = ((ConfigurableApplicationContext) bf).getBeanFactory(); + if (beanFactory instanceof ConfigurableApplicationContext applicationContext) { + return applicationContext.getBeanFactory(); } - if (bf instanceof ConfigurableListableBeanFactory) { - return (ConfigurableListableBeanFactory) bf; + if (beanFactory instanceof ConfigurableListableBeanFactory configurableListableBeanFactory) { + return configurableListableBeanFactory; } return null; } - private static boolean isFactoryMethod(ConfigurableListableBeanFactory clbf, String beanName) { - BeanDefinition bd = clbf.getMergedBeanDefinition(beanName); - return bd.getFactoryMethodName() != null; + private static Map getBeans(ListableBeanFactory beanFactory) { + Map beans = new LinkedHashMap<>(); + beans.putAll(getBeans(beanFactory, Printer.class)); + beans.putAll(getBeans(beanFactory, Parser.class)); + beans.putAll(getBeans(beanFactory, Formatter.class)); + beans.putAll(getBeans(beanFactory, Converter.class)); + beans.putAll(getBeans(beanFactory, ConverterFactory.class)); + beans.putAll(getBeans(beanFactory, GenericConverter.class)); + return beans; } - private static boolean addConverter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, - String beanName, Converter converter) { - ConverterAdapter adapter = getConverterAdapter(beanFactory, beanName, converter); - if (adapter == null) { - return false; - } - registry.addConverter(adapter); - return true; + private static Map getBeans(ListableBeanFactory beanFactory, Class type) { + return beanFactory.getBeansOfType(type); } - private static ConverterAdapter getConverterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, - Converter converter) { - ResolvableType[] types = getResolvableType(beanFactory, beanName); - if (types.length < 2) { - return null; + static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) { + if (bean instanceof GenericConverter converterBean) { + addBean(registry, converterBean, beanType, GenericConverter.class, registry::addConverter, (Runnable) null); } - return new ConverterAdapter(converter, types[0], types[1]); - } - - private static ResolvableType[] getResolvableType(ConfigurableListableBeanFactory beanFactory, String beanName) { - BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); - ResolvableType resolvableType = beanDefinition.getResolvableType(); - return resolvableType.getGenerics(); - } - - private static boolean addPrinter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, - String beanName, Printer printer) { - PrinterAdapter adapter = getPrinterAdapter(beanFactory, beanName, printer); - if (adapter == null) { - return false; + else if (bean instanceof Converter converterBean) { + addBean(registry, converterBean, beanType, Converter.class, registry::addConverter, + ConverterBeanAdapter::new); } - registry.addConverter(adapter); - return true; - } - - private static PrinterAdapter getPrinterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, - Printer printer) { - ResolvableType[] types = getResolvableType(beanFactory, beanName); - if (types.length < 1) { - return null; + else if (bean instanceof ConverterFactory converterBean) { + addBean(registry, converterBean, beanType, ConverterFactory.class, registry::addConverterFactory, + ConverterFactoryBeanAdapter::new); + } + else if (bean instanceof Formatter formatterBean) { + addBean(registry, formatterBean, beanType, Formatter.class, registry::addFormatter, () -> { + registry.addConverter(new PrinterBeanAdapter(formatterBean, beanType)); + registry.addConverter(new ParserBeanAdapter(formatterBean, beanType)); + }); + } + else if (bean instanceof Printer printerBean) { + addBean(registry, printerBean, beanType, Printer.class, registry::addPrinter, PrinterBeanAdapter::new); + } + else if (bean instanceof Parser parserBean) { + addBean(registry, parserBean, beanType, Parser.class, registry::addParser, ParserBeanAdapter::new); } - ConversionService conversionService = beanFactory.getBean(ConversionService.class); - return new PrinterAdapter(types[0].resolve(), printer, conversionService); } - private static boolean addParser(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, - String beanName, Parser parser) { - ParserAdapter adapter = getParserAdapter(beanFactory, beanName, parser); - if (adapter == null) { - return false; - } - registry.addConverter(adapter); - return true; + private static void addBean(FormatterRegistry registry, B bean, ResolvableType beanType, Class type, + Consumer standardRegistrar, BiFunction> beanAdapterFactory) { + addBean(registry, bean, beanType, type, standardRegistrar, + () -> registry.addConverter(beanAdapterFactory.apply(bean, beanType))); } - private static ParserAdapter getParserAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, - Parser parser) { - ResolvableType[] types = getResolvableType(beanFactory, beanName); - if (types.length < 1) { - return null; + private static void addBean(FormatterRegistry registry, B bean, ResolvableType beanType, Class type, + Consumer standardRegistrar, Runnable beanAdapterRegistrar) { + if (beanType != null && beanAdapterRegistrar != null + && ResolvableType.forInstance(bean).as(type).hasUnresolvableGenerics()) { + beanAdapterRegistrar.run(); + return; } - ConversionService conversionService = beanFactory.getBean(ConversionService.class); - return new ParserAdapter(types[0].resolve(), parser, conversionService); + standardRegistrar.accept(bean); } /** - * Adapts a {@link Converter} to a {@link GenericConverter}. - *

- * Reference from - * {@link org.springframework.core.convert.support.GenericConversionService.ConverterAdapter} + * Base class for adapters that adapt a bean to a {@link GenericConverter}. + * + * @param the base type of the bean */ - @SuppressWarnings("unchecked") - private static final class ConverterAdapter implements ConditionalGenericConverter { + abstract static class BeanAdapter implements ConditionalGenericConverter { + + private final B bean; - private final Converter converter; + private final ResolvableTypePair types; - private final ConvertiblePair typeInfo; + BeanAdapter(B bean, ResolvableType beanType) { + Assert.isInstanceOf(beanType.toClass(), bean); + ResolvableType type = ResolvableType.forClass(getClass()).as(BeanAdapter.class).getGeneric(); + ResolvableType[] generics = beanType.as(type.toClass()).getGenerics(); + this.bean = bean; + this.types = getResolvableTypePair(generics); + } - private final ResolvableType targetType; + protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) { + return new ResolvableTypePair(generics[0], generics[1]); + } - ConverterAdapter(Converter converter, ResolvableType sourceType, ResolvableType targetType) { - this.converter = (Converter) converter; - this.typeInfo = new ConvertiblePair(sourceType.toClass(), targetType.toClass()); - this.targetType = targetType; + protected B bean() { + return this.bean; } @Override public Set getConvertibleTypes() { - return Collections.singleton(this.typeInfo); + return Set.of(new ConvertiblePair(this.types.source().toClass(), this.types.target().toClass())); } @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - // Check raw type first... - if (this.typeInfo.getTargetType() != targetType.getObjectType()) { - return false; - } - // Full check for complex generic type match required? - ResolvableType rt = targetType.getResolvableType(); - if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType) - && !this.targetType.hasUnresolvableGenerics()) { - return false; - } - return !(this.converter instanceof ConditionalConverter) - || ((ConditionalConverter) this.converter).matches(sourceType, targetType); + return (this.types.target().toClass() == targetType.getObjectType() + && matchesTargetType(targetType.getResolvableType())); } - @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (source == null) { - return convertNullSource(sourceType, targetType); - } - return this.converter.convert(source); + private boolean matchesTargetType(ResolvableType targetType) { + ResolvableType ours = this.types.target(); + return targetType.getType() instanceof Class || targetType.isAssignableFrom(ours) + || this.types.target().hasUnresolvableGenerics(); } - @Override - public String toString() { - return (this.typeInfo + " : " + this.converter); - } - - /** - * Template method to convert a {@code null} source. - *

- * The default implementation returns {@code null} or the Java 8 - * {@link java.util.Optional#empty()} instance if the target type is - * {@code java.util.Optional}. Subclasses may override this to return custom - * {@code null} objects for specific target types. - * @param sourceType the source type to convert from - * @param targetType the target type to convert to - * @return the converted null object - */ - private Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) { - if (targetType.getObjectType() == Optional.class) { - return Optional.empty(); - } - return null; + protected final boolean conditionalConverterCandidateMatches(Object conditionalConverterCandidate, + TypeDescriptor sourceType, TypeDescriptor targetType) { + return (conditionalConverterCandidate instanceof ConditionalConverter conditionalConverter) + ? conditionalConverter.matches(sourceType, targetType) : true; } - } - - private static class PrinterAdapter implements GenericConverter { + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected final Object convert(Object source, TypeDescriptor targetType, Converter converter) { + return (source != null) ? ((Converter) converter).convert(source) : convertNull(targetType); + } - private final Class fieldType; + private Object convertNull(TypeDescriptor targetType) { + return (targetType.getObjectType() != Optional.class) ? null : Optional.empty(); + } - private final TypeDescriptor printerObjectType; + @Override + public String toString() { + return this.types + " : " + this.bean; + } - @SuppressWarnings("rawtypes") - private final Printer printer; + } - private final ConversionService conversionService; + /** + * Adapts a {@link Printer} bean to a {@link GenericConverter}. + */ + static class PrinterBeanAdapter extends BeanAdapter> { - PrinterAdapter(Class fieldType, Printer printer, ConversionService conversionService) { - this.fieldType = fieldType; - this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer)); - this.printer = printer; - this.conversionService = conversionService; + PrinterBeanAdapter(Printer bean, ResolvableType beanType) { + super(bean, beanType); } @Override - public Set getConvertibleTypes() { - return Collections.singleton(new ConvertiblePair(this.fieldType, String.class)); + protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) { + return new ResolvableTypePair(generics[0], STRING); } @Override - @SuppressWarnings("unchecked") public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (!sourceType.isAssignableTo(this.printerObjectType)) { - source = this.conversionService.convert(source, sourceType, this.printerObjectType); - } - if (source == null) { - return ""; - } - return this.printer.print(source, LocaleContextHolder.getLocale()); - } - - private Class resolvePrinterObjectType(Printer printer) { - return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class); + return (source != null) ? print(source) : ""; } - @Override - public String toString() { - return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer); + @SuppressWarnings("unchecked") + private String print(Object object) { + return ((Printer) bean()).print(object, LocaleContextHolder.getLocale()); } } - private static class ParserAdapter implements GenericConverter { - - private final Class fieldType; - - private final Parser parser; - - private final ConversionService conversionService; + /** + * Adapts a {@link Parser} bean to a {@link GenericConverter}. + */ + static class ParserBeanAdapter extends BeanAdapter> { - ParserAdapter(Class fieldType, Parser parser, ConversionService conversionService) { - this.fieldType = fieldType; - this.parser = parser; - this.conversionService = conversionService; + ParserBeanAdapter(Parser bean, ResolvableType beanType) { + super(bean, beanType); } @Override - public Set getConvertibleTypes() { - return Collections.singleton(new ConvertiblePair(String.class, this.fieldType)); + protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) { + return new ResolvableTypePair(STRING, generics[0]); } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { String text = (String) source; - if (!StringUtils.hasText(text)) { - return null; - } - Object result; + return (!StringUtils.hasText(text)) ? null : parse(text); + } + + private Object parse(String text) { try { - result = this.parser.parse(text, LocaleContextHolder.getLocale()); + return bean().parse(text, LocaleContextHolder.getLocale()); } catch (IllegalArgumentException ex) { throw ex; @@ -580,16 +488,76 @@ public class ApplicationConversionService extends FormattingConversionService { catch (Throwable ex) { throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex); } - TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass()); - if (!resultType.isAssignableTo(targetType)) { - result = this.conversionService.convert(result, resultType, targetType); - } - return result; + } + + } + + /** + * Adapts a {@link Converter} bean to a {@link GenericConverter}. + */ + static final class ConverterBeanAdapter extends BeanAdapter> { + + ConverterBeanAdapter(Converter bean, ResolvableType beanType) { + super(bean, beanType); } @Override - public String toString() { - return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser); + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return super.matches(sourceType, targetType) + && conditionalConverterCandidateMatches(bean(), sourceType, targetType); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return convert(source, targetType, bean()); + } + + } + + /** + * Adapts a {@link ConverterFactory} bean to a {@link GenericConverter}. + */ + private static final class ConverterFactoryBeanAdapter extends BeanAdapter> { + + ConverterFactoryBeanAdapter(ConverterFactory bean, ResolvableType beanType) { + super(bean, beanType); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return super.matches(sourceType, targetType) + && conditionalConverterCandidateMatches(bean(), sourceType, targetType) + && conditionalConverterCandidateMatches(getConverter(targetType::getType), sourceType, targetType); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return convert(source, targetType, getConverter(targetType::getObjectType)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Converter getConverter(Supplier> typeSupplier) { + return ((ConverterFactory) bean()).getConverter(typeSupplier.get()); + } + + } + + /** + * Convertible type information as extracted from bean generics. + * + * @param source the source type + * @param target the target type + */ + record ResolvableTypePair(ResolvableType source, ResolvableType target) { + + ResolvableTypePair { + Assert.notNull(source.resolve(), "'source' cannot be resolved"); + Assert.notNull(target.resolve(), "'target' cannot be resolved"); + } + + @Override + public final String toString() { + return source() + " -> " + target(); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java index eb62ca85c3d..4120879ef2c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,42 +19,53 @@ package org.springframework.boot.convert; import java.text.ParseException; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Set; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Test; +import org.springframework.boot.convert.ApplicationConversionService.ConverterBeanAdapter; +import org.springframework.boot.convert.ApplicationConversionService.ParserBeanAdapter; +import org.springframework.boot.convert.ApplicationConversionService.PrinterBeanAdapter; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; 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.core.convert.converter.GenericConverter; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.format.Parser; import org.springframework.format.Printer; +import org.springframework.format.support.FormattingConversionService; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.withSettings; /** * Tests for {@link ApplicationConversionService}. * * @author Phillip Webb - * @author Shixiong Guo(viviel) + * @author Shixiong Guo */ class ApplicationConversionServiceTests { - private final FormatterRegistry registry = mock(FormatterRegistry.class); + private final FormatterRegistry registry = mock(FormatterRegistry.class, + withSettings().extraInterfaces(ConversionService.class)); @Test void addBeansWhenHasGenericConverterBeanAddConverter() { @@ -102,45 +113,44 @@ class ApplicationConversionServiceTests { } } - @SuppressWarnings("unchecked") @Test - void addBeansWhenHasFactoryMethodConverterBeanAddConverter() { + @SuppressWarnings("unchecked") + void addBeansWhenHasConverterBeanMethodAddConverter() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( - FactoryMethodConverter.class)) { + ConverterBeanMethodConfiguration.class)) { Converter converter = (Converter) context.getBean("converter"); willThrow(IllegalArgumentException.class).given(this.registry).addConverter(converter); ApplicationConversionService.addBeans(this.registry, context); - verify(this.registry).addConverter(converter); - verify(this.registry).addConverter(any(GenericConverter.class)); - verifyNoMoreInteractions(this.registry); + then(this.registry).should().addConverter(any(ConverterBeanAdapter.class)); + then(this.registry).shouldHaveNoMoreInteractions(); } } - @SuppressWarnings("unchecked") @Test - void addBeansWhenHasFactoryMethodPrinterBeanAddPrinter() { + @SuppressWarnings("unchecked") + void addBeansWhenHasPrinterBeanMethodAddPrinter() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( - FactoryMethodPrinter.class)) { + PrinterBeanMethodConfiguration.class)) { Printer printer = (Printer) context.getBean("printer"); willThrow(IllegalArgumentException.class).given(this.registry).addPrinter(printer); ApplicationConversionService.addBeans(this.registry, context); - verify(this.registry).addPrinter(printer); - verify(this.registry).addConverter(any(GenericConverter.class)); - verifyNoMoreInteractions(this.registry); + then(this.registry).should(never()).addPrinter(printer); + then(this.registry).should().addConverter(any(PrinterBeanAdapter.class)); + then(this.registry).shouldHaveNoMoreInteractions(); } } - @SuppressWarnings("unchecked") @Test - void addBeansWhenHasFactoryMethodParserBeanAddParser() { + @SuppressWarnings("unchecked") + void addBeansWhenHasParserBeanMethodAddParser() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( - FactoryMethodParser.class)) { + ParserBeanMethodConfiguration.class)) { Parser parser = (Parser) context.getBean("parser"); willThrow(IllegalArgumentException.class).given(this.registry).addParser(parser); ApplicationConversionService.addBeans(this.registry, context); - verify(this.registry).addParser(parser); - verify(this.registry).addConverter(any(GenericConverter.class)); - verifyNoMoreInteractions(this.registry); + then(this.registry).should(never()).addParser(parser); + then(this.registry).should().addConverter(any(ParserBeanAdapter.class)); + then(this.registry).shouldHaveNoMoreInteractions(); } } @@ -181,6 +191,130 @@ class ApplicationConversionServiceTests { assertUnmodifiableExceptionThrown(() -> instance.removeConvertible(null, null)); } + @Test + void addPrinterBeanWithTypeConvertsUsingTypeInformation() { + FormattingConversionService conversionService = new FormattingConversionService(); + Printer printer = (object, locale) -> object.toString().toUpperCase(locale); + ApplicationConversionService.addBean(conversionService, printer, + ResolvableType.forClassWithGenerics(Printer.class, ExampleRecord.class)); + assertThat(conversionService.convert(new ExampleRecord("test"), String.class)).isEqualTo("TEST"); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert(new OtherRecord("test"), String.class)); + assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addPrinter(printer)) + .withMessageContaining("Unable to extract"); + } + + @Test + void addParserBeanWithTypeConvertsUsingTypeInformation() { + FormattingConversionService conversionService = new FormattingConversionService(); + Parser parser = (text, locale) -> new ExampleRecord(text.toString()); + ApplicationConversionService.addBean(conversionService, parser, + ResolvableType.forClassWithGenerics(Parser.class, ExampleRecord.class)); + assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test")); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert("test", OtherRecord.class)); + assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addParser(parser)) + .withMessageContaining("Unable to extract"); + } + + @Test + @SuppressWarnings("rawtypes") + void addFormatterBeanWithTypeConvertsUsingTypeInformation() { + FormattingConversionService conversionService = new FormattingConversionService(); + Formatter formatter = new Formatter() { + + @Override + public String print(Object object, Locale locale) { + return object.toString().toUpperCase(locale); + } + + @Override + public Object parse(String text, Locale locale) throws ParseException { + return new ExampleRecord(text.toString()); + } + + }; + ApplicationConversionService.addBean(conversionService, formatter, + ResolvableType.forClassWithGenerics(Formatter.class, ExampleRecord.class)); + assertThat(conversionService.convert(new ExampleRecord("test"), String.class)).isEqualTo("TEST"); + assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test")); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert(new OtherRecord("test"), String.class)); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert("test", OtherRecord.class)); + assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addFormatter(formatter)) + .withMessageContaining("Unable to extract"); + } + + @Test + void addConverterBeanWithTypeConvertsUsingTypeInformation() { + FormattingConversionService conversionService = new FormattingConversionService(); + Converter converter = (source) -> new ExampleRecord(source.toString()); + ApplicationConversionService.addBean(conversionService, converter, + ResolvableType.forClassWithGenerics(Converter.class, CharSequence.class, ExampleRecord.class)); + assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test")); + assertThat(conversionService.convert(new StringBuilder("test"), ExampleRecord.class)) + .isEqualTo(new ExampleRecord("test")); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert("test", OtherRecord.class)); + assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addConverter(converter)) + .withMessageContaining("Unable to determine"); + } + + @Test + @SuppressWarnings("rawtypes") + void addConverterBeanWithTypeWhenConditionalChecksCondition() { + FormattingConversionService conversionService = new FormattingConversionService(); + ConditionalConverterConverter converter = new ConditionalConverterConverter() { + + @Override + public Object convert(Object source) { + return new ExampleRecord(source.toString()); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return sourceType.getType() != StringBuilder.class; + } + + }; + ApplicationConversionService.addBean(conversionService, converter, + ResolvableType.forClassWithGenerics(Converter.class, CharSequence.class, ExampleRecord.class)); + assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test")); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert(new StringBuilder("test"), ExampleRecord.class)); + assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addConverter(converter)) + .withMessageContaining("Unable to determine"); + } + + @Test + @SuppressWarnings("unchecked") + void addConverterBeanWithTypeWhenNullSourceCanConvertToOptionEmpty() { + FormattingConversionService conversionService = new FormattingConversionService(); + Converter converter = (source) -> new ExampleRecord(source.toString()); + ApplicationConversionService.addBean(conversionService, converter, + ResolvableType.forClassWithGenerics(Converter.class, CharSequence.class, ExampleRecord.class)); + assertThat(conversionService.convert(null, ExampleRecord.class)).isNull(); + assertThat(conversionService.convert(null, Optional.class)).isEmpty(); + } + + @Test + @SuppressWarnings("rawtypes") + void addConverterFactoryBeanWithTypeConvertsUsingTypeInformation() { + FormattingConversionService conversionService = new FormattingConversionService(); + Converter converter = (source) -> new ExampleRecord(source.toString()); + ConverterFactory converterFactory = (targetType) -> converter; + ApplicationConversionService.addBean(conversionService, converterFactory, + ResolvableType.forClassWithGenerics(ConverterFactory.class, CharSequence.class, ExampleRecord.class)); + assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test")); + assertThat(conversionService.convert(new StringBuilder("test"), ExampleRecord.class)) + .isEqualTo(new ExampleRecord("test")); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert("test", OtherRecord.class)); + assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addConverterFactory(converterFactory)) + .withMessageContaining("Unable to determine"); + } + private void assertUnmodifiableExceptionThrown(ThrowingCallable throwingCallable) { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(throwingCallable) .withMessage("This ApplicationConversionService cannot be modified"); @@ -242,7 +376,7 @@ class ApplicationConversionServiceTests { } @Configuration - static class FactoryMethodConverter { + static class ConverterBeanMethodConfiguration { @Bean Converter converter() { @@ -252,33 +386,40 @@ class ApplicationConversionServiceTests { } @Configuration - static class FactoryMethodPrinter { + static class PrinterBeanMethodConfiguration { @Bean Printer printer() { return (object, locale) -> object.toString(); } - @Bean - ConversionService conversionService() { - return mock(ConversionService.class); - } - } @Configuration - static class FactoryMethodParser { + static class ParserBeanMethodConfiguration { @Bean Parser parser() { return (text, locale) -> Integer.valueOf(text); } - @Bean - ConversionService conversionService() { - return mock(ConversionService.class); + } + + record ExampleRecord(String value) { + + @Override + public final String toString() { + return value(); } } + record OtherRecord(String value) { + + } + + interface ConditionalConverterConverter extends Converter, ConditionalConverter { + + } + }