diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java index 342607ed34..22e0ec58ca 100644 --- a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java @@ -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 generic type?"); + throw new IllegalArgumentException("Unable to extract the parameterized field type from Formatter [" + + formatter.getClass().getName() + "]; does the class parameterize the generic type?"); } return fieldType; } diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java index e7c034c3a1..135c3ced67 100644 --- a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java @@ -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() { @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(); @@ -205,7 +206,7 @@ public class FormattingConversionServiceTests { new TypeDescriptor(modelClass.getField("date")))); assertEquals(new LocalDate(2009, 10, 31), date); - List dates = new ArrayList(); + List dates = new ArrayList<>(); dates.add(new LocalDate(2009, 10, 31).toDateTimeAtCurrentTime().toDate()); dates.add(new LocalDate(2009, 11, 1).toDateTimeAtCurrentTime().toDate()); dates.add(new LocalDate(2009, 11, 2).toDateTimeAtCurrentTime().toDate()); @@ -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 { + @Test(expected = ConversionFailedException.class) + 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 { + @Test(expected = ConversionFailedException.class) + 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 { @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 { + + @Override + public Integer convert(String source) { + return Integer.parseInt(source); + } + } + + + private static class IntegerConverterFactory implements ConverterFactory { + + @Override + @SuppressWarnings("unchecked") + public Converter getConverter(Class targetType) { + if (Integer.class == targetType) { + return (Converter) new IntegerConverter(); + } + else { + throw new IllegalStateException(); + } + } + } + } diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java index f7b1344709..89933b71ff 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java @@ -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 { * Get the converter to convert from S to target type T, where T is also an instance of R. * @param 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 */ Converter getConverter(Class targetType); diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java index 7541d55089..1125b08673 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java @@ -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 */ diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index e051e6cc49..9432b44edf 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; 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; @@ -96,9 +97,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 and targetType " + - " which your Converter 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 and target type for your " + + "Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?"); + } addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1])); } @@ -115,11 +121,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 and target range type R which your " + - "ConverterFactory 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 and target type for your " + + "ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?"); + } + addConverter(new ConverterFactoryAdapter(factory, new ConvertiblePair(typeInfo[0].resolve(), typeInfo[1].resolve()))); } @@ -284,8 +295,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;