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 01f74e55fd5..4fee14a9958 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 @@ -15,34 +15,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.ArrayList; -import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Locale; 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.support.DefaultTypeConverter; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.EvaluationException; -import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionException; -import org.springframework.expression.ExpressionParser; -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; @@ -50,498 +25,57 @@ import org.springframework.ui.format.Formatter; * Binds user-entered values to properties of a model object. * @author Keith Donald * - * @param The type of model object this binder binds to + * @param The kind of model object this binder binds to * @see #add(BindingConfiguration) * @see #bind(Map) */ -@SuppressWarnings("unchecked") -public class Binder { - - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - - private T model; - - private Map bindings; - - private Map typeFormatters = new HashMap(); - - private Map annotationFormatters = new HashMap(); - - private ExpressionParser expressionParser; - - private TypeConverter typeConverter; - - private boolean strict = false; - - private static Formatter defaultFormatter = new Formatter() { - public String format(Object object, Locale locale) { - if (object == null) { - return ""; - } else { - return object.toString(); - } - } - - public Object parse(String formatted, Locale locale) throws ParseException { - if (formatted == "") { - return null; - } else { - return formatted; - } - } - }; +public interface Binder { /** - * Creates a new binder for the model object. - * @param model the model object containing properties this binder will bind to + * The model object this binder binds to. + * @return the model object */ - public Binder(T model) { - this.model = model; - bindings = new HashMap(); - int parserConfig = SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull - | SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize; - expressionParser = new SpelExpressionParser(parserConfig); - typeConverter = new DefaultTypeConverter(); - } - + M getModel(); + /** * Configures if this binder is strict; a strict binder requires all bindings to be registered explicitly using {@link #add(BindingConfiguration)}. * An optimistic binder will implicitly create bindings as required to support {@link #bind(Map)} operations. * Default is optimistic. * @param strict strict binder status */ - public void setStrict(boolean strict) { - this.strict = strict; - } + void setStrict(boolean strict); /** * Adds new binding. * @param binding the binding configuration * @return the new binding created from the configuration provided */ - public Binding add(BindingConfiguration binding) { - Binding newBinding; - try { - newBinding = new BindingImpl(binding); - } catch (org.springframework.expression.ParseException e) { - throw new IllegalArgumentException(e); - } - bindings.put(binding.getProperty(), newBinding); - return newBinding; - } + Binding add(BindingConfiguration binding); /** * Adds a Formatter that will format property values of type propertyType. * @param formatter the formatter * @param propertyType the property type */ - public void add(Formatter formatter, Class propertyType) { - if (propertyType.isAnnotation()) { - annotationFormatters.put(propertyType, new SimpleAnnotationFormatterFactory(formatter)); - } else { - typeFormatters.put(propertyType, formatter); - } - } + void add(Formatter formatter, Class propertyType); /** * Adds a AnnotationFormatterFactory that will format values of properties annotated with a specific annotation. * @param factory the annotation formatter factory */ - public void add(AnnotationFormatterFactory factory) { - annotationFormatters.put(getAnnotationType(factory), factory); - } - - /** - * The model object this binder binds to. - * @return the model object - */ - public T getModel() { - return model; - } + void add(AnnotationFormatterFactory factory); /** * Returns the binding for the property. * @param property the property path * @return the binding */ - public Binding getBinding(String property) { - Binding binding = bindings.get(property); - if (binding == null && !strict) { - return add(new BindingConfiguration(property, null)); - } else { - return binding; - } - } + Binding getBinding(String property); /** * Bind values in the map to the properties of the model object. - * TODO return BindingResults with getSuccesses()/getErrors()/etc? - * @param propertyValues the property values map + * @param userValues user-entered values to bind */ - public List bind(List userValues) { - List results = new ArrayList(userValues.size()); - for (UserValue value : userValues) { - BindingImpl binding = (BindingImpl) getBinding(value.getProperty()); - results.add(binding.setValue(value.getValue())); - } - return results; - } + List bind(List userValues); - class BindingImpl implements Binding { - - private Expression property; - - private Formatter formatter; - - public BindingImpl(BindingConfiguration config) throws org.springframework.expression.ParseException { - property = expressionParser.parseExpression(config.getProperty()); - formatter = config.getFormatter(); - } - - // implementing Binding - - public String getValue() { - Object value; - try { - value = property.getValue(createEvaluationContext()); - } catch (ExpressionException e) { - throw new IllegalStateException("Failed to get property expression value - this should not happen", e); - } - return format(value); - } - - public BindingResult setValue(Object value) { - if (value instanceof String) { - return setStringValue((String) value); - } else if (value instanceof String[]) { - return setStringValues((String[]) value); - } else { - return setObjectValue(value); - } - } - - public String format(Object selectableValue) { - Formatter formatter; - try { - formatter = getFormatter(); - } catch (EvaluationException e) { - throw new IllegalStateException("Failed to get property expression value type - this should not happen", e); - } - Class formattedType = getFormattedObjectType(formatter); - selectableValue = typeConverter.convert(selectableValue, formattedType); - return formatter.format(selectableValue, LocaleContextHolder.getLocale()); - } - - public boolean isCollection() { - Class type; - try { - type = getValueType(); - } catch (EvaluationException e) { - throw new IllegalArgumentException("Failed to get property expression value type - this should not happen", e); - } - TypeDescriptor typeDesc = TypeDescriptor.valueOf(type); - return typeDesc.isCollection() || typeDesc.isArray(); - } - - public String[] getCollectionValues() { - Object multiValue; - try { - multiValue = property.getValue(createEvaluationContext()); - } catch (EvaluationException e) { - throw new IllegalStateException("Failed to get property expression value - this should not happen", e); - } - if (multiValue == null) { - return EMPTY_STRING_ARRAY; - } - TypeDescriptor type = TypeDescriptor.valueOf(multiValue.getClass()); - String[] formattedValues; - if (type.isCollection()) { - Collection values = ((Collection) multiValue); - formattedValues = (String[]) Array.newInstance(String.class, values.size()); - copy(values, formattedValues); - } else if (type.isArray()) { - formattedValues = (String[]) Array.newInstance(String.class, Array.getLength(multiValue)); - copy((Iterable) multiValue, formattedValues); - } else { - throw new IllegalStateException(); - } - return formattedValues; - } - - // internal helpers - - private BindingResult setStringValue(String formatted) { - Formatter formatter; - try { - formatter = getFormatter(); - } catch (EvaluationException e) { - // could occur the property was not found or is not readable - // TODO probably should not handle all EL failures, only type conversion & property not found? - return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e); - } - Object parsed; - try { - parsed = formatter.parse(formatted, LocaleContextHolder.getLocale()); - } catch (ParseException e) { - return new InvalidFormatResult(property.getExpressionString(), formatted, e); - } - return setValue(parsed, formatted); - } - - private BindingResult setStringValues(String[] formatted) { - Formatter formatter; - try { - formatter = getFormatter(); - } catch (EvaluationException e) { - // could occur the property was not found or is not readable - // TODO probably should not handle all EL failures, only type conversion & property not found? - return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e); - } - Class parsedType = getFormattedObjectType(formatter); - if (parsedType == null) { - parsedType = String.class; - } - Object parsed = Array.newInstance(parsedType, formatted.length); - for (int i = 0; i < formatted.length; i++) { - Object parsedValue; - try { - parsedValue = formatter.parse(formatted[i], LocaleContextHolder.getLocale()); - } catch (ParseException e) { - return new InvalidFormatResult(property.getExpressionString(), formatted, e); - } - Array.set(parsed, i, parsedValue); - } - return setValue(parsed, formatted); - } - - private BindingResult setObjectValue(Object value) { - return setValue(value, value); - } - - private Formatter getFormatter() throws EvaluationException { - if (formatter != null) { - return formatter; - } else { - Class type = getValueType(); - Formatter formatter = typeFormatters.get(type); - if (formatter != null) { - return formatter; - } else { - Annotation[] annotations = getAnnotations(); - for (Annotation a : annotations) { - AnnotationFormatterFactory factory = annotationFormatters.get(a.annotationType()); - if (factory != null) { - return factory.getFormatter(a); - } - } - return defaultFormatter; - } - } - } - - private Class getValueType() throws EvaluationException { - return property.getValueType(createEvaluationContext()); - } - - private Annotation[] getAnnotations() throws EvaluationException { - return property.getValueTypeDescriptor(createEvaluationContext()).getAnnotations(); - } - - private void copy(Iterable values, String[] formattedValues) { - int i = 0; - for (Object value : values) { - formattedValues[i] = format(value); - i++; - } - } - - private BindingResult setValue(Object parsedValue, Object userValue) { - try { - property.setValue(createEvaluationContext(), parsedValue); - return new SuccessResult(property.getExpressionString(), userValue); - } catch (EvaluationException e) { - return new ExpressionEvaluationErrorResult(property.getExpressionString(), userValue, e); - } - } - - } - - private EvaluationContext createEvaluationContext() { - StandardEvaluationContext context = new StandardEvaluationContext(); - context.setRootObject(model); - context.addPropertyAccessor(new MapAccessor()); - 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() + "]"); - } - - static class SimpleAnnotationFormatterFactory implements AnnotationFormatterFactory { - - private Formatter formatter; - - public SimpleAnnotationFormatterFactory(Formatter formatter) { - this.formatter = formatter; - } - - public Formatter getFormatter(Annotation annotation) { - return formatter; - } - - } - - static class InvalidFormatResult implements BindingResult { - - private String property; - - private Object formatted; - - private ParseException e; - - public InvalidFormatResult(String property, Object formatted, ParseException e) { - this.property = property; - this.formatted = formatted; - this.e = e; - } - - public String getProperty() { - return property; - } - - public boolean isError() { - return true; - } - - public String getErrorCode() { - return "invalidFormat"; - } - - public Throwable getErrorCause() { - return e; - } - - public Object getUserValue() { - return formatted; - } - } - - static class ExpressionEvaluationErrorResult implements BindingResult { - - private String property; - - private Object formatted; - - private EvaluationException e; - - public ExpressionEvaluationErrorResult(String property, Object formatted, EvaluationException e) { - this.property = property; - this.formatted = formatted; - this.e = e; - } - - public String getProperty() { - return property; - } - - public boolean isError() { - return true; - } - - public String getErrorCode() { - if (e.getMessage().startsWith("EL1034E")) { - return "typeConversionFailure"; - } else if (e.getMessage().startsWith("EL1008E")) { - return "propertyNotFound"; - } else { - // TODO return more specific code based on underlying EvaluationException error code - return "couldNotSetValue"; - } - } - - public Throwable getErrorCause() { - return e; - } - - public Object getUserValue() { - return formatted; - } - } - - static class SuccessResult implements BindingResult { - - private String property; - - private Object formatted; - - public SuccessResult(String property, Object formatted) { - this.property = property; - this.formatted = formatted; - } - - public String getProperty() { - return property; - } - - public boolean isError() { - return false; - } - - public String getErrorCode() { - return null; - } - - public Throwable getErrorCause() { - return null; - } - - public Object getUserValue() { - return formatted; - } - } -} +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingConfiguration.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingConfiguration.java index c7c78fa18db..2423fb6fd8a 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingConfiguration.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingConfiguration.java @@ -15,12 +15,13 @@ */ package org.springframework.ui.binding; +import org.springframework.ui.binding.support.GenericBinder; import org.springframework.ui.format.Formatter; /** - * Configuration used to create a new {@link Binding} registered with a {@link Binder}. + * Configuration used to create a new {@link Binding} registered with a {@link GenericBinder}. * @author Keith Donald - * @see Binder#add(BindingConfiguration) + * @see GenericBinder#add(BindingConfiguration) */ public class BindingConfiguration { diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java new file mode 100644 index 00000000000..25d9058cb1f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java @@ -0,0 +1,518 @@ +/* + * 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.binding.support; + +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.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +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.support.DefaultTypeConverter; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; +import org.springframework.expression.ExpressionParser; +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.binding.Binder; +import org.springframework.ui.binding.Binding; +import org.springframework.ui.binding.BindingConfiguration; +import org.springframework.ui.binding.BindingResult; +import org.springframework.ui.binding.UserValue; +import org.springframework.ui.format.AnnotationFormatterFactory; +import org.springframework.ui.format.Formatter; + +/** + * Binds user-entered values to properties of a model object. + * @author Keith Donald + * + * @param The type of model object this binder binds to + * @see #add(BindingConfiguration) + * @see #bind(Map) + */ +@SuppressWarnings("unchecked") +public class GenericBinder implements Binder { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private M model; + + private Map bindings; + + private Map typeFormatters = new HashMap(); + + private Map annotationFormatters = new HashMap(); + + private ExpressionParser expressionParser; + + private TypeConverter typeConverter; + + private boolean strict = false; + + private static Formatter defaultFormatter = new Formatter() { + public String format(Object object, Locale locale) { + if (object == null) { + return ""; + } else { + return object.toString(); + } + } + + public Object parse(String formatted, Locale locale) throws ParseException { + if (formatted == "") { + return null; + } else { + return formatted; + } + } + }; + + /** + * Creates a new binder for the model object. + * @param model the model object containing properties this binder will bind to + */ + public GenericBinder(M model) { + this.model = model; + bindings = new HashMap(); + int parserConfig = SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull + | SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize; + expressionParser = new SpelExpressionParser(parserConfig); + typeConverter = new DefaultTypeConverter(); + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + public Binding add(BindingConfiguration binding) { + Binding newBinding; + try { + newBinding = new BindingImpl(binding); + } catch (org.springframework.expression.ParseException e) { + throw new IllegalArgumentException(e); + } + bindings.put(binding.getProperty(), newBinding); + return newBinding; + } + + public void add(Formatter formatter, Class propertyType) { + if (propertyType.isAnnotation()) { + annotationFormatters.put(propertyType, new SimpleAnnotationFormatterFactory(formatter)); + } else { + typeFormatters.put(propertyType, formatter); + } + } + + public void add(AnnotationFormatterFactory factory) { + annotationFormatters.put(getAnnotationType(factory), factory); + } + + public M getModel() { + return model; + } + + public Binding getBinding(String property) { + Binding binding = bindings.get(property); + if (binding == null && !strict) { + return add(new BindingConfiguration(property, null)); + } else { + return binding; + } + } + + public List bind(List userValues) { + List results = new ArrayList(userValues.size()); + for (UserValue value : userValues) { + BindingImpl binding = (BindingImpl) getBinding(value.getProperty()); + results.add(binding.setValue(value.getValue())); + } + return results; + } + + class BindingImpl implements Binding { + + private Expression property; + + private Formatter formatter; + + public BindingImpl(BindingConfiguration config) throws org.springframework.expression.ParseException { + property = expressionParser.parseExpression(config.getProperty()); + formatter = config.getFormatter(); + } + + // implementing Binding + + public String getValue() { + Object value; + try { + value = property.getValue(createEvaluationContext()); + } catch (ExpressionException e) { + throw new IllegalStateException("Failed to get property expression value - this should not happen", e); + } + return format(value); + } + + public BindingResult setValue(Object value) { + if (value instanceof String) { + return setStringValue((String) value); + } else if (value instanceof String[]) { + return setStringValues((String[]) value); + } else { + return setObjectValue(value); + } + } + + public String format(Object selectableValue) { + Formatter formatter; + try { + formatter = getFormatter(); + } catch (EvaluationException e) { + throw new IllegalStateException("Failed to get property expression value type - this should not happen", e); + } + Class formattedType = getFormattedObjectType(formatter); + selectableValue = typeConverter.convert(selectableValue, formattedType); + return formatter.format(selectableValue, LocaleContextHolder.getLocale()); + } + + public boolean isCollection() { + Class type; + try { + type = getValueType(); + } catch (EvaluationException e) { + throw new IllegalArgumentException("Failed to get property expression value type - this should not happen", e); + } + TypeDescriptor typeDesc = TypeDescriptor.valueOf(type); + return typeDesc.isCollection() || typeDesc.isArray(); + } + + public String[] getCollectionValues() { + Object multiValue; + try { + multiValue = property.getValue(createEvaluationContext()); + } catch (EvaluationException e) { + throw new IllegalStateException("Failed to get property expression value - this should not happen", e); + } + if (multiValue == null) { + return EMPTY_STRING_ARRAY; + } + TypeDescriptor type = TypeDescriptor.valueOf(multiValue.getClass()); + String[] formattedValues; + if (type.isCollection()) { + Collection values = ((Collection) multiValue); + formattedValues = (String[]) Array.newInstance(String.class, values.size()); + copy(values, formattedValues); + } else if (type.isArray()) { + formattedValues = (String[]) Array.newInstance(String.class, Array.getLength(multiValue)); + copy((Iterable) multiValue, formattedValues); + } else { + throw new IllegalStateException(); + } + return formattedValues; + } + + // internal helpers + + private BindingResult setStringValue(String formatted) { + Formatter formatter; + try { + formatter = getFormatter(); + } catch (EvaluationException e) { + // could occur the property was not found or is not readable + // TODO probably should not handle all EL failures, only type conversion & property not found? + return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e); + } + Object parsed; + try { + parsed = formatter.parse(formatted, LocaleContextHolder.getLocale()); + } catch (ParseException e) { + return new InvalidFormatResult(property.getExpressionString(), formatted, e); + } + return setValue(parsed, formatted); + } + + private BindingResult setStringValues(String[] formatted) { + Formatter formatter; + try { + formatter = getFormatter(); + } catch (EvaluationException e) { + // could occur the property was not found or is not readable + // TODO probably should not handle all EL failures, only type conversion & property not found? + return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e); + } + Class parsedType = getFormattedObjectType(formatter); + if (parsedType == null) { + parsedType = String.class; + } + Object parsed = Array.newInstance(parsedType, formatted.length); + for (int i = 0; i < formatted.length; i++) { + Object parsedValue; + try { + parsedValue = formatter.parse(formatted[i], LocaleContextHolder.getLocale()); + } catch (ParseException e) { + return new InvalidFormatResult(property.getExpressionString(), formatted, e); + } + Array.set(parsed, i, parsedValue); + } + return setValue(parsed, formatted); + } + + private BindingResult setObjectValue(Object value) { + return setValue(value, value); + } + + private Formatter getFormatter() throws EvaluationException { + if (formatter != null) { + return formatter; + } else { + Class type = getValueType(); + Formatter formatter = typeFormatters.get(type); + if (formatter != null) { + return formatter; + } else { + Annotation[] annotations = getAnnotations(); + for (Annotation a : annotations) { + AnnotationFormatterFactory factory = annotationFormatters.get(a.annotationType()); + if (factory != null) { + return factory.getFormatter(a); + } + } + return defaultFormatter; + } + } + } + + private Class getValueType() throws EvaluationException { + return property.getValueType(createEvaluationContext()); + } + + private Annotation[] getAnnotations() throws EvaluationException { + return property.getValueTypeDescriptor(createEvaluationContext()).getAnnotations(); + } + + private void copy(Iterable values, String[] formattedValues) { + int i = 0; + for (Object value : values) { + formattedValues[i] = format(value); + i++; + } + } + + private BindingResult setValue(Object parsedValue, Object userValue) { + try { + property.setValue(createEvaluationContext(), parsedValue); + return new SuccessResult(property.getExpressionString(), userValue); + } catch (EvaluationException e) { + return new ExpressionEvaluationErrorResult(property.getExpressionString(), userValue, e); + } + } + + } + + private EvaluationContext createEvaluationContext() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(model); + context.addPropertyAccessor(new MapAccessor()); + 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() + "]"); + } + + static class SimpleAnnotationFormatterFactory implements AnnotationFormatterFactory { + + private Formatter formatter; + + public SimpleAnnotationFormatterFactory(Formatter formatter) { + this.formatter = formatter; + } + + public Formatter getFormatter(Annotation annotation) { + return formatter; + } + + } + + static class InvalidFormatResult implements BindingResult { + + private String property; + + private Object formatted; + + private ParseException e; + + public InvalidFormatResult(String property, Object formatted, ParseException e) { + this.property = property; + this.formatted = formatted; + this.e = e; + } + + public String getProperty() { + return property; + } + + public boolean isError() { + return true; + } + + public String getErrorCode() { + return "invalidFormat"; + } + + public Throwable getErrorCause() { + return e; + } + + public Object getUserValue() { + return formatted; + } + } + + static class ExpressionEvaluationErrorResult implements BindingResult { + + private String property; + + private Object formatted; + + private EvaluationException e; + + public ExpressionEvaluationErrorResult(String property, Object formatted, EvaluationException e) { + this.property = property; + this.formatted = formatted; + this.e = e; + } + + public String getProperty() { + return property; + } + + public boolean isError() { + return true; + } + + public String getErrorCode() { + if (e.getMessage().startsWith("EL1034E")) { + return "typeConversionFailure"; + } else if (e.getMessage().startsWith("EL1008E")) { + return "propertyNotFound"; + } else { + // TODO return more specific code based on underlying EvaluationException error code + return "couldNotSetValue"; + } + } + + public Throwable getErrorCause() { + return e; + } + + public Object getUserValue() { + return formatted; + } + } + + static class SuccessResult implements BindingResult { + + private String property; + + private Object formatted; + + public SuccessResult(String property, Object formatted) { + this.property = property; + this.formatted = formatted; + } + + public String getProperty() { + return property; + } + + public boolean isError() { + return false; + } + + public String getErrorCode() { + return null; + } + + public Throwable getErrorCause() { + return null; + } + + public Object getUserValue() { + return formatted; + } + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/package.html b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/package.html new file mode 100644 index 00000000000..908b32ace3d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/package.html @@ -0,0 +1,7 @@ + + +

+Default implementation of the Binding API usable in any environment. +

+ + \ No newline at end of file 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/support/GenericBinderTests.java similarity index 87% rename from org.springframework.context/src/test/java/org/springframework/ui/binding/BinderTests.java rename to org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java index f2f6597a96b..722498ba30f 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/support/GenericBinderTests.java @@ -1,4 +1,4 @@ -package org.springframework.ui.binding; +package org.springframework.ui.binding.support; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -19,13 +19,19 @@ import org.junit.Before; import org.junit.Test; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.expression.EvaluationException; +import org.springframework.ui.binding.Binder; +import org.springframework.ui.binding.Binding; +import org.springframework.ui.binding.BindingConfiguration; +import org.springframework.ui.binding.BindingResult; +import org.springframework.ui.binding.UserValue; +import org.springframework.ui.binding.support.GenericBinder; import org.springframework.ui.format.date.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; -public class BinderTests { +public class GenericBinderTests { @Before public void setUp() { @@ -39,7 +45,7 @@ public class BinderTests { @Test public void bindSingleValuesWithDefaultTypeConverterConversion() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); List values = new ArrayList(); values.add(new UserValue("string", "test")); values.add(new UserValue("integer", "3")); @@ -69,7 +75,7 @@ public class BinderTests { @Test public void bindSingleValuesWithDefaultTypeCoversionFailure() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); List values = new ArrayList(); values.add(new UserValue("string", "test")); // bad value @@ -83,7 +89,7 @@ public class BinderTests { @Test public void bindSingleValuePropertyFormatter() throws ParseException { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.add(new BindingConfiguration("date", new DateFormatter())); binder.bind(UserValue.single("date", "2009-06-01")); assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), binder.getModel().getDate()); @@ -91,14 +97,14 @@ public class BinderTests { @Test public void bindSingleValuePropertyFormatterParseException() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.add(new BindingConfiguration("date", new DateFormatter())); binder.bind(UserValue.single("date", "bogus")); } @Test public void bindSingleValueWithFormatterRegistedByType() throws ParseException { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.add(new DateFormatter(), Date.class); binder.bind(UserValue.single("date", "2009-06-01")); assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), binder.getModel().getDate()); @@ -106,7 +112,7 @@ public class BinderTests { @Test public void bindSingleValueWithFormatterRegisteredByAnnotation() throws ParseException { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.add(new CurrencyFormatter(), CurrencyFormat.class); binder.bind(UserValue.single("currency", "$23.56")); assertEquals(new BigDecimal("23.56"), binder.getModel().getCurrency()); @@ -114,7 +120,7 @@ public class BinderTests { @Test public void bindSingleValueWithnAnnotationFormatterFactoryRegistered() throws ParseException { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.add(new CurrencyAnnotationFormatterFactory()); binder.bind(UserValue.single("currency", "$23.56")); assertEquals(new BigDecimal("23.56"), binder.getModel().getCurrency()); @@ -122,7 +128,7 @@ public class BinderTests { @Test public void bindSingleValuePropertyNotFound() throws ParseException { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); List results = binder.bind(UserValue.single("bogus", "2009-06-01")); assertEquals(1, results.size()); assertTrue(results.get(0).isError()); @@ -131,7 +137,7 @@ public class BinderTests { @Test public void getBindingOptimistic() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); Binding b = binder.getBinding("integer"); assertFalse(b.isCollection()); assertEquals("0", b.getValue()); @@ -142,7 +148,7 @@ public class BinderTests { @Test public void getBindingStrict() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.setStrict(true); Binding b = binder.getBinding("integer"); assertNull(b); @@ -157,7 +163,7 @@ public class BinderTests { @Test public void getBindingCustomFormatter() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.add(new BindingConfiguration("currency", new CurrencyFormatter())); Binding b = binder.getBinding("currency"); assertFalse(b.isCollection()); @@ -168,7 +174,7 @@ public class BinderTests { @Test public void getBindingCustomFormatterRequiringTypeCoersion() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); // IntegerFormatter formats Longs, so conversion from Integer -> Long is performed binder.add(new BindingConfiguration("integer", new IntegerFormatter())); Binding b = binder.getBinding("integer"); @@ -178,7 +184,7 @@ public class BinderTests { @Test public void getBindingMultiValued() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); Binding b = binder.getBinding("foos"); assertTrue(b.isCollection()); assertEquals(0, b.getCollectionValues().length); @@ -195,7 +201,7 @@ public class BinderTests { @Test public void getBindingMultiValuedTypeConversionFailure() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); Binding b = binder.getBinding("foos"); assertTrue(b.isCollection()); assertEquals(0, b.getCollectionValues().length); @@ -208,7 +214,7 @@ public class BinderTests { @Test public void bindHandleNullValueInNestedPath() { TestBean testbean = new TestBean(); - Binder binder = new Binder(testbean); + Binder binder = new GenericBinder(testbean); List values = new ArrayList(); // EL configured with some options from SpelExpressionParserConfiguration: @@ -244,7 +250,7 @@ public class BinderTests { @Test public void formatPossibleValue() { - Binder binder = new Binder(new TestBean()); + Binder binder = new GenericBinder(new TestBean()); binder.add(new BindingConfiguration("currency", new CurrencyFormatter())); Binding b = binder.getBinding("currency"); assertEquals("$5.00", b.format(new BigDecimal("5")));