Polish 'Support lambda based converters via bean method signature generics'
See gh-22885
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user