From 9c78616e11370b7b7046baf02bc62bdc453938e5 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Mon, 20 Jul 2009 03:48:32 +0000 Subject: [PATCH] additional binding metadata --- .../springframework/ui/binding/Binding.java | 74 ++++++-- .../config/BindingRuleConfiguration.java | 24 ++- .../ui/binding/support/GenericBinder.java | 20 ++- .../ui/binding/support/PropertyBinding.java | 159 ++++++++---------- .../binding/support/GenericBinderTests.java | 11 +- 5 files changed, 157 insertions(+), 131 deletions(-) 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 fcd8884be84..7cc389ef195 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 @@ -34,10 +34,25 @@ public interface Binding { Object getValue(); /** - * If this Binding is read-only. - * A read-only Binding cannot have source values applied and cannot be committed. + * If this Binding is editable. + * Used to determine if the user can edit the field value. + * A Binding that is not editable cannot have source values applied and cannot be committed. */ - boolean isReadOnly(); + boolean isEditable(); + + /** + * If this Binding is enabled. + * Used to determine if the user can interact with the field. + * A Binding that is not enabled cannot have source values applied and cannot be committed. + */ + boolean isEnabled(); + + /** + * If this Binding is visible. + * Used to determine if the user can see the field. + * A Binding that is not visible cannot have source values applied and cannot be committed. + */ + boolean isVisible(); /** * Apply the source value to this binding. @@ -45,7 +60,7 @@ public interface Binding { * Sets to {@link BindingStatus#DIRTY} if succeeds. * Sets to {@link BindingStatus#INVALID_SOURCE_VALUE} if fails. * @param sourceValue - * @throws IllegalStateException if {@link #isReadOnly()} + * @throws IllegalStateException if {@link #isEditable()} */ void applySourceValue(Object sourceValue); @@ -60,11 +75,12 @@ public interface Binding { BindingStatus getStatus(); /** - * An alert that communicates the details of a BindingStatus change to the user. - * Returns null if the BindingStatus has never changed. - * Returns a {@link Severity#INFO} Alert with code bindSuccess after a successful commit. - * Returns a {@link Severity#ERROR} Alert with code typeMismatch if the source value could not be converted to type required by the Model. - * Returns a {@link Severity#FATAL} Alert with code internalError if the buffered value could not be committed due to a unexpected runtime exception. + * An alert that communicates current status to the user. + * Returns null if {@link BindingStatus#CLEAN} and {@link ValidationStatus#NOT_VALIDATED}. + * Returns a {@link Severity#INFO} Alert with code bindSuccess when {@link BindingStatus#COMMITTED}. + * Returns a {@link Severity#ERROR} Alert with code typeMismatch when {@link BindingStatus#INVALID_SOURCE_VALUE} or {@link BindingStatus#COMMIT_FAILURE} due to a value parse / type conversion error. + * Returns a {@link Severity#FATAL} Alert with code internalError when {@link BindingStatus#COMMIT_FAILURE} due to a unexpected runtime exception. + * Returns a {@link Severity#INFO} Alert describing results of validation if {@link ValidationStatus#VALID} or {@link ValidationStatus#INVALID}. */ Alert getStatusAlert(); @@ -72,10 +88,16 @@ public interface Binding { * Commit the buffered value to the model. * Sets to {@link BindingStatus#COMMITTED} if succeeds. * Sets to {@link BindingStatus#COMMIT_FAILURE} if fails. - * @throws IllegalStateException if not {@link BindingStatus#DIRTY} or {@link #isReadOnly()} + * @throws IllegalStateException if not {@link BindingStatus#DIRTY} or {@link #isEditable()} */ void commit(); + /** + * Clear the buffered value without committing. + * @throws IllegalStateException if BindingStatus is CLEAN or COMMITTED. + */ + void revert(); + /** * Access raw model values. */ @@ -91,14 +113,14 @@ public interface Binding { /** * If bound to an indexable Collection, either a {@link java.util.List} or an array. */ - boolean isIndexable(); + boolean isList(); /** * 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); + Binding getListElementBinding(int index); /** * If bound to a {@link java.util.Map}. @@ -110,11 +132,11 @@ public interface Binding { * @param key the map key * @return the keyed binding */ - Binding getKeyedBinding(Object key); + Binding getMapValueBinding(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 list binding, expects the model value to be a potential list 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 @@ -145,7 +167,7 @@ public interface Binding { } /** - * The states of a Binding. + * Binding states. * @author Keith Donald */ public enum BindingStatus { @@ -175,4 +197,26 @@ public interface Binding { */ COMMIT_FAILURE } + + /** + * Validation states. + * @author Keith Donald + */ + public enum ValidationStatus { + + /** + * Initial state: No validation has run. + */ + NOT_VALIDATED, + + /** + * Validation has succeeded. + */ + VALID, + + /** + * Validation has failed. + */ + INVALID + } } \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRuleConfiguration.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRuleConfiguration.java index aa7994f4bf1..7360d2d332c 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRuleConfiguration.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/config/BindingRuleConfiguration.java @@ -18,7 +18,7 @@ package org.springframework.ui.binding.config; import org.springframework.ui.format.Formatter; /** - * A fluent interface for configuring a newly added binding. + * A fluent interface for configuring a newly added binding rule. * @author Keith Donald */ public interface BindingRuleConfiguration { @@ -31,23 +31,21 @@ public interface BindingRuleConfiguration { BindingRuleConfiguration formatWith(Formatter formatter); /** - * If a collection property, set the Formatter to use to format indexed collection elements. + * If a indexable map property, set the Formatter to use to format map key indexes. + * Default is null. + */ + BindingRuleConfiguration formatKeysWith(Formatter formatter); + + /** + * If an indexable list or map property, set the Formatter to use to format indexed 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. - * 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].
-	 * 
+ * Mark the binding as read only. + * A read-only binding cannot have source values applied and cannot be committed. */ - BindingRuleConfiguration required(); + BindingRuleConfiguration readOnly(); } \ No newline at end of file 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 d0265623b3f..a8e6c7da0c4 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 @@ -26,6 +26,7 @@ import org.springframework.ui.binding.Binding; import org.springframework.ui.binding.BindingResult; import org.springframework.ui.binding.BindingResults; import org.springframework.ui.binding.Binding.BindingStatus; +import org.springframework.ui.binding.config.BindingRuleConfiguration; import org.springframework.util.Assert; /** @@ -77,6 +78,15 @@ public class GenericBinder implements Binder { this.typeConverter = typeConverter; } + /** + * + * @param propertyPath binding rule property path in format prop.nestedListProp[].nestedMapProp[].nestedProp + * @return + */ + public BindingRuleConfiguration bindingRule(String propertyPath) { + return null; + } + // implementing Binder public Object getModel() { @@ -93,9 +103,9 @@ public class GenericBinder implements Binder { for (PropertyPathElement element : path.getNestedElements()) { if (element.isIndex()) { if (binding.isMap()) { - binding = binding.getKeyedBinding(element.getValue()); - } else if (binding.isIndexable()) { - binding = binding.getIndexedBinding(element.getIntValue()); + binding = binding.getMapValueBinding(element.getValue()); + } else if (binding.isList()) { + binding = binding.getListElementBinding(element.getIntValue()); } else { throw new IllegalArgumentException("Attempted to index a property that is not a Collection or Map"); } @@ -135,7 +145,7 @@ public class GenericBinder implements Binder { private BindingResult bind(Map.Entry sourceValue, Binding binding) { String property = sourceValue.getKey(); Object value = sourceValue.getValue(); - if (binding.isReadOnly()) { + if (binding.isEditable()) { return new PropertyNotWriteableResult(property, value, messageSource); } else { binding.applySourceValue(value); @@ -145,5 +155,5 @@ public class GenericBinder implements Binder { return new BindingStatusResult(property, value, binding.getStatusAlert()); } } - + } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyBinding.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyBinding.java index 37300d16ec0..40bee219729 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyBinding.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyBinding.java @@ -53,7 +53,7 @@ public class PropertyBinding implements Binding { private ValueBuffer buffer; - private BindingStatus bindingStatus; + private BindingStatus status; public PropertyBinding(String property, Object model, TypeConverter typeConverter) { this.propertyDescriptor = findPropertyDescriptor(property, model); @@ -61,36 +61,43 @@ public class PropertyBinding implements Binding { this.model = model; this.typeConverter = typeConverter; this.buffer = new ValueBuffer(getModel()); - bindingStatus = BindingStatus.CLEAN; + status = BindingStatus.CLEAN; } public Object getValue() { - if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) { + if (status == BindingStatus.INVALID_SOURCE_VALUE) { return sourceValue; - } else if (bindingStatus == BindingStatus.DIRTY || bindingStatus == BindingStatus.COMMIT_FAILURE) { + } else if (status == BindingStatus.DIRTY || status == BindingStatus.COMMIT_FAILURE) { return formatValue(buffer.getValue()); } else { return formatValue(getModel().getValue()); } } - public boolean isReadOnly() { - return propertyDescriptor.getWriteMethod() == null || markedNotEditable(); + public boolean isEditable() { + return propertyDescriptor.getWriteMethod() != null || !markedNotEditable(); + } + + public boolean isEnabled() { + return true; + } + + public boolean isVisible() { + return true; } public void applySourceValue(Object sourceValue) { - if (isReadOnly()) { - throw new IllegalStateException("Property is read only"); - } + assertEditable(); + assertEnabled(); if (sourceValue instanceof String) { try { buffer.setValue(valueFormatter.parse((String) sourceValue, getLocale())); sourceValue = null; - bindingStatus = BindingStatus.DIRTY; + status = BindingStatus.DIRTY; } catch (ParseException e) { this.sourceValue = sourceValue; sourceValueParseException = e; - bindingStatus = BindingStatus.INVALID_SOURCE_VALUE; + status = BindingStatus.INVALID_SOURCE_VALUE; } } else if (sourceValue instanceof String[]) { String[] sourceValues = (String[]) sourceValue; @@ -107,24 +114,24 @@ public class PropertyBinding implements Binding { } catch (ParseException e) { this.sourceValue = sourceValue; sourceValueParseException = e; - bindingStatus = BindingStatus.INVALID_SOURCE_VALUE; + status = BindingStatus.INVALID_SOURCE_VALUE; break; } } - if (bindingStatus != BindingStatus.INVALID_SOURCE_VALUE) { + if (status != BindingStatus.INVALID_SOURCE_VALUE) { buffer.setValue(parsed); sourceValue = null; - bindingStatus = BindingStatus.DIRTY; + status = BindingStatus.DIRTY; } } } public BindingStatus getStatus() { - return bindingStatus; + return status; } public Alert getStatusAlert() { - if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) { + if (status == BindingStatus.INVALID_SOURCE_VALUE) { return new Alert() { public String getCode() { return "typeMismatch"; @@ -138,7 +145,7 @@ public class PropertyBinding implements Binding { return Severity.ERROR; } }; - } else if (bindingStatus == BindingStatus.COMMIT_FAILURE) { + } else if (status == BindingStatus.COMMIT_FAILURE) { if (buffer.getFlushException() instanceof ConversionFailedException) { return new Alert() { public String getCode() { @@ -168,7 +175,7 @@ public class PropertyBinding implements Binding { } }; } - } else if (bindingStatus == BindingStatus.COMMITTED) { + } else if (status == BindingStatus.COMMITTED) { return new Alert() { public String getCode() { return "bindSucces"; @@ -188,14 +195,30 @@ public class PropertyBinding implements Binding { } public void commit() { - if (bindingStatus != BindingStatus.DIRTY) { - throw new IllegalStateException("Binding not dirty; nothing to commit"); - } - buffer.flush(); - if (buffer.flushFailed()) { - bindingStatus = BindingStatus.COMMIT_FAILURE; + assertEditable(); + assertEnabled(); + if (status == BindingStatus.DIRTY) { + buffer.flush(); + if (buffer.flushFailed()) { + status = BindingStatus.COMMIT_FAILURE; + } else { + status = BindingStatus.COMMITTED; + } } else { - bindingStatus = BindingStatus.COMMITTED; + throw new IllegalStateException("Binding is not dirty; nothing to commit"); + } + } + + public void revert() { + if (status == BindingStatus.INVALID_SOURCE_VALUE) { + sourceValue = null; + sourceValueParseException = null; + status = BindingStatus.CLEAN; + } else if (status == BindingStatus.DIRTY || status == BindingStatus.COMMIT_FAILURE) { + buffer.clear(); + status = BindingStatus.CLEAN; + } else { + throw new IllegalStateException("Nothing to revert"); } } @@ -210,9 +233,6 @@ public class PropertyBinding implements Binding { } public void setValue(Object value) { - if (isReadOnly()) { - throw new IllegalStateException("Property is read-only"); - } TypeDescriptor targetType = new TypeDescriptor(new MethodParameter(propertyDescriptor.getWriteMethod(), 0)); if (value != null && typeConverter.canConvert(value.getClass(), targetType)) { value = typeConverter.convert(value, targetType); @@ -230,11 +250,11 @@ public class PropertyBinding implements Binding { return new PropertyBinding(nestedProperty, getValue(), typeConverter); } - public boolean isIndexable() { + public boolean isList() { return getModel().getValueType().isArray() || List.class.isAssignableFrom(getModel().getValueType()); } - public Binding getIndexedBinding(int index) { + public Binding getListElementBinding(int index) { assertListProperty(); //return new IndexedBinding(index, (List) getValue(), getCollectionTypeDescriptor(), typeConverter); return null; @@ -244,7 +264,7 @@ public class PropertyBinding implements Binding { return Map.class.isAssignableFrom(getModel().getValueType()); } - public Binding getKeyedBinding(Object key) { + public Binding getMapValueBinding(Object key) { assertMapProperty(); if (key instanceof String) { try { @@ -259,7 +279,7 @@ public class PropertyBinding implements Binding { public String formatValue(Object value) { Formatter formatter; - if (isIndexable() || isMap()) { + if (isList() || isMap()) { formatter = indexedValueFormatter; } else { formatter = valueFormatter; @@ -343,7 +363,7 @@ public class PropertyBinding implements Binding { } private void assertScalarProperty() { - if (isIndexable()) { + if (isList()) { throw new IllegalArgumentException("Is a Collection but should be a scalar"); } if (isMap()) { @@ -352,75 +372,30 @@ public class PropertyBinding implements Binding { } private void assertListProperty() { - if (!isIndexable()) { + if (!isList()) { throw new IllegalStateException("Not a List property binding"); } } private void assertMapProperty() { - if (!isIndexable()) { + if (!isList()) { throw new IllegalStateException("Not a Map property binding"); } } + + private void assertEditable() { + if (!isEditable()) { + throw new IllegalStateException("Binding is not editable"); + } + } + + private void assertEnabled() { + if (!isEditable()) { + throw new IllegalStateException("Binding is not enabled"); + } + } private boolean markedNotEditable() { return false; } - - static class ValueBuffer { - - private Object value; - - private boolean hasValue; - - private Model model; - - private boolean flushFailed; - - private Exception flushFailureCause; - - public ValueBuffer(Model model) { - this.model = model; - } - - public boolean hasValue() { - return hasValue; - } - - public Object getValue() { - if (!hasValue()) { - throw new IllegalStateException("No value in buffer"); - } - return value; - } - - public void setValue(Object value) { - this.value = value; - hasValue = true; - } - - public void flush() { - try { - model.setValue(value); - clear(); - } catch (Exception e) { - flushFailed = true; - flushFailureCause = e; - } - } - - public void clear() { - value = null; - hasValue = false; - flushFailed = false; - } - - public boolean flushFailed() { - return flushFailed; - } - - public Exception getFlushException() { - return flushFailureCause; - } - } } \ No newline at end of file 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 b3e8023f46d..3f0a166c7e9 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 @@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue; import java.math.BigDecimal; import java.text.ParseException; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; @@ -21,6 +22,7 @@ import org.springframework.ui.binding.BindingResults; import org.springframework.ui.format.AnnotationFormatterFactory; import org.springframework.ui.format.Formatted; import org.springframework.ui.format.Formatter; +import org.springframework.ui.format.date.DateFormatter; import org.springframework.ui.format.number.CurrencyFormat; import org.springframework.ui.format.number.CurrencyFormatter; @@ -55,7 +57,6 @@ public class GenericBinderTests { BindingResults results = binder.bind(values); assertEquals(3, results.size()); - System.out.println(results); assertEquals("string", results.get(0).getProperty()); assertFalse(results.get(0).isFailure()); assertEquals("test", results.get(0).getSourceValue()); @@ -87,17 +88,15 @@ public class GenericBinderTests { assertEquals("typeMismatch", results.get(1).getAlert().getCode()); } - /* @Test public void bindSingleValuePropertyFormatter() throws ParseException { - BindingRulesBuilder builder = new BindingRulesBuilder(TestBean.class); - builder.bind("date").formatWith(new DateFormatter());; - GenericBinder binder = new GenericBinder(bean, builder.getBindingRules()); - + GenericBinder binder = new GenericBinder(bean); + binder.bindingRule("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() { BindingRulesBuilder builder = new BindingRulesBuilder(TestBean.class);