From 2bbf827d5779c6a08ddbf25e9b8ad28c9f727654 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Wed, 8 Jul 2009 21:43:35 +0000 Subject: [PATCH] numerous binding enhancements; removed lazy binding for time being --- .../springframework/ui/binding/Binder.java | 17 +- .../springframework/ui/binding/Binding.java | 12 +- ...BinderFactory.java => BindingFactory.java} | 23 +- .../ui/binding/BindingResult.java | 3 +- .../binding/MissingSourceValuesException.java | 56 ++ .../org/springframework/ui/binding/Model.java | 6 - .../ui/binding/NoSuchBindingException.java | 17 + .../AnnotatedModelBinderConfigurer.java | 35 +- .../binding/support/BindingConfiguration.java | 48 +- .../support/DefaultBindingConfiguration.java | 61 ++ .../ui/binding/support/GenericBinder.java | 907 +++++++++--------- .../ui/binding/support/WebBinder.java | 2 +- .../ui/binding/support/WebBinderFactory.java | 35 - .../lifecycle/BindAndValidateLifecycle.java | 26 +- .../ui/validation/ValidationFailure.java | 21 + .../ui/validation/ValidationResult.java | 23 - .../ui/validation/ValidationResults.java | 20 - .../ui/validation/Validator.java | 16 +- .../binding/support/GenericBinderTests.java | 180 ++-- .../ui/binding/support/WebBinderTests.java | 7 +- .../BindAndValidateLifecycleTests.java | 47 +- 21 files changed, 805 insertions(+), 757 deletions(-) rename org.springframework.context/src/main/java/org/springframework/ui/binding/{BinderFactory.java => BindingFactory.java} (65%) create mode 100644 org.springframework.context/src/main/java/org/springframework/ui/binding/MissingSourceValuesException.java create mode 100644 org.springframework.context/src/main/java/org/springframework/ui/binding/NoSuchBindingException.java create mode 100644 org.springframework.context/src/main/java/org/springframework/ui/binding/support/DefaultBindingConfiguration.java delete mode 100644 org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinderFactory.java create mode 100644 org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationFailure.java delete mode 100644 org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResult.java delete mode 100644 org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResults.java 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 f9a4b3faf93..3ebc31fda6f 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 @@ -23,25 +23,14 @@ import java.util.Map; * @since 3.0 * @see #bind(Map) */ -public interface Binder { - - /** - * The model object this binder binds to. - * @return the model object - */ - Object getModel(); - - /** - * Returns the binding for the property. - * @param property the property path - * @return the binding - */ - Binding getBinding(String property); +public interface Binder extends BindingFactory { /** * Bind the source values to the properties of the model. + * A result is returned for each registered {@link Binding}. * @param sourceValues the source values to bind * @return the results of the binding operation + * @throws MissingSourceValuesException when the sourceValues Map is missing entries for required bindings */ BindingResults bind(Map sourceValues); diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/Binding.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/Binding.java index 7aa3e474205..6c3f181350d 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/Binding.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/Binding.java @@ -23,14 +23,21 @@ package org.springframework.ui.binding; public interface Binding { /** - * The formatted value to display in the user interface. + * The name of the bound model property. + */ + String getProperty(); + + /** + * The formatted property value to display in the user interface. */ String getValue(); /** - * Set the property associated with this binding to the value provided. + * Set the property to the value provided. * The value may be a formatted String, a formatted String[] if a collection binding, or an Object of a type that can be coersed to the underlying property type. * @param value the new value to bind + * @return a summary of the result of the binding + * @throws BindException if an unrecoverable exception occurs */ BindingResult setValue(Object value); @@ -58,5 +65,4 @@ public interface Binding { */ Class getType(); - } \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/BinderFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingFactory.java similarity index 65% rename from org.springframework.context/src/main/java/org/springframework/ui/binding/BinderFactory.java rename to org.springframework.context/src/main/java/org/springframework/ui/binding/BindingFactory.java index 0ecd4ef4397..b29f8894d01 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/BinderFactory.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingFactory.java @@ -16,16 +16,21 @@ package org.springframework.ui.binding; /** - * A factory for model Binders. + * A factory for model property bindings. * @author Keith Donald - * @since 3.0 */ -public interface BinderFactory { - +public interface BindingFactory { + /** - * Get the Binder for the model - * @param model the model - * @return the binder + * The model object for which property bindings may be accessed. */ - Binder getBinder(Object model); -} + Object getModel(); + + /** + * Get a binding to a model property.. + * @param property the property path + * @throws NoSuchBindingException if no binding to the property exists + */ + Binding getBinding(String property); + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java index 028261c8d9d..ec03e0bafb3 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/BindingResult.java @@ -27,7 +27,8 @@ import org.springframework.ui.alert.Alert; public interface BindingResult { /** - * The name of the model property associated with this binding result. + * The model property this binding result is for. + * @see Binder#getBinding(String) */ String getProperty(); diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/MissingSourceValuesException.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/MissingSourceValuesException.java new file mode 100644 index 00000000000..62012516be2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/MissingSourceValuesException.java @@ -0,0 +1,56 @@ +/* + * 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; + +import java.util.List; +import java.util.Map; + +/** + * Exception thrown by a Binder when a required source value is missing unexpectedly from the sourceValues map. + * Indicates a client configuration error. + * @author Keith Donald + * @see Binder#bind(Map) + */ +public class MissingSourceValuesException extends RuntimeException { + + private List missing; + + /** + * Creates a new missing source values exeption. + * @param missing + * @param sourceValues + */ + public MissingSourceValuesException(List missing, Map sourceValues) { + super(getMessage(missing, sourceValues)); + this.missing = missing; + } + + /** + * The property paths for which source values were missing. + */ + public List getMissing() { + return missing; + } + + private static String getMessage(List missingRequired, Map sourceValues) { + if (missingRequired.size() == 1) { + return "Missing a source value for required propertyPath [" + missingRequired.get(0) + "]; sourceValues map contained " + sourceValues.keySet(); + } else { + return "Missing source values to required propertyPaths " + missingRequired + "; sourceValues map contained " + sourceValues.keySet(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java index ad2b0156752..628e907e984 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java @@ -31,10 +31,4 @@ public @interface Model { */ String value() default ""; - /** - * Configures strict model binding. - * @see Binder#setStrict(boolean) - */ - boolean strictBinding() default false; - } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/NoSuchBindingException.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/NoSuchBindingException.java new file mode 100644 index 00000000000..b184781c623 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/NoSuchBindingException.java @@ -0,0 +1,17 @@ +package org.springframework.ui.binding; + +/** + * Thrown by a BindingFactory when no binding to a property exists. + * @author Keith Donald + * @see BindingFactory#getBinding(String) + */ +public class NoSuchBindingException extends RuntimeException { + + /** + * Creates a new no such binding exception. + * @param property the requested property for which there is no binding + */ + public NoSuchBindingException(String property) { + super("No binding to property '" + property + "' exists"); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/AnnotatedModelBinderConfigurer.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/AnnotatedModelBinderConfigurer.java index 89303de0047..0592a569278 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/AnnotatedModelBinderConfigurer.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/AnnotatedModelBinderConfigurer.java @@ -8,33 +8,26 @@ import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.ui.binding.Bound; -import org.springframework.ui.binding.Model; final class AnnotatedModelBinderConfigurer { public void configure(GenericBinder binder) { Class modelClass = binder.getModel().getClass(); - Model m = AnnotationUtils.findAnnotation(modelClass, Model.class); - if (m != null) { - binder.setStrict(m.strictBinding()); + BeanInfo beanInfo; + try { + beanInfo = Introspector.getBeanInfo(modelClass); + } catch (IntrospectionException e) { + throw new IllegalStateException("Unable to introspect model " + binder.getModel(), e); } - if (binder.isStrict()) { - BeanInfo beanInfo; - try { - beanInfo = Introspector.getBeanInfo(modelClass); - } catch (IntrospectionException e) { - throw new IllegalStateException("Unable to introspect model " + binder.getModel(), e); - } - // TODO do we have to still flush introspector cache here? - for (PropertyDescriptor prop : beanInfo.getPropertyDescriptors()) { - Method getter = prop.getReadMethod(); - Bound b = AnnotationUtils.getAnnotation(getter, Bound.class); - if (b != null) { - // TODO should we wire formatter here if using a format annotation - an optimization? - binder.configureBinding(new BindingConfiguration(prop.getName(), null)); - } + // TODO do we have to still flush introspector cache here? + for (PropertyDescriptor prop : beanInfo.getPropertyDescriptors()) { + Method getter = prop.getReadMethod(); + Bound b = AnnotationUtils.getAnnotation(getter, Bound.class); + if (b != null) { + // TODO should we wire formatter here if using a format annotation - an optimization? + binder.addBinding(prop.getName()); } } - } - + } + } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/BindingConfiguration.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/BindingConfiguration.java index 57e06ab7f26..c74f091ea74 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/BindingConfiguration.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/BindingConfiguration.java @@ -15,43 +15,33 @@ */ package org.springframework.ui.binding.support; -import org.springframework.ui.binding.Binding; import org.springframework.ui.format.Formatter; /** - * Configuration used to create a new {@link Binding} registered with a {@link GenericBinder}. + * A fluent interface for configuring a newly added binding to a property path. * @author Keith Donald - * @since 3.0 - * @see GenericBinder#configureBinding(BindingConfiguration) + * @see GenericBinder#addBinding(String) */ -public final class BindingConfiguration { - - private String property; - - private Formatter formatter; +public interface BindingConfiguration { /** - * Creates a new Binding configuration. - * @param property the property to bind to - * @param formatter the formatter to use to format property values + * Set the Formatter to use to format bound property values. + * If a collection property, the formatter is used to format collection element values. + * Default is null. */ - public BindingConfiguration(String property, Formatter formatter) { - this.property = property; - this.formatter = formatter; - } + BindingConfiguration formatWith(Formatter formatter); /** - * The name of the model property to bind to. + * Mark the binding as required. + * A required binding will generate an exception if no sourceValue is provided to a bind invocation. + * This attribute is used to detect client configuration errors. + * It is not intended to be used as a user data validation constraint. + * Examples: + *
+	 * name=required - will generate an exception if 'name' is not contained in the source values map.
+	 * addresses=required - will generate an exception if 'addresses[n]' is not contained in the source value map for at least one n.
+	 * addresses.city=required - will generate an exception if 'addresses[n].city' is not present in the source values map, for every address[n].
+	 * 
*/ - public String getProperty() { - return property; - } - - /** - * The Formatter to use to format bound property values. - */ - public Formatter getFormatter() { - return formatter; - } - -} + BindingConfiguration required(); +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/DefaultBindingConfiguration.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/DefaultBindingConfiguration.java new file mode 100644 index 00000000000..dadcfe735a7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/DefaultBindingConfiguration.java @@ -0,0 +1,61 @@ +/* + * 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 org.springframework.ui.format.Formatter; + +final class DefaultBindingConfiguration implements BindingConfiguration { + + private String propertyPath; + + private Formatter formatter; + + private boolean required; + + /** + * Creates a new Binding configuration. + * @param property the property to bind to + * @param formatter the formatter to use to format property values + */ + public DefaultBindingConfiguration(String propertyPath) { + this.propertyPath = propertyPath; + } + + // implementing BindingConfiguration + + public BindingConfiguration formatWith(Formatter formatter) { + this.formatter = formatter; + return this; + } + + public BindingConfiguration required() { + this.required = true; + return this; + } + + public String getPropertyPath() { + return propertyPath; + } + + public Formatter getFormatter() { + return formatter; + } + + public boolean isRequired() { + return required; + } + +} 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 index 730caa2dc34..2bcad9f7cfa 100644 --- 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 @@ -22,11 +22,12 @@ import java.lang.reflect.TypeVariable; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import org.springframework.context.MessageSource; import org.springframework.context.expression.MapAccessor; @@ -55,6 +56,8 @@ import org.springframework.ui.binding.Binder; import org.springframework.ui.binding.Binding; import org.springframework.ui.binding.BindingResult; import org.springframework.ui.binding.BindingResults; +import org.springframework.ui.binding.MissingSourceValuesException; +import org.springframework.ui.binding.NoSuchBindingException; import org.springframework.ui.format.AnnotationFormatterFactory; import org.springframework.ui.format.Formatter; import org.springframework.ui.message.MessageBuilder; @@ -65,10 +68,11 @@ import org.springframework.util.Assert; * A generic {@link Binder binder} suitable for use in most environments. * @author Keith Donald * @since 3.0 - * @see #configureBinding(BindingConfiguration) + * @see #addBinding(String) + * @see #registerFormatter(Class, Formatter) + * @see #registerFormatterFactory(AnnotationFormatterFactory) * @see #setFormatterRegistry(FormatterRegistry) * @see #setMessageSource(MessageSource) - * @see #setStrict(boolean) * @see #setTypeConverter(TypeConverter) * @see #bind(Map) */ @@ -79,7 +83,7 @@ public class GenericBinder implements Binder { private Object model; - private Map bindings; + private Set bindingFactories; private FormatterRegistry formatterRegistry = new GenericFormatterRegistry(); @@ -87,8 +91,6 @@ public class GenericBinder implements Binder { private TypeConverter typeConverter; - private boolean strict = false; - private static Formatter defaultFormatter = new DefaultFormatter(); private MessageSource messageSource; @@ -98,86 +100,39 @@ public class GenericBinder implements Binder { * @param model the model object containing properties this binder will bind to */ public GenericBinder(Object model) { - Assert.notNull(model, "The model Object is required"); + Assert.notNull(model, "The model to bind to is required"); this.model = model; - bindings = new HashMap(); + bindingFactories = new LinkedHashSet(); int parserConfig = SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull | SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize; expressionParser = new SpelExpressionParser(parserConfig); typeConverter = new DefaultTypeConverter(); } - public Object getModel() { - return model; - } - /** - * Is this binder strict? - * A strict binder requires all bindings to be registered explicitly using {@link #configureBinding(BindingConfiguration)}. + * Add a binding to a model property. + * The property may be a path to a member property like "name", or a nested property like "address.city" or "addresses.city". + * If the property path is nested and traverses a collection or Map, do not use indexes. + * The property path should express the model-class property structure like addresses.city, not an object structure like addresses[0].city. + * Examples: + *
+	 * name - bind to property 'name'
+	 * addresses - bind to property 'addresses', presumably a List<Address> e.g. allowing property expressions like addresses={ 12345 Macy Lane, 1977 Bel Aire Estates } and addresses[0]=12345 Macy Lane
+	 * addresses.city - bind to property 'addresses.city', for all indexed addresses in the collection e.g. allowing property expressions like addresses[0].city=Melbourne
+	 * address.city - bind to property 'address.city'
+	 * favoriteFoodByFoodGroup - bind to property 'favoriteFoodByFoodGroup', presumably a Map; e.g. allowing favoriteFoodByFoodGroup={ DAIRY=Milk, MEAT=Steak } and favoriteFoodByFoodGroup['DAIRY']=Milk
+	 * favoriteFoodByFoodGroup.name - bind to property 'favoriteFoodByFoodGroup.name', for all keyed Foods in the map; e.g. allowing favoriteFoodByFoodGroup['DAIRY'].name=Milk
+	 * 
+ * @param propertyPath the model property path + * @return a BindingConfiguration object, allowing additional configuration of the newly added binding + * @throws IllegalArgumentException if no such property path exists on the model */ - public boolean isStrict() { - return strict; + public BindingConfiguration addBinding(String propertyPath) { + DefaultBindingConfiguration configuration = new DefaultBindingConfiguration(propertyPath); + bindingFactories.add(new BindingFactory(configuration)); + return configuration; } - /** - * Configures if this binder is strict. - * A strict binder requires all bindings to be registered explicitly using {@link #configureBinding(BindingConfiguration)}. - * An optimistic binder will implicitly create bindings as required to support {@link #bind(UserValues)} operations. - * Default is optimistic. - * @param strict strict binder status - */ - public void setStrict(boolean strict) { - this.strict = strict; - } - - /** - * Configures the registry of Formatters to query when no explicit Formatter has been registered for a Binding. - * Allows Formatters to be applied by property type and by property annotation. - * @param registry the formatter registry - */ - public void setFormatterRegistry(FormatterRegistry formatterRegistry) { - Assert.notNull(formatterRegistry, "The FormatterRegistry is required"); - this.formatterRegistry = formatterRegistry; - } - - /** - * Configure the MessageSource that resolves localized {@link BindingResult} alert messages. - * @param messageSource the message source - */ - public void setMessageSource(MessageSource messageSource) { - Assert.notNull(messageSource, "The MessageSource is required"); - this.messageSource = messageSource; - } - - /** - * Configure the TypeConverter that converts values as required by Binding setValue and getValue attempts. - * For a setValue attempt, the TypeConverter will be asked to perform a conversion if the value parsed by the Binding's Formatter is not assignable to the target property type. - * For a getValue attempt, the TypeConverter will be asked to perform a conversion if the property type does not match the type T required by the Binding's Formatter. - * @param typeConverter the type converter used by the binding system, which is based on Spring EL - * @see EvaluationContext#getTypeConverter() - */ - public void setTypeConverter(TypeConverter typeConverter) { - Assert.notNull(messageSource, "The TypeConverter is required"); - this.typeConverter = typeConverter; - } - - /** - * Configures a new binding on this binder. - * @param configuration the binding configuration - * @return the new binding created from the configuration provided - */ - public Binding configureBinding(BindingConfiguration configuration) { - Binding binding; - try { - // TODO should probably only allow binding to be created if property exists on model - binding = new BindingImpl(configuration); - } catch (org.springframework.expression.ParseException e) { - throw new IllegalArgumentException(e); - } - bindings.put(configuration.getProperty(), binding); - return binding; - } - /** * Register a Formatter to format the model properties of a specific property type. * Convenience method that calls {@link FormatterRegistry#add(Class, Formatter)} internally. @@ -198,27 +153,62 @@ public class GenericBinder implements Binder { formatterRegistry.add(factory); } - public Binding getBinding(String property) { - Binding binding = bindings.get(property); - if (binding == null && !strict) { - return configureBinding(new BindingConfiguration(property, null)); - } else { - return binding; - } + /** + * Configures the registry of Formatters to query when no explicit Formatter has been registered for a Binding. + * Allows Formatters to be applied by property type and by property annotation. + * @param registry the formatter registry + */ + public void setFormatterRegistry(FormatterRegistry formatterRegistry) { + Assert.notNull(formatterRegistry, "The FormatterRegistry is required"); + this.formatterRegistry = formatterRegistry; } + /** + * Configure the MessageSource that resolves localized {@link BindingResult} alert messages. + * @param messageSource the message source + */ + public void setMessageSource(MessageSource messageSource) { + Assert.notNull(messageSource, "The MessageSource is required"); + this.messageSource = messageSource; + } + + /** + * Configure the TypeConverter that converts values as required by Binding setValue and getValue attempts. + * For a setValue attempt, the TypeConverter will be asked to perform a conversion if the value parsed by the Binding's Formatter is not assignable to the target property type. + * For a getValue attempt, the TypeConverter will be asked to perform a conversion if the property type does not match the type T required by the Binding's Formatter. + * @param typeConverter the type converter used by the binding system, which is based on Spring EL + * @see EvaluationContext#getTypeConverter() + */ + public void setTypeConverter(TypeConverter typeConverter) { + Assert.notNull(messageSource, "The TypeConverter is required"); + this.typeConverter = typeConverter; + } + + // implementing BindingFactory + + public Object getModel() { + return model; + } + + public Binding getBinding(String property) { + for (BindingFactory factory: bindingFactories) { + if (factory.hasBindingFor(property)) { + return factory.getBinding(property); + } + } + throw new NoSuchBindingException(property); + } + + // implementing Binder + public BindingResults bind(Map sourceValues) { sourceValues = filter(sourceValues); + checkRequired(sourceValues); ArrayListBindingResults results = new ArrayListBindingResults(sourceValues.size()); for (Map.Entry sourceValue : sourceValues.entrySet()) { String property = sourceValue.getKey(); Object value = sourceValue.getValue(); - BindingImpl binding = (BindingImpl) getBinding(property); - if (binding != null) { - results.add(binding.setValue(value)); - } else { - results.add(new NoSuchBindingResult(property, value)); - } + results.add(getBinding(property).setValue(value)); } return results; } @@ -227,9 +217,9 @@ public class GenericBinder implements Binder { /** * Hook subclasses may use to filter the source values to bind. - * This hook allows the binder to pre-process the source values before binding occurs. -- * For example, a Binder might insert empty or default values for fields that are not present. -- * As another example, a Binder might collapse multiple source values into a single source value. + * This hook allows the binder to pre-process the source values before binding occurs. + * For example, a Binder might insert empty or default values for fields that are not present. + * As another example, a Binder might collapse multiple source values into a single source value. * @param sourceValues the original source values map provided by the caller * @return the filtered source values map that will be used to bind */ @@ -239,6 +229,293 @@ public class GenericBinder implements Binder { // internal helpers + private void checkRequired(Map sourceValues) { + List missingRequired = new ArrayList(); + for (BindingFactory factory : bindingFactories) { + if (factory.configuration.isRequired()) { + boolean found = false; + for (String property : sourceValues.keySet()) { + if (factory.hasBindingFor(property)) { + found = true; + } + } + if (!found) { + missingRequired.add(factory.getPropertyPath()); + } + } + } + if (!missingRequired.isEmpty()) { + throw new MissingSourceValuesException(missingRequired, sourceValues); + } + } + + private EvaluationContext createEvaluationContext() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(model); + context.addPropertyAccessor(new MapAccessor()); + context.setTypeConverter(new StandardTypeConverter(typeConverter)); + return context; + } + + class BindingFactory { + + private DefaultBindingConfiguration configuration; + + public BindingFactory(DefaultBindingConfiguration configuration) { + this.configuration = configuration; + } + + public String getPropertyPath() { + return configuration.getPropertyPath(); + } + + public boolean hasBindingFor(String property) { + String propertyPath = propertyPath(property); + return configuration.getPropertyPath().equals(propertyPath); + } + + public Binding getBinding(String property) { + Expression propertyExpression; + try { + propertyExpression = expressionParser.parseExpression(property); + } catch (org.springframework.expression.ParseException e) { + throw new IllegalArgumentException( + "Unable to get model binding; cannot parse property expression from property string [" + + property + "]", e); + } + try { + propertyExpression.getValueType(createEvaluationContext()); + } catch (EvaluationException e) { + throw new IllegalArgumentException("Unable to get model binding; cannot access property '" + + propertyExpression.getExpressionString() + "'", e); + } + return new BindingImpl(propertyExpression, configuration.getFormatter()); + } + + private String propertyPath(String property) { + StringBuilder sb = new StringBuilder(property); + int searchIndex = 0; + while (searchIndex != -1) { + int keyStart = sb.indexOf("[", searchIndex); + searchIndex = -1; + if (keyStart != -1) { + int keyEnd = sb.indexOf("]", keyStart + 1); + if (keyEnd != -1) { + sb.delete(keyStart, keyEnd + 1); + searchIndex = keyEnd + 1; + } + } + } + return sb.toString(); + } + + } + + class BindingImpl implements Binding { + + private Expression property; + + private Formatter formatter; + + public BindingImpl(Expression property, Formatter formatter) { + this.property = property; + this.formatter = formatter; + } + + // implementing Binding + + public String getProperty() { + return property.getExpressionString(); + } + + 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 = getFormatter(); + Class formattedType = getFormattedObjectType(formatter); + selectableValue = typeConverter.convert(selectableValue, formattedType); + return formatter.format(selectableValue, LocaleContextHolder.getLocale()); + } + + public boolean isCollection() { + Class type = getType(); + 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; + } + + // public impl only + + public Class getType() { + Class type; + try { + type = property.getValueType(createEvaluationContext()); + } catch (EvaluationException e) { + throw new IllegalArgumentException( + "Failed to get property expression value type - this should not happen", e); + } + return type; + } + + // internal helpers + + private BindingResult setStringValue(String formatted) { + Object parsed; + try { + parsed = getFormatter().parse(formatted, LocaleContextHolder.getLocale()); + } catch (ParseException e) { + return new InvalidFormat(property.getExpressionString(), formatted, e); + } + return setValue(parsed, formatted); + } + + private BindingResult setStringValues(String[] formatted) { + Formatter formatter = getFormatter(); + 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 InvalidFormat(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() { + if (formatter != null) { + return formatter; + } else { + TypeDescriptor type; + try { + type = property.getValueTypeDescriptor(createEvaluationContext()); + } catch (EvaluationException e) { + throw new IllegalArgumentException( + "Failed to get property expression value type descriptor - this should not happen", e); + } + Formatter formatter = formatterRegistry.getFormatter(type); + return formatter != null ? formatter : defaultFormatter; + } + } + + 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 Success(property.getExpressionString(), userValue); + } catch (EvaluationException e) { + return new EvaluationError(property.getExpressionString(), userValue, e); + } + } + + 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 DefaultFormatter implements 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; + } + } + } + static class ArrayListBindingResults implements BindingResults { private List results; @@ -301,277 +578,7 @@ public class GenericBinder implements Binder { } - 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 = getType(); - 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; - } - - // public impl only - - public Class getType() { - Class type; - try { - type = property.getValueType(createEvaluationContext()); - } catch (EvaluationException e) { - throw new IllegalArgumentException( - "Failed to get property expression value type - this should not happen", e); - } - return type; - } - - // internal helpers - - private BindingResult setStringValue(String formatted) { - Formatter formatter; - try { - formatter = getFormatter(); - } catch (EvaluationException e) { - // could occur if 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 if 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 { - Formatter formatter = formatterRegistry.getFormatter(property - .getValueTypeDescriptor(createEvaluationContext())); - return formatter != null ? formatter : defaultFormatter; - } - } - - 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 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 DefaultFormatter implements 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; - } - } - } - - class NoSuchBindingResult implements BindingResult { - - private String property; - - private Object sourceValue; - - public NoSuchBindingResult(String property, Object sourceValue) { - this.property = property; - this.sourceValue = sourceValue; - } - - public String getProperty() { - return property; - } - - public Object getSourceValue() { - return sourceValue; - } - - public boolean isFailure() { - return true; - } - - public Alert getAlert() { - return new AbstractAlert() { - public String getCode() { - return "noSuchBinding"; - } - - public Severity getSeverity() { - return Severity.WARNING; - } - - public String getMessage() { - MessageBuilder builder = new MessageBuilder(messageSource); - builder.code(getCode()); - builder.arg("label", new ResolvableArgument(property)); - builder.arg("value", sourceValue); - builder.defaultMessage("Failed to bind to property '" + property - + "'; no binding has been added for the property"); - return builder.build(); - } - }; - } - } - - class InvalidFormatResult implements BindingResult { + class InvalidFormat implements BindingResult { private String property; @@ -579,7 +586,7 @@ public class GenericBinder implements Binder { private ParseException cause; - public InvalidFormatResult(String property, Object sourceValue, ParseException cause) { + public InvalidFormat(String property, Object sourceValue, ParseException cause) { this.property = property; this.sourceValue = sourceValue; this.cause = cause; @@ -621,104 +628,13 @@ public class GenericBinder implements Binder { } } - class ExpressionEvaluationErrorResult implements BindingResult { + class Success implements BindingResult { private String property; private Object sourceValue; - private EvaluationException cause; - - public ExpressionEvaluationErrorResult(String property, Object sourceValue, EvaluationException cause) { - this.property = property; - this.sourceValue = sourceValue; - this.cause = cause; - } - - public String getProperty() { - return property; - } - - public Object getSourceValue() { - return sourceValue; - } - - public boolean isFailure() { - return true; - } - - public Alert getAlert() { - return new AbstractAlert() { - public String getCode() { - SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode(); - if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { - return "conversionFailed"; - } else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { - // TODO should probably force property exists before even creating binding - return "propertyNotFound"; - } else { - return "couldNotSetValue"; - } - } - - public Severity getSeverity() { - SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode(); - if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { - return Severity.FATAL; - } else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { - return Severity.WARNING; - } else { - return Severity.FATAL; - } - } - - public String getMessage() { - SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode(); - if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { - AccessException accessException = (AccessException) cause.getCause(); - if (accessException.getCause() != null) { - Throwable cause = accessException.getCause(); - if (cause instanceof SpelEvaluationException - && ((SpelEvaluationException) cause).getMessageCode() == SpelMessage.TYPE_CONVERSION_ERROR) { - ConversionFailedException failure = (ConversionFailedException) cause.getCause(); - MessageBuilder builder = new MessageBuilder(messageSource); - builder.code("conversionFailed"); - builder.arg("label", new ResolvableArgument(property)); - builder.arg("value", sourceValue); - builder.defaultMessage("Failed to bind to property '" + property + "'; user value " - + StylerUtils.style(sourceValue) + " could not be converted to property type [" - + failure.getTargetType().getName() + "]"); - return builder.build(); - } - } - } else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { - MessageBuilder builder = new MessageBuilder(messageSource); - builder.code(getCode()); - builder.arg("label", new ResolvableArgument(property)); - builder.arg("value", sourceValue); - builder.defaultMessage("Failed to bind to property '" + property + "'; no such property exists on model"); - return builder.build(); - } - MessageBuilder builder = new MessageBuilder(messageSource); - builder.code("couldNotSetValue"); - builder.arg("label", new ResolvableArgument(property)); - builder.arg("value", sourceValue); - builder.defaultMessage("Failed to bind to property '" + property + "'; reason = " + cause.getLocalizedMessage()); - return builder.build(); - } - - }; - } - - } - - class SuccessResult implements BindingResult { - - private String property; - - private Object sourceValue; - - public SuccessResult(String property, Object sourceValue) { + public Success(String property, Object sourceValue) { this.property = property; this.sourceValue = sourceValue; } @@ -750,14 +666,89 @@ public class GenericBinder implements Binder { builder.code("bindSuccess"); builder.arg("label", new ResolvableArgument(property)); builder.arg("value", sourceValue); - builder.defaultMessage("Successfully bound user value " + StylerUtils.style(sourceValue) + "to property '" + property + "'"); - return builder.build(); + builder.defaultMessage("Successfully bound user value " + StylerUtils.style(sourceValue) + + "to property '" + property + "'"); + return builder.build(); } }; } } + class EvaluationError implements BindingResult { + + private String property; + + private Object sourceValue; + + private EvaluationException cause; + + public EvaluationError(String property, Object sourceValue, EvaluationException cause) { + this.property = property; + this.sourceValue = sourceValue; + this.cause = cause; + } + + public String getProperty() { + return property; + } + + public Object getSourceValue() { + return sourceValue; + } + + public boolean isFailure() { + return true; + } + + public Alert getAlert() { + return new AbstractAlert() { + public String getCode() { + SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode(); + if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { + return "conversionFailed"; + } else { + return "couldNotSetValue"; + } + } + + public Severity getSeverity() { + return Severity.FATAL; + } + + public String getMessage() { + SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode(); + if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { + AccessException accessException = (AccessException) cause.getCause(); + if (accessException.getCause() != null) { + Throwable cause = accessException.getCause(); + if (cause instanceof SpelEvaluationException + && ((SpelEvaluationException) cause).getMessageCode() == SpelMessage.TYPE_CONVERSION_ERROR) { + ConversionFailedException failure = (ConversionFailedException) cause.getCause(); + MessageBuilder builder = new MessageBuilder(messageSource); + builder.code("conversionFailed"); + builder.arg("label", new ResolvableArgument(property)); + builder.arg("value", sourceValue); + builder.defaultMessage("Failed to bind to property '" + property + "'; user value " + + StylerUtils.style(sourceValue) + " could not be converted to property type [" + + failure.getTargetType().getName() + "]"); + return builder.build(); + } + } + } + MessageBuilder builder = new MessageBuilder(messageSource); + builder.code("couldNotSetValue"); + builder.arg("label", new ResolvableArgument(property)); + builder.arg("value", sourceValue); + builder.defaultMessage("Failed to bind to property '" + property + "'; reason = " + + cause.getLocalizedMessage()); + return builder.build(); + } + + }; + } + + } static abstract class AbstractAlert implements Alert { public String toString() { return getCode() + " - " + getMessage(); diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinder.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinder.java index 2d915e95505..adbd8094e46 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinder.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinder.java @@ -26,7 +26,7 @@ import java.util.Map; * @see #setDefaultPrefix(String) * @see #setPresentPrefix(String) */ -class WebBinder extends GenericBinder { +public class WebBinder extends GenericBinder { private String defaultPrefix = "!"; diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinderFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinderFactory.java deleted file mode 100644 index 6f3304ef977..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/WebBinderFactory.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.binding.support; - -import org.springframework.ui.binding.Binder; -import org.springframework.ui.binding.BinderFactory; - -/** - * Factory for Binders suited for use in web environments. - * TODO - BinderConfiguration objects indexed by model class? - * @author Keith Donald - * @since 3.0 - */ -public class WebBinderFactory implements BinderFactory { - - public Binder getBinder(Object model) { - WebBinder binder = new WebBinder(model); - new AnnotatedModelBinderConfigurer().configure(binder); - return binder; - } - -} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/BindAndValidateLifecycle.java b/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/BindAndValidateLifecycle.java index 765f6822b70..40413c7061a 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/BindAndValidateLifecycle.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/BindAndValidateLifecycle.java @@ -15,12 +15,15 @@ */ package org.springframework.ui.lifecycle; +import java.util.Collections; +import java.util.List; import java.util.Map; import org.springframework.ui.alert.AlertContext; import org.springframework.ui.binding.Binder; import org.springframework.ui.binding.BindingResult; import org.springframework.ui.binding.BindingResults; +import org.springframework.ui.validation.ValidationFailure; import org.springframework.ui.validation.Validator; /** @@ -58,17 +61,28 @@ public final class BindAndValidateLifecycle { this.validationDecider = validationDecider; } + /** + * Execute the bind and validate lifecycle. + * Any bind or validation errors are recorded as alerts against the {@link AlertContext}. + * @param sourceValues the source values to bind and validate + */ public void execute(Map sourceValues) { BindingResults bindingResults = binder.bind(sourceValues); - if (validator != null && validationDecider.shouldValidateAfter(bindingResults)) { - // TODO get validation results - validator.validate(binder.getModel(), bindingResults.successes().properties()); - } + List validationFailures = validate(bindingResults); for (BindingResult result : bindingResults.failures()) { - // TODO - you may want to ignore some alerts like propertyNotFound alertContext.add(result.getProperty(), result.getAlert()); } - // TODO translate validation results into messages + for (ValidationFailure failure : validationFailures) { + alertContext.add(failure.getProperty(), failure.getAlert()); + } + } + + private List validate(BindingResults bindingResults) { + if (validator != null && validationDecider.shouldValidateAfter(bindingResults)) { + return validator.validate(binder.getModel(), bindingResults.successes().properties()); + } else { + return Collections.emptyList(); + } } } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationFailure.java b/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationFailure.java new file mode 100644 index 00000000000..31366e418c0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationFailure.java @@ -0,0 +1,21 @@ +package org.springframework.ui.validation; + +import org.springframework.ui.alert.Alert; + +/** + * A single validation failure generated by a Validator. + * @author Keith Donald + * @see Validator#validate(Object, java.util.List) + */ +public interface ValidationFailure { + + /** + * The name of the model property associated with this validation result. + */ + String getProperty(); + + /** + * Gets the alert for this validation failure, appropriate for rendering the failure to the user. + */ + Alert getAlert(); +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResult.java b/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResult.java deleted file mode 100644 index 29255af53cb..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResult.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.springframework.ui.validation; - -import org.springframework.ui.alert.Alert; - -public interface ValidationResult { - - /** - * The name of the model property associated with this validation result. - */ - String getProperty(); - - /** - * Indicates if the validation failed. - */ - boolean isFailure(); - - /** - * Gets the alert for this validation result, appropriate for rendering the result to the user. - * An alert describing a successful validation will have info severity. - * An alert describing a failed validation will have either warning or error severity. - */ - Alert getAlert(); -} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResults.java b/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResults.java deleted file mode 100644 index b5d41ab1c2a..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/ui/validation/ValidationResults.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2002-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.validation; - -public interface ValidationResults extends Iterable { - -} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/validation/Validator.java b/org.springframework.context/src/main/java/org/springframework/ui/validation/Validator.java index 6496764fc70..2d3d7da892a 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/validation/Validator.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/validation/Validator.java @@ -17,6 +17,18 @@ package org.springframework.ui.validation; import java.util.List; -public interface Validator { - ValidationResults validate(M model, List properties); +/** + * Validates a model object. + * @author Keith Donald + * @param the type of model object this validator supports + */ +public interface Validator { + + /** + * Validate the properties of the model object. + * @param model the model object + * @param properties the properties to validate + * @return a list of validation failures, empty if there were no failures + */ + List validate(Object model, List properties); } diff --git a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java index 0e3a4113e0f..f593c61a761 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java @@ -2,7 +2,6 @@ package org.springframework.ui.binding.support; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.math.BigDecimal; @@ -23,6 +22,8 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.ui.binding.Binding; import org.springframework.ui.binding.BindingResult; import org.springframework.ui.binding.BindingResults; +import org.springframework.ui.binding.MissingSourceValuesException; +import org.springframework.ui.binding.NoSuchBindingException; import org.springframework.ui.format.AnnotationFormatterFactory; import org.springframework.ui.format.Formatter; import org.springframework.ui.format.date.DateFormatter; @@ -31,33 +32,39 @@ import org.springframework.ui.format.number.CurrencyFormatter; import org.springframework.ui.format.number.IntegerFormatter; import org.springframework.ui.message.MockMessageSource; +import edu.emory.mathcs.backport.java.util.Arrays; + public class GenericBinderTests { private TestBean bean; - + private GenericBinder binder; - + @Before public void setUp() { bean = new TestBean(); binder = new GenericBinder(bean); LocaleContextHolder.setLocale(Locale.US); } - + @After public void tearDown() { LocaleContextHolder.setLocale(null); } - + @Test public void bindSingleValuesWithDefaultTypeConverterConversion() { + binder.addBinding("string"); + binder.addBinding("integer"); + binder.addBinding("foo"); + Map values = new LinkedHashMap(); values.put("string", "test"); values.put("integer", "3"); values.put("foo", "BAR"); BindingResults results = binder.bind(values); assertEquals(3, results.size()); - + assertEquals("string", results.get(0).getProperty()); assertFalse(results.get(0).isFailure()); assertEquals("test", results.get(0).getSourceValue()); @@ -69,14 +76,18 @@ public class GenericBinderTests { assertEquals("foo", results.get(2).getProperty()); assertFalse(results.get(2).isFailure()); assertEquals("BAR", results.get(2).getSourceValue()); - + assertEquals("test", bean.getString()); assertEquals(3, bean.getInteger()); assertEquals(FooEnum.BAR, bean.getFoo()); } @Test - public void bindSingleValuesWithDefaultTypeCoversionFailure() { + public void bindSingleValuesWithDefaultTypeConversionFailure() { + binder.addBinding("string"); + binder.addBinding("integer"); + binder.addBinding("foo"); + Map values = new LinkedHashMap(); values.put("string", "test"); // bad value @@ -90,118 +101,72 @@ public class GenericBinderTests { @Test public void bindSingleValuePropertyFormatter() throws ParseException { - binder.configureBinding(new BindingConfiguration("date", new DateFormatter())); + binder.addBinding("date").formatWith(new DateFormatter()); binder.bind(Collections.singletonMap("date", "2009-06-01")); assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate()); } @Test public void bindSingleValuePropertyFormatterParseException() { - binder.configureBinding(new BindingConfiguration("date", new DateFormatter())); + binder.addBinding("date").formatWith(new DateFormatter()); binder.bind(Collections.singletonMap("date", "bogus")); } @Test public void bindSingleValueWithFormatterRegistedByType() throws ParseException { + binder.addBinding("date"); binder.registerFormatter(Date.class, new DateFormatter()); binder.bind(Collections.singletonMap("date", "2009-06-01")); assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate()); } - + @Test public void bindSingleValueWithFormatterRegisteredByAnnotation() throws ParseException { + binder.addBinding("currency"); GenericFormatterRegistry registry = new GenericFormatterRegistry(); registry.add(CurrencyFormat.class, new CurrencyFormatter()); binder.setFormatterRegistry(registry); binder.bind(Collections.singletonMap("currency", "$23.56")); assertEquals(new BigDecimal("23.56"), bean.getCurrency()); } - + @Test - public void bindSingleValueWithnAnnotationFormatterFactoryRegistered() throws ParseException { + public void bindSingleValueWithAnnotationFormatterFactoryRegistered() throws ParseException { + binder.addBinding("currency"); binder.registerFormatterFactory(new CurrencyAnnotationFormatterFactory()); binder.bind(Collections.singletonMap("currency", "$23.56")); assertEquals(new BigDecimal("23.56"), bean.getCurrency()); } - - @Test + + @Test(expected = NoSuchBindingException.class) public void bindSingleValuePropertyNotFound() throws ParseException { - BindingResults results = binder.bind(Collections.singletonMap("bogus", "2009-06-01")); - assertEquals(1, results.size()); - assertTrue(results.get(0).isFailure()); - assertEquals("propertyNotFound", results.get(0).getAlert().getCode()); + binder.bind(Collections.singletonMap("bogus", "2009-06-01")); } - - @Test - public void bindUserValuesCreatedFromUserMap() { + + @Test(expected=MissingSourceValuesException.class) + public void bindMissingRequiredSourceValue() { + binder.addBinding("string"); + binder.addBinding("integer").required(); Map userMap = new LinkedHashMap(); userMap.put("string", "test"); - userMap.put("integer", "3"); - BindingResults results = binder.bind(userMap); - assertEquals(2, results.size()); - assertEquals("test", results.get(0).getSourceValue()); - assertEquals("3", results.get(1).getSourceValue()); - assertEquals("test", bean.getString()); - assertEquals(3, bean.getInteger()); - } - - @Test - public void getBindingOptimistic() { - Binding b = binder.getBinding("integer"); - assertFalse(b.isCollection()); - assertEquals("0", b.getValue()); - BindingResult result = b.setValue("5"); - assertEquals("5", b.getValue()); - assertFalse(result.isFailure()); - } - - @Test - public void getBindingStrict() { - binder.setStrict(true); - Binding b = binder.getBinding("integer"); - assertNull(b); - binder.configureBinding(new BindingConfiguration("integer", null)); - b = binder.getBinding("integer"); - assertFalse(b.isCollection()); - assertEquals("0", b.getValue()); - BindingResult result = b.setValue("5"); - assertEquals("5", b.getValue()); - assertFalse(result.isFailure()); - } - - @Test - public void bindStrictNoMappingBindings() { - binder.setStrict(true); - binder.configureBinding(new BindingConfiguration("integer", null)); - Map values = new LinkedHashMap(); - values.put("integer", "3"); - values.put("foo", "BAR"); - BindingResults results = binder.bind(values); - assertEquals(2, results.size()); - - assertEquals("integer", results.get(0).getProperty()); - assertFalse(results.get(0).isFailure()); - assertEquals("3", results.get(0).getSourceValue()); - - assertEquals("foo", results.get(1).getProperty()); - assertTrue(results.get(1).isFailure()); - assertEquals("BAR", results.get(1).getSourceValue()); + // missing "integer" + binder.bind(userMap); } @Test public void getBindingCustomFormatter() { - binder.configureBinding(new BindingConfiguration("currency", new CurrencyFormatter())); + binder.addBinding("currency").formatWith(new CurrencyFormatter()); Binding b = binder.getBinding("currency"); assertFalse(b.isCollection()); assertEquals("", b.getValue()); b.setValue("$23.56"); assertEquals("$23.56", b.getValue()); } - + @Test public void getBindingCustomFormatterRequiringTypeCoersion() { // IntegerFormatter formats Longs, so conversion from Integer -> Long is performed - binder.configureBinding(new BindingConfiguration("integer", new IntegerFormatter())); + binder.addBinding("integer").formatWith(new IntegerFormatter()); Binding b = binder.getBinding("integer"); b.setValue("2,300"); assertEquals("2,300", b.getValue()); @@ -210,16 +175,19 @@ public class GenericBinderTests { @Test public void invalidFormatBindingResultCustomAlertMessage() { MockMessageSource messages = new MockMessageSource(); - messages.addMessage("invalidFormat", Locale.US, "Please enter an integer in format ### for the #{label} field; you entered #{value}"); + messages.addMessage("invalidFormat", Locale.US, + "Please enter an integer in format ### for the #{label} field; you entered #{value}"); binder.setMessageSource(messages); - binder.configureBinding(new BindingConfiguration("integer", new IntegerFormatter())); + binder.addBinding("integer").formatWith(new IntegerFormatter()); Binding b = binder.getBinding("integer"); BindingResult result = b.setValue("bogus"); - assertEquals("Please enter an integer in format ### for the integer field; you entered bogus", result.getAlert().getMessage()); + assertEquals("Please enter an integer in format ### for the integer field; you entered bogus", result + .getAlert().getMessage()); } @Test public void getBindingMultiValued() { + binder.addBinding("foos"); Binding b = binder.getBinding("foos"); assertTrue(b.isCollection()); assertEquals(0, b.getCollectionValues().length); @@ -234,8 +202,20 @@ public class GenericBinderTests { assertEquals("BOOP", values[2]); } + @Test + public void getBindingMultiValuedIndexAccess() { + binder.addBinding("foos"); + bean.setFoos(Arrays.asList(new FooEnum[] { FooEnum.BAR })); + Binding b = binder.getBinding("foos[0]"); + assertFalse(b.isCollection()); + assertEquals("BAR", b.getValue()); + b.setValue("BAZ"); + assertEquals("BAZ", b.getValue()); + } + @Test public void getBindingMultiValuedTypeConversionFailure() { + binder.addBinding("foos"); Binding b = binder.getBinding("foos"); assertTrue(b.isCollection()); assertEquals(0, b.getCollectionValues().length); @@ -243,18 +223,23 @@ public class GenericBinderTests { assertTrue(result.isFailure()); assertEquals("conversionFailed", result.getAlert().getCode()); } - + @Test public void bindHandleNullValueInNestedPath() { + binder.addBinding("addresses.street"); + binder.addBinding("addresses.city"); + binder.addBinding("addresses.state"); + binder.addBinding("addresses.zip"); + Map values = new LinkedHashMap(); - + // EL configured with some options from SpelExpressionParserConfiguration: // (see where Binder creates the parser) // - new addresses List is created if null // - new entries automatically built if List is currently too short - all new entries // are new instances of the type of the list entry, they are not null. // not currently doing anything for maps or arrays - + values.put("addresses[0].street", "4655 Macy Lane"); values.put("addresses[0].city", "Melbourne"); values.put("addresses[0].state", "FL"); @@ -281,7 +266,7 @@ public class GenericBinderTests { @Test public void formatPossibleValue() { - binder.configureBinding(new BindingConfiguration("currency", new CurrencyFormatter())); + binder.addBinding("currency").formatWith(new CurrencyFormatter()); Binding b = binder.getBinding("currency"); assertEquals("$5.00", b.format(new BigDecimal("5"))); } @@ -298,7 +283,7 @@ public class GenericBinderTests { private BigDecimal currency; private List foos; private List
addresses; - + public String getString() { return string; } @@ -355,7 +340,7 @@ public class GenericBinderTests { public void setAddresses(List
addresses) { this.addresses = addresses; } - + } public static class Address { @@ -364,50 +349,51 @@ public class GenericBinderTests { private String state; private String zip; private String country; - + public String getStreet() { return street; } - + public void setStreet(String street) { this.street = street; } - + public String getCity() { return city; } - + public void setCity(String city) { this.city = city; } - + public String getState() { return state; } - + public void setState(String state) { this.state = state; } - + public String getZip() { return zip; } - + public void setZip(String zip) { this.zip = zip; } - + public String getCountry() { return country; } - + public void setCountry(String country) { this.country = country; } - + } - - public static class CurrencyAnnotationFormatterFactory implements AnnotationFormatterFactory { + + public static class CurrencyAnnotationFormatterFactory implements + AnnotationFormatterFactory { public Formatter getFormatter(CurrencyFormat annotation) { return new CurrencyFormatter(); } diff --git a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java index 4524807ff0e..417a287b3d6 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/WebBinderTests.java @@ -37,10 +37,15 @@ public class WebBinderTests { @Test public void bindUserValuesCreatedFromUserMap() throws ParseException { + binder.addBinding("string"); + binder.addBinding("integer"); + binder.addBinding("date").formatWith(new DateFormatter()); + binder.addBinding("bool"); + binder.addBinding("currency"); + binder.addBinding("addresses"); GenericFormatterRegistry registry = new GenericFormatterRegistry(); registry.add(CurrencyFormat.class, new CurrencyFormatter()); binder.setFormatterRegistry(registry); - binder.configureBinding(new BindingConfiguration("date", new DateFormatter())); Map userMap = new LinkedHashMap(); userMap.put("string", "test"); userMap.put("_integer", "doesn't matter"); diff --git a/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/BindAndValidateLifecycleTests.java b/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/BindAndValidateLifecycleTests.java index 7cc5ece5eca..92754d48f42 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/BindAndValidateLifecycleTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/BindAndValidateLifecycleTests.java @@ -3,10 +3,8 @@ package org.springframework.ui.lifecycle; import static org.junit.Assert.assertEquals; import java.math.BigDecimal; -import java.util.ArrayList; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -16,18 +14,18 @@ import org.springframework.ui.alert.Alert; import org.springframework.ui.alert.Alerts; import org.springframework.ui.alert.Severity; import org.springframework.ui.alert.support.DefaultAlertContext; -import org.springframework.ui.binding.Binder; import org.springframework.ui.binding.Bound; import org.springframework.ui.binding.Model; -import org.springframework.ui.binding.support.WebBinderFactory; +import org.springframework.ui.binding.support.WebBinder; import org.springframework.ui.format.number.CurrencyFormat; -import org.springframework.ui.validation.ValidationResult; -import org.springframework.ui.validation.ValidationResults; +import org.springframework.ui.validation.ValidationFailure; import org.springframework.ui.validation.Validator; import org.springframework.ui.validation.constraint.Impact; import org.springframework.ui.validation.constraint.Message; import org.springframework.ui.validation.constraint.ValidationConstraint; +import edu.emory.mathcs.backport.java.util.Collections; + public class BindAndValidateLifecycleTests { private BindAndValidateLifecycle lifecycle; @@ -40,20 +38,23 @@ public class BindAndValidateLifecycleTests { public void setUp() { model = new TestBean(); alertContext = new DefaultAlertContext(); - Binder binder = new WebBinderFactory().getBinder(model); - Validator validator = new TestBeanValidator(); + WebBinder binder = new WebBinder(model); + binder.addBinding("string"); + binder.addBinding("integer"); + binder.addBinding("foo"); + Validator validator = new TestBeanValidator(); lifecycle = new BindAndValidateLifecycle(binder, validator, alertContext); } - static class TestBeanValidator implements Validator { - public ValidationResults validate(TestBean model, List properties) { - TestValidationResults results = new TestValidationResults(); + static class TestBeanValidator implements Validator { + public List validate(Object model, List properties) { + TestBean bean = (TestBean) model; RequiredConstraint required = new RequiredConstraint(); - boolean valid = required.validate(model.getString()); + boolean valid = required.validate(bean); if (!valid) { } - return results; + return Collections.emptyList(); } } @@ -111,21 +112,9 @@ public class BindAndValidateLifecycleTests { } } } - - static class TestValidationResults implements ValidationResults { - private List results = new ArrayList(); - public void add(ValidationResult result) { - results.add(result); - } - - public Iterator iterator() { - return results.iterator(); - } - - } - static class TestValidationFailure implements ValidationResult { + static class TestValidationFailure implements ValidationFailure { private String property; @@ -143,10 +132,6 @@ public class BindAndValidateLifecycleTests { return Alerts.error(message); } - public boolean isFailure() { - return true; - } - } @Test @@ -292,7 +277,7 @@ public class BindAndValidateLifecycleTests { } - @Model(value="testBean", strictBinding=true) + @Model(value="testBean") public class TestAnnotatedBean { private String editable;