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 973ed78137d..f37f61116a6 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
@@ -21,7 +21,7 @@ import java.util.Map;
* Binds user-entered values to properties of a model object.
* @author Keith Donald
* @since 3.0
- * @see #addBinding(String)
+ * @see #bind(String)
* @see #getBinding(String)
* @see #bind(Map)
*/
@@ -32,26 +32,6 @@ public interface Binder {
*/
Object getModel();
- /**
- * 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 BindingConfiguration addBinding(String propertyPath);
-
/**
* Get a binding to a model property..
* @param property the property path
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 1a3e2ca799e..b606069afc6 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
@@ -15,59 +15,128 @@
*/
package org.springframework.ui.binding;
+import org.springframework.ui.alert.Alert;
+
/**
- * A binding between a user interface element and a model property.
+ * A binding between a source element and a model property.
* @author Keith Donald
* @since 3.0
*/
public interface Binding {
/**
- * The name of the bound model property.
+ * The bound value to display in the UI.
+ * Is the formatted model value if not dirty.
+ * Is the buffered value if dirty.
*/
- String getProperty();
+ Object getValue();
/**
- * The type of the underlying property associated with this binding.
+ * If this Binding is read-only.
+ * A read-only Binding cannot have source values applied and cannot be committed.
*/
- Class> getType();
-
- /**
- * The formatted property value to display in the user interface.
- */
- String getValue();
-
- /**
- * 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);
+ boolean isReadOnly();
/**
- * Formats a candidate model property value for display in the user interface.
- * @param potentialValue a possible value
- * @return the formatted value to display in the user interface
+ * Apply the source value to this binding.
+ * The source value is parsed, validated, and stored in the binding's value buffer.
+ * Sets 'dirty' status to true.
+ * Sets 'valid' status to false if the source value is not valid.
+ * @param sourceValue
+ * @throws IllegalStateException if read only
*/
- String format(Object potentialValue);
+ void applySourceValue(Object sourceValue);
+
+ /**
+ * True if there is an uncommitted value in the binding buffer.
+ * Set to true after applying a source value.
+ * Set to false after a commit.
+ */
+ boolean isDirty();
+
+ /**
+ * False if dirty and the buffered value is invalid.
+ * False if dirty and the buffered value appears valid but could not be committed.
+ * True otherwise.
+ */
+ boolean isValid();
+
+ /**
+ * Commit the buffered value to the model.
+ * @throws IllegalStateException if not dirty, not valid, or read-only
+ */
+ void commit();
/**
- * Is this a collection binding?
- * If so, a client may call {@link #getCollectionValues()} to get the collection element values for display in the user interface.
- * In this case, the client typically allocates one indexed field to each value.
- * A client may then call {@link #setValues(String[])} to update model property values from those fields.
- * Alternatively, a client may call {@link #getValue()} to render the collection as a single value for display in a single field, such as a large text area.
- * The client would then call {@link #setValue(Object)} to update that single value from the field.
+ * An Alert that communicates the current status of this Binding.
*/
- boolean isCollection();
+ Alert getStatusAlert();
+
+ /**
+ * Access raw model values.
+ */
+ Model getModel();
+
+ /**
+ * Get a Binding to a nested property value.
+ * @param nestedProperty the nested property name, such as "foo"; should not be a property path like "foo.bar"
+ * @return the binding to the nested property
+ */
+ Binding getBinding(String nestedProperty);
/**
- * When a collection binding, the formatted values to display in the user interface.
- * If not a collection binding, throws an IllegalStateException.
- * @throws IllegalStateException
+ * If bound to an indexable Collection, either a {@link java.util.List} or an array.
*/
- String[] getCollectionValues();
+ boolean isIndexable();
+ /**
+ * If a List, get a Binding to a element in the List.
+ * @param index the element index
+ * @return the indexed binding
+ */
+ Binding getIndexedBinding(int index);
+
+ /**
+ * If bound to a {@link java.util.Map}.
+ */
+ boolean isMap();
+
+ /**
+ * If a Map, get a Binding to a value in the Map.
+ * @param key the map key
+ * @return the keyed binding
+ */
+ Binding getKeyedBinding(Object key);
+
+ /**
+ * Format a potential model value for display.
+ * If an indexable binding, expects the model value to be a potential collection element & uses the configured element formatter.
+ * If a map binding, expects the model value to be a potential map value & uses the configured map value formatter.
+ * @param potentialValue the potential value
+ * @return the formatted string
+ */
+ String formatValue(Object potentialModelValue);
+
+ /**
+ * For accessing the raw bound model object.
+ * @author Keith Donald
+ */
+ public interface Model {
+
+ /**
+ * Read the raw model value.
+ */
+ Object getValue();
+
+ /**
+ * The model value type.
+ */
+ Class> getValueType();
+
+ /**
+ * Set the model value.
+ * @throws IllegalStateException if this binding is read-only
+ */
+ void setValue(Object value);
+ }
}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRule.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRule.java
new file mode 100644
index 00000000000..c020c09cb76
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRule.java
@@ -0,0 +1,19 @@
+package org.springframework.ui.binding.config;
+
+import org.springframework.ui.format.Formatter;
+
+public interface BindingRule {
+
+ String getPropertyPath();
+
+ Formatter> getFormatter();
+
+ boolean isRequired();
+
+ boolean isCollectionBinding();
+
+ Formatter> getKeyFormatter();
+
+ Formatter> getValueFormatter();
+
+}
\ 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/config/BindingRuleConfiguration.java
similarity index 76%
rename from org.springframework.context/src/main/java/org/springframework/ui/binding/BindingConfiguration.java
rename to org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRuleConfiguration.java
index 81420e6890b..aa7994f4bf1 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/config/BindingRuleConfiguration.java
@@ -13,25 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.ui.binding;
+package org.springframework.ui.binding.config;
-import org.springframework.ui.binding.support.GenericBinder;
import org.springframework.ui.format.Formatter;
/**
* A fluent interface for configuring a newly added binding.
* @author Keith Donald
- * @see GenericBinder#addBinding(String)
*/
-public interface BindingConfiguration {
+public interface BindingRuleConfiguration {
/**
* Set the Formatter to use to format bound property values.
- * If a collection property, the formatter is used to format collection element values.
+ * If a collection property, this formatter is used to format the Collection as a String.
* Default is null.
*/
- BindingConfiguration formatWith(Formatter> formatter);
+ BindingRuleConfiguration formatWith(Formatter> formatter);
+ /**
+ * If a collection property, set the Formatter to use to format indexed collection elements.
+ * Default is null.
+ */
+ BindingRuleConfiguration formatElementsWith(Formatter> formatter);
+
/**
* Mark the binding as required.
* A required binding will generate an exception if no sourceValue is provided to a bind invocation.
@@ -44,5 +48,6 @@ public interface BindingConfiguration {
* addresses.city=required - will generate an exception if 'addresses[n].city' is not present in the source values map, for every address[n].
*
*/
- BindingConfiguration required();
+ BindingRuleConfiguration required();
+
}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRules.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRules.java
new file mode 100644
index 00000000000..02278280123
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRules.java
@@ -0,0 +1,9 @@
+package org.springframework.ui.binding.config;
+
+import java.util.List;
+
+public interface BindingRules extends List {
+
+ Class> getModelType();
+
+}
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRulesBuilder.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRulesBuilder.java
new file mode 100644
index 00000000000..3025bb37cf5
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRulesBuilder.java
@@ -0,0 +1,120 @@
+package org.springframework.ui.binding.config;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+
+import org.springframework.core.GenericCollectionTypeResolver;
+import org.springframework.util.Assert;
+
+/**
+ * Builder for constructing the rules for binding to a model.
+ * @author Keith Donald
+ * @param the model type
+ */
+public class BindingRulesBuilder {
+
+ private BindingRules bindingRules;
+
+ /**
+ * Creates a new BindingRuleBuilder.
+ * @param modelType the type of model to build binding rules against
+ */
+ public BindingRulesBuilder(Class> modelType) {
+ Assert.notNull(modelType, "The model type is required");
+ bindingRules = new ArrayListBindingRules(modelType);
+ }
+
+ /**
+ * Creates a rule for binding to the model property.
+ * @param propertyPath the model property path
+ * @return allows additional binding configuration options to be specified fluently
+ * @throws IllegalArgumentException if the property path is invalid given the modelType
+ */
+ public BindingRuleConfiguration bind(String propertyPath) {
+ boolean collectionBinding = validate(propertyPath);
+ ConfigurableBindingRule rule = new ConfigurableBindingRule(propertyPath);
+ if (collectionBinding) {
+ rule.markCollectionBinding();
+ }
+ bindingRules.add(rule);
+ return rule;
+ }
+
+ /**
+ * The built list of binding rules.
+ * Call after recording {@link #bind(String)} instructions.
+ */
+ public BindingRules getBindingRules() {
+ return bindingRules;
+ }
+
+ private boolean validate(String propertyPath) {
+ boolean collectionBinding = false;
+ String[] props = propertyPath.split("\\.");
+ if (props.length == 0) {
+ props = new String[] { propertyPath };
+ }
+ Class> modelType = bindingRules.getModelType();
+ for (int i = 0; i < props.length; i ++) {
+ String prop = props[i];
+ PropertyDescriptor[] propDescs = getBeanInfo(modelType).getPropertyDescriptors();
+ boolean found = false;
+ for (PropertyDescriptor propDesc : propDescs) {
+ if (prop.equals(propDesc.getName())) {
+ found = true;
+ Class> propertyType = propDesc.getPropertyType();
+ if (Collection.class.isAssignableFrom(propertyType)) {
+ modelType = GenericCollectionTypeResolver.getCollectionReturnType(propDesc.getReadMethod());
+ if (i == (props.length - 1)) {
+ collectionBinding = true;
+ }
+ } else if (Map.class.isAssignableFrom(propertyType)) {
+ modelType = GenericCollectionTypeResolver.getMapValueReturnType(propDesc.getReadMethod());
+ if (i == (props.length - 1)) {
+ collectionBinding = true;
+ }
+ } else {
+ modelType = propertyType;
+ }
+ break;
+ }
+ }
+ if (!found) {
+ if (props.length > 1) {
+ throw new IllegalArgumentException("No property named '" + prop + "' found on model class [" + modelType.getName() + "] as part of property path '" + propertyPath + "'");
+ } else {
+ throw new IllegalArgumentException("No property named '" + prop + "' found on model class [" + modelType.getName() + "]");
+ }
+ }
+ }
+ return collectionBinding;
+ }
+
+ private BeanInfo getBeanInfo(Class> clazz) {
+ try {
+ return Introspector.getBeanInfo(clazz);
+ } catch (IntrospectionException e) {
+ throw new IllegalStateException("Unable to introspect model type " + clazz);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ static class ArrayListBindingRules extends ArrayList implements BindingRules {
+
+ private Class> modelType;
+
+ public ArrayListBindingRules(Class> modelType) {
+ this.modelType = modelType;
+ }
+
+ public Class> getModelType() {
+ return modelType;
+ }
+
+ }
+}
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/config/ConfigurableBindingRule.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/ConfigurableBindingRule.java
new file mode 100644
index 00000000000..d87fa34e87b
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/ConfigurableBindingRule.java
@@ -0,0 +1,68 @@
+package org.springframework.ui.binding.config;
+
+import org.springframework.ui.format.Formatter;
+
+public class ConfigurableBindingRule implements BindingRuleConfiguration, BindingRule {
+
+ private String propertyPath;
+
+ private Formatter> formatter;
+
+ private boolean required;
+
+ private boolean collectionBinding;
+
+ private Formatter> elementFormatter;
+
+ public ConfigurableBindingRule(String propertyPath) {
+ this.propertyPath = propertyPath;
+ }
+
+ // implementing BindingRuleConfiguration
+
+ public BindingRuleConfiguration formatWith(Formatter> formatter) {
+ this.formatter = formatter;
+ return this;
+ }
+
+ public BindingRuleConfiguration required() {
+ this.required = true;
+ return this;
+ }
+
+ public BindingRuleConfiguration formatElementsWith(Formatter> formatter) {
+ this.elementFormatter = formatter;
+ return this;
+ }
+
+ // implementing BindingRule
+
+ public String getPropertyPath() {
+ return propertyPath;
+ }
+
+ public Formatter> getFormatter() {
+ return formatter;
+ }
+
+ public boolean isRequired() {
+ return required;
+ }
+
+ public boolean isCollectionBinding() {
+ return collectionBinding;
+ }
+
+ public void markCollectionBinding() {
+ this.collectionBinding = true;
+ }
+
+ public Formatter> getValueFormatter() {
+ return elementFormatter;
+ }
+
+ public Formatter> getKeyFormatter() {
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/ArrayListBindingResults.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/ArrayListBindingResults.java
new file mode 100644
index 00000000000..c1f4984ac20
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/ArrayListBindingResults.java
@@ -0,0 +1,76 @@
+/**
+ *
+ */
+package org.springframework.ui.binding.support;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.springframework.ui.binding.BindingResult;
+import org.springframework.ui.binding.BindingResults;
+
+class ArrayListBindingResults implements BindingResults {
+
+ private List results;
+
+ public ArrayListBindingResults() {
+ results = new ArrayList();
+ }
+
+ public ArrayListBindingResults(int size) {
+ results = new ArrayList(size);
+ }
+
+ public void add(BindingResult result) {
+ results.add(result);
+ }
+
+ // implementing Iterable
+
+ public Iterator iterator() {
+ return results.iterator();
+ }
+
+ // implementing BindingResults
+
+ public BindingResults successes() {
+ ArrayListBindingResults results = new ArrayListBindingResults();
+ for (BindingResult result : this) {
+ if (!result.isFailure()) {
+ results.add(result);
+ }
+ }
+ return results;
+ }
+
+ public BindingResults failures() {
+ ArrayListBindingResults results = new ArrayListBindingResults();
+ for (BindingResult result : this) {
+ if (result.isFailure()) {
+ results.add(result);
+ }
+ }
+ return results;
+ }
+
+ public BindingResult get(int index) {
+ return results.get(index);
+ }
+
+ public List properties() {
+ List properties = new ArrayList(results.size());
+ for (BindingResult result : this) {
+ properties.add(result.getProperty());
+ }
+ return properties;
+ }
+
+ public int size() {
+ return results.size();
+ }
+
+ public String toString() {
+ return "[BindingResults = " + results.toString() + "]";
+ }
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/BindingStatusResult.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/BindingStatusResult.java
new file mode 100644
index 00000000000..5982181f434
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/BindingStatusResult.java
@@ -0,0 +1,40 @@
+/**
+ *
+ */
+package org.springframework.ui.binding.support;
+
+import org.springframework.ui.alert.Alert;
+import org.springframework.ui.alert.Severity;
+import org.springframework.ui.binding.BindingResult;
+
+class BindingStatusResult implements BindingResult {
+
+ private String property;
+
+ private Object sourceValue;
+
+ private Alert bindingStatusAlert;
+
+ public BindingStatusResult(String property, Object sourceValue, Alert alert) {
+ this.property = property;
+ this.sourceValue = sourceValue;
+ this.bindingStatusAlert = alert;
+ }
+
+ public String getProperty() {
+ return property;
+ }
+
+ public Object getSourceValue() {
+ return sourceValue;
+ }
+
+ public boolean isFailure() {
+ return bindingStatusAlert.getSeverity().compareTo(Severity.INFO) > 1;
+ }
+
+ public Alert getAlert() {
+ return bindingStatusAlert;
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/CollectionTypeDescriptor.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/CollectionTypeDescriptor.java
new file mode 100644
index 00000000000..cfb1b69697c
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/CollectionTypeDescriptor.java
@@ -0,0 +1,50 @@
+/**
+ *
+ */
+package org.springframework.ui.binding.support;
+
+import org.springframework.util.ObjectUtils;
+
+public class CollectionTypeDescriptor {
+
+ private Class> collectionType;
+
+ private Class> elementType;
+
+ /**
+ * Creates a new generic collection property type
+ * @param collectionType the collection type
+ * @param elementType the element type
+ */
+ public CollectionTypeDescriptor(Class> collectionType, Class> elementType) {
+ this.collectionType = collectionType;
+ this.elementType = elementType;
+ }
+
+ /**
+ * The collection type.
+ */
+ public Class> getCollectionType() {
+ return collectionType;
+ }
+
+ /**
+ * The element type.
+ */
+ public Class> getElementType() {
+ return elementType;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof CollectionTypeDescriptor)) {
+ return false;
+ }
+ CollectionTypeDescriptor type = (CollectionTypeDescriptor) o;
+ return collectionType.equals(type.collectionType)
+ && ObjectUtils.nullSafeEquals(elementType, type.elementType);
+ }
+
+ public int hashCode() {
+ return collectionType.hashCode() + elementType.hashCode();
+ }
+}
\ 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
deleted file mode 100644
index 308af77d892..00000000000
--- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/DefaultBindingConfiguration.java
+++ /dev/null
@@ -1,62 +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.BindingConfiguration;
-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/DefaultFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/DefaultFormatter.java
new file mode 100644
index 00000000000..e190cce2347
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/DefaultFormatter.java
@@ -0,0 +1,30 @@
+/**
+ *
+ */
+package org.springframework.ui.binding.support;
+
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.springframework.ui.format.Formatter;
+
+class DefaultFormatter implements Formatter