Polish 'Support lambda based converters via bean method signature generics'

See gh-22885
This commit is contained in:
Phillip Webb
2025-01-29 18:43:34 -08:00
parent 2921b1428e
commit dcb2dd597e
2 changed files with 379 additions and 270 deletions

View File

@@ -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<Map.Entry<String, ?>> 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<String, ?> 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<String, Object> getBeans(ListableBeanFactory beanFactory) {
Map<String, Object> 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;
private static <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
return beanFactory.getBeansOfType(type);
}
static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) {
if (bean instanceof GenericConverter converterBean) {
addBean(registry, converterBean, beanType, GenericConverter.class, registry::addConverter, (Runnable) null);
}
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;
else if (bean instanceof Converter<?, ?> converterBean) {
addBean(registry, converterBean, beanType, Converter.class, registry::addConverter,
ConverterBeanAdapter::new);
}
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 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);
}
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 <B, T> void addBean(FormatterRegistry registry, B bean, ResolvableType beanType, Class<T> type,
Consumer<B> standardRegistrar, BiFunction<B, ResolvableType, BeanAdapter<?>> beanAdapterFactory) {
addBean(registry, bean, beanType, type, standardRegistrar,
() -> registry.addConverter(beanAdapterFactory.apply(bean, beanType)));
}
private static boolean addParser(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory,
String beanName, Parser<?> parser) {
ParserAdapter adapter = getParserAdapter(beanFactory, beanName, parser);
if (adapter == null) {
return false;
private static <B, T> void addBean(FormatterRegistry registry, B bean, ResolvableType beanType, Class<T> type,
Consumer<B> standardRegistrar, Runnable beanAdapterRegistrar) {
if (beanType != null && beanAdapterRegistrar != null
&& ResolvableType.forInstance(bean).as(type).hasUnresolvableGenerics()) {
beanAdapterRegistrar.run();
return;
}
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);
standardRegistrar.accept(bean);
}
/**
* Adapts a {@link Converter} to a {@link GenericConverter}.
* <p>
* Reference from
* {@link org.springframework.core.convert.support.GenericConversionService.ConverterAdapter}
* Base class for adapters that adapt a bean to a {@link GenericConverter}.
*
* @param <B> the base type of the bean
*/
@SuppressWarnings("unchecked")
private static final class ConverterAdapter implements ConditionalGenericConverter {
abstract static class BeanAdapter<B> implements ConditionalGenericConverter {
private final Converter<Object, Object> converter;
private final B bean;
private final ConvertiblePair typeInfo;
private final ResolvableTypePair types;
private final ResolvableType targetType;
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);
}
ConverterAdapter(Converter<?, ?> converter, ResolvableType sourceType, ResolvableType targetType) {
this.converter = (Converter<Object, Object>) converter;
this.typeInfo = new ConvertiblePair(sourceType.toClass(), targetType.toClass());
this.targetType = targetType;
protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) {
return new ResolvableTypePair(generics[0], generics[1]);
}
protected B bean() {
return this.bean;
}
@Override
public Set<ConvertiblePair> 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();
}
protected final boolean conditionalConverterCandidateMatches(Object conditionalConverterCandidate,
TypeDescriptor sourceType, TypeDescriptor targetType) {
return (conditionalConverterCandidate instanceof ConditionalConverter conditionalConverter)
? conditionalConverter.matches(sourceType, targetType) : true;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected final Object convert(Object source, TypeDescriptor targetType, Converter<?, ?> converter) {
return (source != null) ? ((Converter) converter).convert(source) : convertNull(targetType);
}
private Object convertNull(TypeDescriptor targetType) {
return (targetType.getObjectType() != Optional.class) ? null : Optional.empty();
}
@Override
public String toString() {
return (this.typeInfo + " : " + this.converter);
}
/**
* Template method to convert a {@code null} source.
* <p>
* 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;
return this.types + " : " + this.bean;
}
}
private static class PrinterAdapter implements GenericConverter {
/**
* Adapts a {@link Printer} bean to a {@link GenericConverter}.
*/
static class PrinterBeanAdapter extends BeanAdapter<Printer<?>> {
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;
PrinterBeanAdapter(Printer<?> bean, ResolvableType beanType) {
super(bean, beanType);
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) {
return new ResolvableTypePair(generics[0], STRING);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return (source != null) ? print(source) : "";
}
@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 String print(Object object) {
return ((Printer<Object>) bean()).print(object, LocaleContextHolder.getLocale());
}
}
private static class ParserAdapter implements GenericConverter {
/**
* Adapts a {@link Parser} bean to a {@link GenericConverter}.
*/
static class ParserBeanAdapter extends BeanAdapter<Parser<?>> {
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;
ParserBeanAdapter(Parser<?> bean, ResolvableType beanType) {
super(bean, beanType);
}
@Override
public Set<ConvertiblePair> 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<Converter<?, ?>> {
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<ConverterFactory<?, ?>> {
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<Object, ?> getConverter(Supplier<Class<?>> 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();
}
}

View File

@@ -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<String, Integer> converter = (Converter<String, Integer>) 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<Integer> printer = (Printer<Integer>) 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<Integer> parser = (Parser<Integer>) 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<String, Integer> converter() {
@@ -252,33 +386,40 @@ class ApplicationConversionServiceTests {
}
@Configuration
static class FactoryMethodPrinter {
static class PrinterBeanMethodConfiguration {
@Bean
Printer<Integer> printer() {
return (object, locale) -> object.toString();
}
@Bean
ConversionService conversionService() {
return mock(ConversionService.class);
}
}
@Configuration
static class FactoryMethodParser {
static class ParserBeanMethodConfiguration {
@Bean
Parser<Integer> 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<S, T> extends Converter<S, T>, ConditionalConverter {
}
}