Merge pull request #22885 from viviel
* pr/22885: Polish 'Support lambda based converters via bean method signature generics' Support lambda based converters via bean method signature generics Closes gh-22885
This commit is contained in:
commit
4eae8a096c
|
@ -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,12 +17,24 @@
|
|||
package org.springframework.boot.convert;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
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.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 +49,8 @@ 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;
|
||||
|
||||
/**
|
||||
|
@ -49,10 +63,13 @@ import org.springframework.util.StringValueResolver;
|
|||
* against registry instance.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @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;
|
||||
|
@ -265,35 +282,284 @@ 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<Object> 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);
|
||||
}
|
||||
else if (bean instanceof Converter<?, ?> converter) {
|
||||
registry.addConverter(converter);
|
||||
}
|
||||
else if (bean instanceof Formatter<?> formatter) {
|
||||
registry.addFormatter(formatter);
|
||||
}
|
||||
else if (bean instanceof Printer<?> printer) {
|
||||
registry.addPrinter(printer);
|
||||
}
|
||||
else if (bean instanceof Parser<?> parser) {
|
||||
registry.addParser(parser);
|
||||
}
|
||||
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) {
|
||||
if (beanFactory instanceof ConfigurableApplicationContext applicationContext) {
|
||||
return applicationContext.getBeanFactory();
|
||||
}
|
||||
if (beanFactory instanceof ConfigurableListableBeanFactory configurableListableBeanFactory) {
|
||||
return configurableListableBeanFactory;
|
||||
}
|
||||
return 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 <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);
|
||||
}
|
||||
else if (bean instanceof Converter<?, ?> converterBean) {
|
||||
addBean(registry, converterBean, beanType, Converter.class, registry::addConverter,
|
||||
ConverterBeanAdapter::new);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 <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;
|
||||
}
|
||||
standardRegistrar.accept(bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for adapters that adapt a bean to a {@link GenericConverter}.
|
||||
*
|
||||
* @param <B> the base type of the bean
|
||||
*/
|
||||
abstract static class BeanAdapter<B> implements ConditionalGenericConverter {
|
||||
|
||||
private final B bean;
|
||||
|
||||
private final ResolvableTypePair types;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) {
|
||||
return new ResolvableTypePair(generics[0], generics[1]);
|
||||
}
|
||||
|
||||
protected B bean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Set.of(new ConvertiblePair(this.types.source().toClass(), this.types.target().toClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
return (this.types.target().toClass() == targetType.getObjectType()
|
||||
&& matchesTargetType(targetType.getResolvableType()));
|
||||
}
|
||||
|
||||
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.types + " : " + this.bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a {@link Printer} bean to a {@link GenericConverter}.
|
||||
*/
|
||||
static class PrinterBeanAdapter extends BeanAdapter<Printer<?>> {
|
||||
|
||||
PrinterBeanAdapter(Printer<?> bean, ResolvableType beanType) {
|
||||
super(bean, beanType);
|
||||
}
|
||||
|
||||
@Override
|
||||
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")
|
||||
private String print(Object object) {
|
||||
return ((Printer<Object>) bean()).print(object, LocaleContextHolder.getLocale());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a {@link Parser} bean to a {@link GenericConverter}.
|
||||
*/
|
||||
static class ParserBeanAdapter extends BeanAdapter<Parser<?>> {
|
||||
|
||||
ParserBeanAdapter(Parser<?> bean, ResolvableType beanType) {
|
||||
super(bean, beanType);
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
return (!StringUtils.hasText(text)) ? null : parse(text);
|
||||
}
|
||||
|
||||
private Object parse(String text) {
|
||||
try {
|
||||
return bean().parse(text, LocaleContextHolder.getLocale());
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,34 +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.never;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
/**
|
||||
* Tests for {@link ApplicationConversionService}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @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() {
|
||||
|
@ -94,6 +113,47 @@ class ApplicationConversionServiceTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void addBeansWhenHasConverterBeanMethodAddConverter() {
|
||||
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
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);
|
||||
then(this.registry).should().addConverter(any(ConverterBeanAdapter.class));
|
||||
then(this.registry).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void addBeansWhenHasPrinterBeanMethodAddPrinter() {
|
||||
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
PrinterBeanMethodConfiguration.class)) {
|
||||
Printer<Integer> printer = (Printer<Integer>) context.getBean("printer");
|
||||
willThrow(IllegalArgumentException.class).given(this.registry).addPrinter(printer);
|
||||
ApplicationConversionService.addBeans(this.registry, context);
|
||||
then(this.registry).should(never()).addPrinter(printer);
|
||||
then(this.registry).should().addConverter(any(PrinterBeanAdapter.class));
|
||||
then(this.registry).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void addBeansWhenHasParserBeanMethodAddParser() {
|
||||
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
ParserBeanMethodConfiguration.class)) {
|
||||
Parser<Integer> parser = (Parser<Integer>) context.getBean("parser");
|
||||
willThrow(IllegalArgumentException.class).given(this.registry).addParser(parser);
|
||||
ApplicationConversionService.addBeans(this.registry, context);
|
||||
then(this.registry).should(never()).addParser(parser);
|
||||
then(this.registry).should().addConverter(any(ParserBeanAdapter.class));
|
||||
then(this.registry).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void isConvertViaObjectSourceTypeWhenObjectSourceReturnsTrue() {
|
||||
// Uses ObjectToCollectionConverter
|
||||
|
@ -131,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");
|
||||
|
@ -191,4 +375,51 @@ class ApplicationConversionServiceTests {
|
|||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ConverterBeanMethodConfiguration {
|
||||
|
||||
@Bean
|
||||
Converter<String, Integer> converter() {
|
||||
return Integer::valueOf;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class PrinterBeanMethodConfiguration {
|
||||
|
||||
@Bean
|
||||
Printer<Integer> printer() {
|
||||
return (object, locale) -> object.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ParserBeanMethodConfiguration {
|
||||
|
||||
@Bean
|
||||
Parser<Integer> parser() {
|
||||
return (text, locale) -> Integer.valueOf(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record ExampleRecord(String value) {
|
||||
|
||||
@Override
|
||||
public final String toString() {
|
||||
return value();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record OtherRecord(String value) {
|
||||
|
||||
}
|
||||
|
||||
interface ConditionalConverterConverter<S, T> extends Converter<S, T>, ConditionalConverter {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue