ConversionService detects generic type declaration on target class behind proxy as well
Issue: SPR-14822
This commit is contained in:
parent
52b029d71d
commit
f7d740fa69
|
@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
|
||||
import org.springframework.context.EmbeddedValueResolverAware;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.core.DecoratingProxy;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
|
@ -97,9 +98,13 @@ public class FormattingConversionService extends GenericConversionService
|
|||
|
||||
static Class<?> getFieldType(Formatter<?> formatter) {
|
||||
Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class);
|
||||
if (fieldType == null && formatter instanceof DecoratingProxy) {
|
||||
fieldType = GenericTypeResolver.resolveTypeArgument(
|
||||
((DecoratingProxy) formatter).getDecoratedClass(), Formatter.class);
|
||||
}
|
||||
if (fieldType == null) {
|
||||
throw new IllegalArgumentException("Unable to extract parameterized field type argument from Formatter [" +
|
||||
formatter.getClass().getName() + "]; does the formatter parameterize the <T> generic type?");
|
||||
throw new IllegalArgumentException("Unable to extract the parameterized field type from Formatter [" +
|
||||
formatter.getClass().getName() + "]; does the class parameterize the <T> generic type?");
|
||||
}
|
||||
return fieldType;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ import org.junit.After;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.ConfigurablePropertyAccessor;
|
||||
import org.springframework.beans.PropertyAccessorFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
@ -45,6 +45,7 @@ import org.springframework.context.support.GenericApplicationContext;
|
|||
import org.springframework.core.convert.ConversionFailedException;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.format.Formatter;
|
||||
import org.springframework.format.Printer;
|
||||
|
@ -81,7 +82,7 @@ public class FormattingConversionServiceTests {
|
|||
|
||||
|
||||
@Test
|
||||
public void testFormatFieldForTypeWithFormatter() throws ParseException {
|
||||
public void formatFieldForTypeWithFormatter() throws ParseException {
|
||||
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
|
||||
String formatted = formattingService.convert(3, String.class);
|
||||
assertEquals("3", formatted);
|
||||
|
@ -90,7 +91,7 @@ public class FormattingConversionServiceTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testFormatFieldForTypeWithPrinterParserWithCoercion() throws ParseException {
|
||||
public void formatFieldForTypeWithPrinterParserWithCoercion() throws ParseException {
|
||||
formattingService.addConverter(new Converter<DateTime, LocalDate>() {
|
||||
@Override
|
||||
public LocalDate convert(DateTime source) {
|
||||
|
@ -107,7 +108,7 @@ public class FormattingConversionServiceTests {
|
|||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testFormatFieldForValueInjection() {
|
||||
public void formatFieldForValueInjection() {
|
||||
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
|
||||
ac.registerBeanDefinition("valueBean", new RootBeanDefinition(ValueBean.class));
|
||||
ac.registerBeanDefinition("conversionService", new RootBeanDefinition(FormattingConversionServiceFactoryBean.class));
|
||||
|
@ -118,7 +119,7 @@ public class FormattingConversionServiceTests {
|
|||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testFormatFieldForValueInjectionUsingMetaAnnotations() {
|
||||
public void formatFieldForValueInjectionUsingMetaAnnotations() {
|
||||
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
|
||||
RootBeanDefinition bd = new RootBeanDefinition(MetaValueBean.class);
|
||||
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
|
@ -140,20 +141,20 @@ public class FormattingConversionServiceTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testFormatFieldForAnnotation() throws Exception {
|
||||
public void formatFieldForAnnotation() throws Exception {
|
||||
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory());
|
||||
doTestFormatFieldForAnnotation(Model.class, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatFieldForAnnotationWithDirectFieldAccess() throws Exception {
|
||||
public void formatFieldForAnnotationWithDirectFieldAccess() throws Exception {
|
||||
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory());
|
||||
doTestFormatFieldForAnnotation(Model.class, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testFormatFieldForAnnotationWithPlaceholders() throws Exception {
|
||||
public void formatFieldForAnnotationWithPlaceholders() throws Exception {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
|
||||
Properties props = new Properties();
|
||||
|
@ -169,7 +170,7 @@ public class FormattingConversionServiceTests {
|
|||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testFormatFieldForAnnotationWithPlaceholdersAndFactoryBean() throws Exception {
|
||||
public void formatFieldForAnnotationWithPlaceholdersAndFactoryBean() throws Exception {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
|
||||
Properties props = new Properties();
|
||||
|
@ -239,61 +240,61 @@ public class FormattingConversionServiceTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testPrintNull() throws ParseException {
|
||||
public void printNull() throws ParseException {
|
||||
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
|
||||
assertEquals("", formattingService.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNull() throws ParseException {
|
||||
public void parseNull() throws ParseException {
|
||||
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
|
||||
assertNull(formattingService
|
||||
.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseEmptyString() throws ParseException {
|
||||
public void parseEmptyString() throws ParseException {
|
||||
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
|
||||
assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBlankString() throws ParseException {
|
||||
public void parseBlankString() throws ParseException {
|
||||
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
|
||||
assertNull(formattingService.convert(" ", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test(expected = ConversionFailedException.class)
|
||||
public void testParseParserReturnsNull() throws ParseException {
|
||||
public void parseParserReturnsNull() throws ParseException {
|
||||
formattingService.addFormatterForFieldType(Integer.class, new NullReturningFormatter());
|
||||
assertNull(formattingService.convert("1", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test(expected = ConversionFailedException.class)
|
||||
public void testParseNullPrimitiveProperty() throws ParseException {
|
||||
public void parseNullPrimitiveProperty() throws ParseException {
|
||||
formattingService.addFormatterForFieldType(Integer.class, new NumberStyleFormatter());
|
||||
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(int.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrintNullDefault() throws ParseException {
|
||||
public void printNullDefault() throws ParseException {
|
||||
assertEquals(null, formattingService
|
||||
.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNullDefault() throws ParseException {
|
||||
public void parseNullDefault() throws ParseException {
|
||||
assertNull(formattingService
|
||||
.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseEmptyStringDefault() throws ParseException {
|
||||
public void parseEmptyStringDefault() throws ParseException {
|
||||
assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatFieldForAnnotationWithSubclassAsFieldType() throws Exception {
|
||||
public void formatFieldForAnnotationWithSubclassAsFieldType() throws Exception {
|
||||
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory() {
|
||||
@Override
|
||||
public Printer<?> getPrinter(org.springframework.format.annotation.DateTimeFormat annotation, Class<?> fieldType) {
|
||||
|
@ -319,7 +320,7 @@ public class FormattingConversionServiceTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterDefaultValueViaFormatter() {
|
||||
public void registerDefaultValueViaFormatter() {
|
||||
registerDefaultValue(Date.class, new Date());
|
||||
}
|
||||
|
||||
|
@ -340,6 +341,45 @@ public class FormattingConversionServiceTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void introspectedFormatter() throws ParseException {
|
||||
formattingService.addFormatter(new NumberStyleFormatter());
|
||||
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proxiedFormatter() throws ParseException {
|
||||
Formatter<?> formatter = new NumberStyleFormatter();
|
||||
formattingService.addFormatter((Formatter<?>) new ProxyFactory(formatter).getProxy());
|
||||
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void introspectedConverter() {
|
||||
formattingService.addConverter(new IntegerConverter());
|
||||
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proxiedConverter() {
|
||||
Converter<?, ?> converter = new IntegerConverter();
|
||||
formattingService.addConverter((Converter<?, ?>) new ProxyFactory(converter).getProxy());
|
||||
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void introspectedConverterFactory() {
|
||||
formattingService.addConverterFactory(new IntegerConverterFactory());
|
||||
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proxiedConverterFactory() {
|
||||
ConverterFactory<?, ?> converterFactory = new IntegerConverterFactory();
|
||||
formattingService.addConverterFactory((ConverterFactory<?, ?>) new ProxyFactory(converterFactory).getProxy());
|
||||
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
|
||||
}
|
||||
|
||||
|
||||
public static class ValueBean {
|
||||
|
||||
|
@ -348,6 +388,7 @@ public class FormattingConversionServiceTests {
|
|||
public Date date;
|
||||
}
|
||||
|
||||
|
||||
public static class MetaValueBean {
|
||||
|
||||
@MyDateAnn
|
||||
|
@ -357,18 +398,21 @@ public class FormattingConversionServiceTests {
|
|||
public Double number;
|
||||
}
|
||||
|
||||
|
||||
@Value("${myDate}")
|
||||
@org.springframework.format.annotation.DateTimeFormat(pattern="MM-d-yy")
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public static @interface MyDateAnn {
|
||||
public @interface MyDateAnn {
|
||||
}
|
||||
|
||||
|
||||
@Value("${myNumber}")
|
||||
@NumberFormat(style = NumberFormat.Style.PERCENT)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public static @interface MyNumberAnn {
|
||||
public @interface MyNumberAnn {
|
||||
}
|
||||
|
||||
|
||||
public static class Model {
|
||||
|
||||
@org.springframework.format.annotation.DateTimeFormat(style="S-")
|
||||
|
@ -386,6 +430,7 @@ public class FormattingConversionServiceTests {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static class ModelWithPlaceholders {
|
||||
|
||||
@org.springframework.format.annotation.DateTimeFormat(style="${dateStyle}")
|
||||
|
@ -403,11 +448,13 @@ public class FormattingConversionServiceTests {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@org.springframework.format.annotation.DateTimeFormat(pattern="${datePattern}")
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public static @interface MyDatePattern {
|
||||
public @interface MyDatePattern {
|
||||
}
|
||||
|
||||
|
||||
public static class NullReturningFormatter implements Formatter<Integer> {
|
||||
|
||||
@Override
|
||||
|
@ -419,17 +466,42 @@ public class FormattingConversionServiceTests {
|
|||
public Integer parse(String text, Locale locale) throws ParseException {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class MyDate extends Date {
|
||||
}
|
||||
|
||||
|
||||
private static class ModelWithSubclassField {
|
||||
|
||||
@org.springframework.format.annotation.DateTimeFormat(style = "S-")
|
||||
public MyDate date;
|
||||
}
|
||||
|
||||
|
||||
private static class IntegerConverter implements Converter<String, Integer> {
|
||||
|
||||
@Override
|
||||
public Integer convert(String source) {
|
||||
return Integer.parseInt(source);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class IntegerConverterFactory implements ConverterFactory<String, Number> {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
|
||||
if (Integer.class == targetType) {
|
||||
return (Converter<String, T>) new IntegerConverter();
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
* Copyright 2002-2016 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.
|
||||
|
@ -34,7 +34,7 @@ public interface ConverterFactory<S, R> {
|
|||
* Get the converter to convert from S to target type T, where T is also an instance of R.
|
||||
* @param <T> the target type
|
||||
* @param targetType the target type to convert to
|
||||
* @return A converter from S to T
|
||||
* @return a converter from S to T
|
||||
*/
|
||||
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
|
||||
|
||||
|
|
|
@ -49,12 +49,12 @@ public interface ConverterRegistry {
|
|||
/**
|
||||
* Add a ranged converter factory to this registry.
|
||||
* The convertible source/target type pair is derived from the ConverterFactory's parameterized types.
|
||||
* @throws IllegalArgumentException if the parameterized types could not be resolved.
|
||||
* @throws IllegalArgumentException if the parameterized types could not be resolved
|
||||
*/
|
||||
void addConverterFactory(ConverterFactory<?, ?> converterFactory);
|
||||
void addConverterFactory(ConverterFactory<?, ?> factory);
|
||||
|
||||
/**
|
||||
* Remove any converters from sourceType to targetType.
|
||||
* Remove any converters from {@code sourceType} to {@code targetType}.
|
||||
* @param sourceType the source type
|
||||
* @param targetType the target type
|
||||
*/
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.DecoratingProxy;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionException;
|
||||
import org.springframework.core.convert.ConversionFailedException;
|
||||
|
@ -83,9 +84,14 @@ public class GenericConversionService implements ConfigurableConversionService {
|
|||
|
||||
@Override
|
||||
public void addConverter(Converter<?, ?> converter) {
|
||||
ResolvableType[] typeInfo = getRequiredTypeInfo(converter, Converter.class);
|
||||
Assert.notNull(typeInfo, "Unable to the determine sourceType <S> and targetType " +
|
||||
"<T> which your Converter<S, T> converts between; declare these generic types.");
|
||||
ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
|
||||
if (typeInfo == null && converter instanceof DecoratingProxy) {
|
||||
typeInfo = getRequiredTypeInfo(((DecoratingProxy) converter).getDecoratedClass(), Converter.class);
|
||||
}
|
||||
if (typeInfo == null) {
|
||||
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
|
||||
"Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
|
||||
}
|
||||
addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
|
||||
}
|
||||
|
||||
|
@ -102,11 +108,16 @@ public class GenericConversionService implements ConfigurableConversionService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void addConverterFactory(ConverterFactory<?, ?> converterFactory) {
|
||||
ResolvableType[] typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class);
|
||||
Assert.notNull(typeInfo, "Unable to the determine source type <S> and target range type R which your " +
|
||||
"ConverterFactory<S, R> converts between; declare these generic types.");
|
||||
addConverter(new ConverterFactoryAdapter(converterFactory,
|
||||
public void addConverterFactory(ConverterFactory<?, ?> factory) {
|
||||
ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
|
||||
if (typeInfo == null && factory instanceof DecoratingProxy) {
|
||||
typeInfo = getRequiredTypeInfo(((DecoratingProxy) factory).getDecoratedClass(), ConverterFactory.class);
|
||||
}
|
||||
if (typeInfo == null) {
|
||||
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
|
||||
"ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?");
|
||||
}
|
||||
addConverter(new ConverterFactoryAdapter(factory,
|
||||
new ConvertiblePair(typeInfo[0].resolve(), typeInfo[1].resolve())));
|
||||
}
|
||||
|
||||
|
@ -271,8 +282,8 @@ public class GenericConversionService implements ConfigurableConversionService {
|
|||
|
||||
// Internal helpers
|
||||
|
||||
private ResolvableType[] getRequiredTypeInfo(Object converter, Class<?> genericIfc) {
|
||||
ResolvableType resolvableType = ResolvableType.forClass(converter.getClass()).as(genericIfc);
|
||||
private ResolvableType[] getRequiredTypeInfo(Class<?> converterClass, Class<?> genericIfc) {
|
||||
ResolvableType resolvableType = ResolvableType.forClass(converterClass).as(genericIfc);
|
||||
ResolvableType[] generics = resolvableType.getGenerics();
|
||||
if (generics.length < 2) {
|
||||
return null;
|
||||
|
|
Loading…
Reference in New Issue