Added AnnotatioFormatterFactory allowing Formatters to be created from property @Annotation values; polish

This commit is contained in:
Keith Donald 2009-06-07 21:22:37 +00:00
parent 534871e6f6
commit 65c90c56c0
16 changed files with 158 additions and 189 deletions

View File

@ -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<T> {
@ -32,9 +39,9 @@ public class Binder<T> {
private Map<String, Binding> bindings;
private Map<Class<?>, Formatter<?>> typeFormatters = new HashMap<Class<?>, Formatter<?>>();
private Map<Class, Formatter> typeFormatters = new HashMap<Class, Formatter>();
private Map<Class<?>, Formatter<?>> annotationFormatters = new HashMap<Class<?>, Formatter<?>>();
private Map<Class, AnnotationFormatterFactory> annotationFormatters = new HashMap<Class, AnnotationFormatterFactory>();
private ExpressionParser expressionParser;
@ -43,11 +50,7 @@ public class Binder<T> {
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<T> {
}
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<T> {
}
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<T> {
}
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<T> {
// 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<T> {
} 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<T> {
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 <A> 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;
}
}
}

View File

@ -0,0 +1,7 @@
package org.springframework.ui.format;
import java.lang.annotation.Annotation;
public interface AnnotationFormatterFactory<A extends Annotation, T> {
Formatter<T> getFormatter(A annotation);
}

View File

@ -50,10 +50,6 @@ public class DateFormatter implements Formatter<Date> {
this.pattern = pattern;
}
public Class<Date> getFormattedObjectType() {
return Date.class;
}
public String format(Date date, Locale locale) {
if (date == null) {
return "";

View File

@ -25,12 +25,6 @@ import java.util.Locale;
*/
public interface Formatter<T> {
/**
* Returns the type of object this formatter can format.
* @return the formatted object type
*/
Class<T> getFormattedObjectType();
/**
* Format the object of type T for display.
* @param object the object to format

View File

@ -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 <T> The type of formatter
* @param formatter the formatter
* @param registry the converter registry
*/
public static <T> void add(Formatter<T> formatter,
ConverterRegistry registry) {
registry.add(new FormattingConverter<T>(formatter));
registry.add(new ParsingConverter<T>(formatter));
}
/**
* Remove the Formatter/converter adapters previously registered for the formatted type.
* @param <T> the formatted type
* @param formattedType the formatted type
* @param registry the converter registry
*/
public static <T> void remove(Class<T> formattedType,
ConverterRegistry registry) {
registry.removeConverter(formattedType, String.class);
registry.removeConverter(String.class, formattedType);
}
}

View File

@ -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<T> implements Converter<T, String> {
private Formatter<T> formatter;
public FormattingConverter(Formatter<T> formatter) {
this.formatter = formatter;
}
public Class<T> getSourceType() {
return formatter.getFormattedObjectType();
}
public Class<String> getTargetType() {
return String.class;
}
public String convert(T source) {
return formatter.format(source, LocaleContextHolder.getLocale());
}
}

View File

@ -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<T> implements Converter<String, T> {
private Formatter<T> formatter;
public ParsingConverter(Formatter<T> formatter) {
this.formatter = formatter;
}
public T convert(String source) throws ParseException {
return formatter.parse(source, LocaleContextHolder.getLocale());
}
}

View File

@ -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<CurrencyFormat, BigDecimal> {
public Formatter<BigDecimal> getFormatter(CurrencyFormat annotation) {
return new CurrencyFormatter();
}
}

View File

@ -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 {
}

View File

@ -38,10 +38,6 @@ public class CurrencyFormatter implements Formatter<BigDecimal> {
private boolean lenient;
public Class<BigDecimal> getFormattedObjectType() {
return BigDecimal.class;
}
public String format(BigDecimal decimal, Locale locale) {
if (decimal == null) {
return "";

View File

@ -22,7 +22,6 @@ import java.util.Locale;
/**
* Produces NumberFormat instances that format currency values.
*
* @author Keith Donald
* @see NumberFormat
*/

View File

@ -40,10 +40,6 @@ public class DecimalFormatter implements Formatter<BigDecimal> {
initDefaults();
}
public Class<BigDecimal> getFormattedObjectType() {
return BigDecimal.class;
}
public DecimalFormatter(String pattern) {
initDefaults();
formatFactory.setPattern(pattern);

View File

@ -33,10 +33,6 @@ public class IntegerFormatter implements Formatter<Long> {
private boolean lenient;
public Class<Long> getFormattedObjectType() {
return Long.class;
}
public String format(Long integer, Locale locale) {
if (integer == null) {
return "";

View File

@ -35,10 +35,6 @@ public class PercentFormatter implements Formatter<BigDecimal> {
private boolean lenient;
public Class<BigDecimal> getFormattedObjectType() {
return BigDecimal.class;
}
public String format(BigDecimal decimal, Locale locale) {
if (decimal == null) {
return "";

View File

@ -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<TestBean> binder = new Binder<TestBean>(new TestBean());
Map<String, String> propertyValues = new HashMap<String, String>();
propertyValues.put("string", "test");
@ -62,7 +62,7 @@ public class BinderTests {
}
@Test
public void bindSingleValuePropertyFormatterParsing() throws ParseException {
public void bindSingleValuePropertyFormatter() throws ParseException {
Binder<TestBean> binder = new Binder<TestBean>(new TestBean());
binder.add(new BindingConfiguration("date", new DateFormatter()));
Map<String, String> propertyValues = new HashMap<String, String>();
@ -82,7 +82,7 @@ public class BinderTests {
}
@Test
public void bindSingleValueTypeFormatterParsing() throws ParseException {
public void bindSingleValueWithFormatterRegistedByType() throws ParseException {
Binder<TestBean> binder = new Binder<TestBean>(new TestBean());
binder.add(new DateFormatter(), Date.class);
Map<String, String> propertyValues = new HashMap<String, String>();
@ -92,9 +92,19 @@ public class BinderTests {
}
@Test
public void bindSingleValueAnnotationFormatterParsing() throws ParseException {
public void bindSingleValueWithFormatterRegisteredByAnnotation() throws ParseException {
Binder<TestBean> binder = new Binder<TestBean>(new TestBean());
binder.add(new CurrencyFormatter(), Currency.class);
binder.add(new CurrencyFormatter(), CurrencyFormat.class);
Map<String, String> propertyValues = new HashMap<String, String>();
propertyValues.put("currency", "$23.56");
binder.bind(propertyValues);
assertEquals(new BigDecimal("23.56"), binder.getModel().getCurrency());
}
@Test
public void bindSingleValueWithnAnnotationFormatterFactoryRegistered() throws ParseException {
Binder<TestBean> binder = new Binder<TestBean>(new TestBean());
binder.add(new CurrencyAnnotationFormatterFactory());
Map<String, String> propertyValues = new HashMap<String, String>();
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;

View File

@ -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