diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java index ad17f0dad11..728858aa3cd 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/Binder.java @@ -2,6 +2,9 @@ package org.springframework.ui.binding; import java.lang.annotation.Annotation; import java.lang.reflect.Array; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.text.ParseException; import java.util.Collection; import java.util.HashMap; @@ -10,8 +13,11 @@ import java.util.Map; import org.springframework.context.expression.MapAccessor; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.GenericTypeResolver; import org.springframework.core.convert.TypeConverter; 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.DefaultTypeConverter; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; @@ -22,6 +28,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParserConfiguration; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; +import org.springframework.ui.format.AnnotationFormatterFactory; import org.springframework.ui.format.Formatter; public class Binder { @@ -32,9 +39,9 @@ public class Binder { private Map bindings; - private Map, Formatter> typeFormatters = new HashMap, Formatter>(); + private Map typeFormatters = new HashMap(); - private Map, Formatter> annotationFormatters = new HashMap, Formatter>(); + private Map annotationFormatters = new HashMap(); private ExpressionParser expressionParser; @@ -43,11 +50,7 @@ public class Binder { private boolean strict = false; private static Formatter defaultFormatter = new Formatter() { - - public Class getFormattedObjectType() { - return String.class; - } - + public String format(Object object, Locale locale) { if (object == null) { return ""; @@ -92,15 +95,17 @@ public class Binder { } public void add(Formatter formatter, Class propertyType) { - if (propertyType == null) { - propertyType = formatter.getFormattedObjectType(); - } if (propertyType.isAnnotation()) { - annotationFormatters.put(propertyType, formatter); + annotationFormatters.put(propertyType, new SimpleAnnotationFormatterFactory(formatter)); } else { typeFormatters.put(propertyType, formatter); } } + + // TODO determine Annotation type from factory using reflection + public void add(AnnotationFormatterFactory factory) { + annotationFormatters.put(getAnnotationType(factory), factory); + } public T getModel() { return model; @@ -152,12 +157,13 @@ public class Binder { } public void setValue(String formatted) { - setValue(parse(formatted)); + setValue(parse(formatted, getFormatter())); } public String format(Object selectableValue) { Formatter formatter = getFormatter(); - selectableValue = typeConverter.convert(selectableValue, formatter.getFormattedObjectType()); + Class formattedType = getFormattedObjectType(formatter); + selectableValue = typeConverter.convert(selectableValue, formattedType); return formatter.format(selectableValue, LocaleContextHolder.getLocale()); } @@ -192,9 +198,14 @@ public class Binder { } public void setValues(String[] formattedValues) { - Object values = Array.newInstance(getFormatter().getFormattedObjectType(), formattedValues.length); + Formatter formatter = getFormatter(); + Class parsedType = getFormattedObjectType(formatter); + if (parsedType == null) { + parsedType = String.class; + } + Object values = Array.newInstance(parsedType, formattedValues.length); for (int i = 0; i < formattedValues.length; i++) { - Array.set(values, i, parse(formattedValues[i])); + Array.set(values, i, parse(formattedValues[i], formatter)); } setValue(values); } @@ -205,14 +216,15 @@ public class Binder { // internal helpers - private Object parse(String formatted) { + private Object parse(String formatted, Formatter formatter) { try { - return getFormatter().parse(formatted, LocaleContextHolder.getLocale()); + return formatter.parse(formatted, LocaleContextHolder.getLocale()); } catch (ParseException e) { throw new IllegalArgumentException("Invalid format " + formatted, e); } } + @SuppressWarnings("unchecked") private Formatter getFormatter() { if (formatter != null) { return formatter; @@ -224,9 +236,9 @@ public class Binder { } else { Annotation[] annotations = getAnnotations(); for (Annotation a : annotations) { - formatter = annotationFormatters.get(a.annotationType()); - if (formatter != null) { - return formatter; + AnnotationFormatterFactory factory = annotationFormatters.get(a.annotationType()); + if (factory != null) { + return factory.getFormatter(a); } } return defaultFormatter; @@ -276,4 +288,66 @@ public class Binder { context.setTypeConverter(new StandardTypeConverter(typeConverter)); return context; } + + private Class getAnnotationType(AnnotationFormatterFactory factory) { + Class classToIntrospect = factory.getClass(); + while (classToIntrospect != null) { + Type[] genericInterfaces = classToIntrospect.getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType pInterface = (ParameterizedType) genericInterface; + if (AnnotationFormatterFactory.class.isAssignableFrom((Class) pInterface.getRawType())) { + return getParameterClass(pInterface.getActualTypeArguments()[0], factory.getClass()); + } + } + } + classToIntrospect = classToIntrospect.getSuperclass(); + } + throw new IllegalArgumentException("Unable to extract Annotation type A argument from AnnotationFormatterFactory [" + + factory.getClass().getName() + "]; does the factory parameterize the generic type?"); + } + + private Class getFormattedObjectType(Formatter formatter) { + // TODO consider caching this info + Class classToIntrospect = formatter.getClass(); + while (classToIntrospect != null) { + Type[] genericInterfaces = classToIntrospect.getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType pInterface = (ParameterizedType) genericInterface; + if (Formatter.class.isAssignableFrom((Class) pInterface.getRawType())) { + return getParameterClass(pInterface.getActualTypeArguments()[0], formatter.getClass()); + } + } + } + classToIntrospect = classToIntrospect.getSuperclass(); + } + return null; + } + + private Class getParameterClass(Type parameterType, Class converterClass) { + if (parameterType instanceof TypeVariable) { + parameterType = GenericTypeResolver.resolveTypeVariable((TypeVariable) parameterType, converterClass); + } + if (parameterType instanceof Class) { + return (Class) parameterType; + } + throw new IllegalArgumentException("Unable to obtain the java.lang.Class for parameterType [" + parameterType + + "] on Formatter [" + converterClass.getName() + "]"); + } + + @SuppressWarnings("unchecked") + static class SimpleAnnotationFormatterFactory implements AnnotationFormatterFactory { + + private Formatter formatter; + + public SimpleAnnotationFormatterFactory(Formatter formatter) { + this.formatter = formatter; + } + + public Formatter getFormatter(Annotation annotation) { + return formatter; + } + + } } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java new file mode 100644 index 00000000000..4ffcd9655f9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java @@ -0,0 +1,7 @@ +package org.springframework.ui.format; + +import java.lang.annotation.Annotation; + +public interface AnnotationFormatterFactory { + Formatter getFormatter(A annotation); +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/DateFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/DateFormatter.java index ccafcef39ff..91c4743d4d2 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/DateFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/DateFormatter.java @@ -50,10 +50,6 @@ public class DateFormatter implements Formatter { this.pattern = pattern; } - public Class getFormattedObjectType() { - return Date.class; - } - public String format(Date date, Locale locale) { if (date == null) { return ""; diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java index 063500c353d..1f3b01e9d9f 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java @@ -25,12 +25,6 @@ import java.util.Locale; */ public interface Formatter { - /** - * Returns the type of object this formatter can format. - * @return the formatted object type - */ - Class getFormattedObjectType(); - /** * Format the object of type T for display. * @param object the object to format diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterConverterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterConverterFactory.java deleted file mode 100644 index 89def18352e..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterConverterFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2004-2009 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ui.format; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.ConverterRegistry; - -/** - * A factory for adapting formatting and parsing logic in a {@link Formatter} to the {@link Converter} contract. - * @author Keith Donald - */ -public class FormatterConverterFactory { - - /** - * Register converter adapters for the formatter. - * An adapter will be registered for formatting to String as well as parsing from String. - * @param The type of formatter - * @param formatter the formatter - * @param registry the converter registry - */ - public static void add(Formatter formatter, - ConverterRegistry registry) { - registry.add(new FormattingConverter(formatter)); - registry.add(new ParsingConverter(formatter)); - } - - /** - * Remove the Formatter/converter adapters previously registered for the formatted type. - * @param the formatted type - * @param formattedType the formatted type - * @param registry the converter registry - */ - public static void remove(Class formattedType, - ConverterRegistry registry) { - registry.removeConverter(formattedType, String.class); - registry.removeConverter(String.class, formattedType); - } - -} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/FormattingConverter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/FormattingConverter.java deleted file mode 100644 index e196f04e637..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/FormattingConverter.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2004-2009 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ui.format; - -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.convert.converter.Converter; - -class FormattingConverter implements Converter { - - private Formatter formatter; - - public FormattingConverter(Formatter formatter) { - this.formatter = formatter; - } - - public Class getSourceType() { - return formatter.getFormattedObjectType(); - } - - public Class getTargetType() { - return String.class; - } - - public String convert(T source) { - return formatter.format(source, LocaleContextHolder.getLocale()); - } - -} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/ParsingConverter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/ParsingConverter.java deleted file mode 100644 index 95a37202c44..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/ParsingConverter.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2004-2009 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ui.format; - -import java.text.ParseException; - -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.convert.converter.Converter; - -class ParsingConverter implements Converter { - - private Formatter formatter; - - public ParsingConverter(Formatter formatter) { - this.formatter = formatter; - } - - public T convert(String source) throws ParseException { - return formatter.parse(source, LocaleContextHolder.getLocale()); - } - -} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyAnnotationFormatterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyAnnotationFormatterFactory.java new file mode 100644 index 00000000000..972cfe8c80f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyAnnotationFormatterFactory.java @@ -0,0 +1,12 @@ +package org.springframework.ui.format.number; + +import java.math.BigDecimal; + +import org.springframework.ui.format.AnnotationFormatterFactory; +import org.springframework.ui.format.Formatter; + +public class CurrencyAnnotationFormatterFactory implements AnnotationFormatterFactory { + public Formatter getFormatter(CurrencyFormat annotation) { + return new CurrencyFormatter(); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormat.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormat.java new file mode 100644 index 00000000000..b2bb9fb76b4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormat.java @@ -0,0 +1,18 @@ +package org.springframework.ui.format.number; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A annotation to apply to a BigDecimal property to have property values formatted as currency using a {@link CurrencyFormatter}. + * @author Keith Donald + */ +@Target( { ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CurrencyFormat { + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java index 73f86b681ae..b5ed5c73233 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java @@ -38,10 +38,6 @@ public class CurrencyFormatter implements Formatter { private boolean lenient; - public Class getFormattedObjectType() { - return BigDecimal.class; - } - public String format(BigDecimal decimal, Locale locale) { if (decimal == null) { return ""; diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java index f157e6ec8f2..1513bef9b08 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java @@ -22,7 +22,6 @@ import java.util.Locale; /** * Produces NumberFormat instances that format currency values. - * * @author Keith Donald * @see NumberFormat */ diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java index c4d41b066ce..fa41a4145de 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java @@ -40,10 +40,6 @@ public class DecimalFormatter implements Formatter { initDefaults(); } - public Class getFormattedObjectType() { - return BigDecimal.class; - } - public DecimalFormatter(String pattern) { initDefaults(); formatFactory.setPattern(pattern); diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java index 4c7d282aea0..22421b876f2 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java @@ -33,10 +33,6 @@ public class IntegerFormatter implements Formatter { private boolean lenient; - public Class getFormattedObjectType() { - return Long.class; - } - public String format(Long integer, Locale locale) { if (integer == null) { return ""; diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java index 94304f55210..c57c4e92a4a 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java @@ -35,10 +35,6 @@ public class PercentFormatter implements Formatter { private boolean lenient; - public Class getFormattedObjectType() { - return BigDecimal.class; - } - public String format(BigDecimal decimal, Locale locale) { if (decimal == null) { return ""; diff --git a/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java b/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java index 49f2c93b5d3..3feb9519d03 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java @@ -5,8 +5,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.math.BigDecimal; import java.text.ParseException; import java.util.Date; @@ -22,6 +20,8 @@ import org.junit.Before; import org.junit.Test; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.ui.format.DateFormatter; +import org.springframework.ui.format.number.CurrencyAnnotationFormatterFactory; +import org.springframework.ui.format.number.CurrencyFormat; import org.springframework.ui.format.number.CurrencyFormatter; import org.springframework.ui.format.number.IntegerFormatter; @@ -52,7 +52,7 @@ public class BinderTests { // TODO should update error context, not throw exception @Test(expected=IllegalArgumentException.class) - public void bindSingleValuesWithDefaultTypeCoversionFailures() { + public void bindSingleValuesWithDefaultTypeCoversionFailure() { Binder binder = new Binder(new TestBean()); Map propertyValues = new HashMap(); propertyValues.put("string", "test"); @@ -62,7 +62,7 @@ public class BinderTests { } @Test - public void bindSingleValuePropertyFormatterParsing() throws ParseException { + public void bindSingleValuePropertyFormatter() throws ParseException { Binder binder = new Binder(new TestBean()); binder.add(new BindingConfiguration("date", new DateFormatter())); Map propertyValues = new HashMap(); @@ -82,7 +82,7 @@ public class BinderTests { } @Test - public void bindSingleValueTypeFormatterParsing() throws ParseException { + public void bindSingleValueWithFormatterRegistedByType() throws ParseException { Binder binder = new Binder(new TestBean()); binder.add(new DateFormatter(), Date.class); Map propertyValues = new HashMap(); @@ -92,9 +92,19 @@ public class BinderTests { } @Test - public void bindSingleValueAnnotationFormatterParsing() throws ParseException { + public void bindSingleValueWithFormatterRegisteredByAnnotation() throws ParseException { Binder binder = new Binder(new TestBean()); - binder.add(new CurrencyFormatter(), Currency.class); + binder.add(new CurrencyFormatter(), CurrencyFormat.class); + Map propertyValues = new HashMap(); + propertyValues.put("currency", "$23.56"); + binder.bind(propertyValues); + assertEquals(new BigDecimal("23.56"), binder.getModel().getCurrency()); + } + + @Test + public void bindSingleValueWithnAnnotationFormatterFactoryRegistered() throws ParseException { + Binder binder = new Binder(new TestBean()); + binder.add(new CurrencyAnnotationFormatterFactory()); Map propertyValues = new HashMap(); propertyValues.put("currency", "$23.56"); binder.bind(propertyValues); @@ -261,7 +271,7 @@ public class BinderTests { this.foo = foo; } - @Currency + @CurrencyFormat public BigDecimal getCurrency() { return currency; } @@ -288,11 +298,6 @@ public class BinderTests { } - @Retention(RetentionPolicy.RUNTIME) - public @interface Currency { - - } - public static class Address { private String street; private String city; diff --git a/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java b/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java index 79d6d028d2c..ef9d9eeaa27 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/message/support/DefaultMessageContextTests.java @@ -1,13 +1,15 @@ package org.springframework.ui.message.support; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import java.util.List; import java.util.Locale; import java.util.Map; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.StaticMessageSource; import org.springframework.ui.message.Message; import org.springframework.ui.message.MessageBuilder; @@ -24,6 +26,12 @@ public class DefaultMessageContextTests { messageSource.addMessage("invalidFormat", Locale.US, "{0} must be in format {1}"); messageSource.addMessage("mathForm.decimalField", Locale.US, "Decimal Field"); context = new DefaultMessageContext(messageSource); + LocaleContextHolder.setLocale(Locale.US); + } + + @After + public void tearDown() { + LocaleContextHolder.setLocale(null); } @Test