From 09c5d0eb977d0eb4cd85b77091b03997255fdf85 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Sun, 19 Jul 2009 06:26:48 +0000 Subject: [PATCH] binding status --- .../springframework/ui/binding/Binding.java | 76 +++++-- .../ui/binding/support/GenericBinder.java | 3 +- .../ui/binding/support/PropertyBinding.java | 211 ++++++++++++++---- .../binding/support/GenericBinderTests.java | 20 +- 4 files changed, 226 insertions(+), 84 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 b606069afc6..3256caf6ce9 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 @@ -16,6 +16,7 @@ package org.springframework.ui.binding; import org.springframework.ui.alert.Alert; +import org.springframework.ui.alert.Severity; /** * A binding between a source element and a model property. @@ -25,9 +26,10 @@ import org.springframework.ui.alert.Alert; public interface Binding { /** - * The bound value to display in the UI. - * Is the formatted model value if not dirty. - * Is the buffered value if dirty. + * The value to display in the UI. + * Is the formatted model value if {@link BindingStatus#CLEAN} or {@link BindingStatus#COMMITTED}. + * Is the formatted buffered value if {@link BindingStatus#DIRTY} or {@link BindingStatus#COMMIT_FAILURE}. + * Is the source value if {@link BindingStatus#INVALID_SOURCE_VALUE}. */ Object getValue(); @@ -39,38 +41,40 @@ public interface Binding { /** * 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. + * The source value is parsed and stored in the binding's value buffer. + * Sets to {@link BindingStatus#DIRTY} if succeeds. + * Sets to {@link BindingStatus#INVALID_SOURCE_VALUE} if fails. * @param sourceValue * @throws IllegalStateException if read only */ 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. + * The current binding status. + * Initially {@link BindingStatus#CLEAN clean}. + * Is {@link BindingStatus#DIRTY} after applying a source value to the value buffer. + * Is {@link BindingStatus#COMMITTED} after successfully committing the buffered value. + * Is {@link BindingStatus#INVALID_SOURCE_VALUE} if a source value could not be applied. + * Is {@link BindingStatus#COMMIT_FAILURE} if a buffered value could not be committed. */ - boolean isDirty(); + BindingStatus getStatus(); /** - * 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. + * 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. */ - boolean isValid(); + Alert getStatusAlert(); /** * Commit the buffered value to the model. - * @throws IllegalStateException if not dirty, not valid, or read-only + * Sets to {@link BindingStatus#CLEAN} if succeeds. + * Sets to {@link BindingStatus#COMMIT_FAILURE} if fails. + * @throws IllegalStateException if not {@link BindingStatus#DIRTY} or read-only */ void commit(); - - /** - * An Alert that communicates the current status of this Binding. - */ - Alert getStatusAlert(); /** * Access raw model values. @@ -139,4 +143,36 @@ public interface Binding { */ void setValue(Object value); } + + /** + * The states of a Binding. + * @author Keith Donald + */ + public enum BindingStatus { + + /** + * Initial state: No value is buffered, and there is a direct channel to the model value. + */ + CLEAN, + + /** + * An invalid source value is applied. + */ + INVALID_SOURCE_VALUE, + + /** + * The binding buffer contains a valid value that has not been committed. + */ + DIRTY, + + /** + * The buffered value has been committed. + */ + COMMITTED, + + /** + * The buffered value failed to commit. + */ + COMMIT_FAILURE + } } \ 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 989da08ba01..d0265623b3f 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 @@ -25,6 +25,7 @@ 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.Binding.BindingStatus; import org.springframework.util.Assert; /** @@ -138,7 +139,7 @@ public class GenericBinder implements Binder { return new PropertyNotWriteableResult(property, value, messageSource); } else { binding.applySourceValue(value); - if (binding.isValid()) { + if (binding.getStatus() == BindingStatus.DIRTY) { binding.commit(); } 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 f44015d19cd..37300d16ec0 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 @@ -20,9 +20,11 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeConverter; import org.springframework.core.convert.TypeDescriptor; import org.springframework.ui.alert.Alert; +import org.springframework.ui.alert.Severity; import org.springframework.ui.binding.Binding; import org.springframework.ui.format.Formatter; import org.springframework.util.ReflectionUtils; @@ -46,39 +48,49 @@ public class PropertyBinding implements Binding { private Object sourceValue; - // TODO make a ValueBuffer - private Object bufferedValue; + @SuppressWarnings("unused") + private ParseException sourceValueParseException; + + private ValueBuffer buffer; + + private BindingStatus bindingStatus; public PropertyBinding(String property, Object model, TypeConverter typeConverter) { this.propertyDescriptor = findPropertyDescriptor(property, model); this.property = property; this.model = model; this.typeConverter = typeConverter; + this.buffer = new ValueBuffer(getModel()); + bindingStatus = BindingStatus.CLEAN; } public Object getValue() { - if (isDirty()) { - // TODO null check isn't good enough - if (bufferedValue != null) { - return formatValue(bufferedValue); - } else { - return sourceValue; - } + if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) { + return sourceValue; + } else if (bindingStatus == BindingStatus.DIRTY || bindingStatus == BindingStatus.COMMIT_FAILURE) { + return formatValue(buffer.getValue()); } else { return formatValue(getModel().getValue()); } } + public boolean isReadOnly() { + return propertyDescriptor.getWriteMethod() == null || markedNotEditable(); + } + public void applySourceValue(Object sourceValue) { if (isReadOnly()) { - throw new IllegalStateException("Property is read-only"); + throw new IllegalStateException("Property is read only"); } - this.sourceValue = sourceValue; if (sourceValue instanceof String) { try { - this.bufferedValue = valueFormatter.parse((String) sourceValue, getLocale()); + buffer.setValue(valueFormatter.parse((String) sourceValue, getLocale())); + sourceValue = null; + bindingStatus = BindingStatus.DIRTY; } catch (ParseException e) { - + this.sourceValue = sourceValue; + sourceValueParseException = e; + bindingStatus = BindingStatus.INVALID_SOURCE_VALUE; } } else if (sourceValue instanceof String[]) { String[] sourceValues = (String[]) sourceValue; @@ -87,50 +99,103 @@ public class PropertyBinding implements Binding { parsedType = String.class; } Object parsed = Array.newInstance(parsedType, sourceValues.length); - boolean parseError = false; for (int i = 0; i < sourceValues.length; i++) { Object parsedValue; try { parsedValue = indexedValueFormatter.parse(sourceValues[i], LocaleContextHolder.getLocale()); Array.set(parsed, i, parsedValue); } catch (ParseException e) { - parseError = true; + this.sourceValue = sourceValue; + sourceValueParseException = e; + bindingStatus = BindingStatus.INVALID_SOURCE_VALUE; + break; } } - if (!parseError) { - bufferedValue = parsed; + if (bindingStatus != BindingStatus.INVALID_SOURCE_VALUE) { + buffer.setValue(parsed); + sourceValue = null; + bindingStatus = BindingStatus.DIRTY; } } } - - public boolean isDirty() { - return sourceValue != null || bufferedValue != null; + + public BindingStatus getStatus() { + return bindingStatus; } + + public Alert getStatusAlert() { + if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) { + return new Alert() { + public String getCode() { + return "typeMismatch"; + } - public boolean isValid() { - if (!isDirty()) { - return true; - } else { - if (bufferedValue == null) { - return false; + public String getMessage() { + return "Could not parse source value"; + } + + public Severity getSeverity() { + return Severity.ERROR; + } + }; + } else if (bindingStatus == BindingStatus.COMMIT_FAILURE) { + if (buffer.getFlushException() instanceof ConversionFailedException) { + return new Alert() { + public String getCode() { + return "typeMismatch"; + } + + public String getMessage() { + return "Could not convert source value"; + } + + public Severity getSeverity() { + return Severity.ERROR; + } + }; } else { - return true; + return new Alert() { + public String getCode() { + return "internalError"; + } + + public String getMessage() { + return "Internal error occurred"; + } + + public Severity getSeverity() { + return Severity.FATAL; + } + }; } + } else if (bindingStatus == BindingStatus.COMMITTED) { + return new Alert() { + public String getCode() { + return "bindSucces"; + } + + public String getMessage() { + return "Binding successful"; + } + + public Severity getSeverity() { + return Severity.INFO; + } + }; + } else { + return null; } } public void commit() { - if (!isDirty()) { + if (bindingStatus != BindingStatus.DIRTY) { throw new IllegalStateException("Binding not dirty; nothing to commit"); } - if (!isValid()) { - throw new IllegalStateException("Binding is invalid; only commit valid bindings"); - } - try { - getModel().setValue(bufferedValue); - this.bufferedValue = null; - } catch (Exception e) { - + buffer.flush(); + if (buffer.flushFailed()) { + bindingStatus = BindingStatus.COMMIT_FAILURE; + } else { + bindingStatus = BindingStatus.COMMITTED; } } @@ -157,10 +222,6 @@ public class PropertyBinding implements Binding { }; } - public Alert getStatusAlert() { - return null; - } - public Binding getBinding(String nestedProperty) { assertScalarProperty(); if (getValue() == null) { @@ -196,14 +257,16 @@ public class PropertyBinding implements Binding { return null; } - public boolean isReadOnly() { - return propertyDescriptor.getWriteMethod() != null && !markedNotEditable(); - } - public String formatValue(Object value) { - Class formattedType = getFormattedObjectType(valueFormatter.getClass()); + Formatter formatter; + if (isIndexable() || isMap()) { + formatter = indexedValueFormatter; + } else { + formatter = valueFormatter; + } + Class formattedType = getFormattedObjectType(formatter.getClass()); value = typeConverter.convert(value, formattedType); - return valueFormatter.format(value, getLocale()); + return formatter.format(value, getLocale()); } // internal helpers @@ -304,4 +367,60 @@ public class PropertyBinding implements Binding { 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 e88a8a8b6ad..b3e8023f46d 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,37 +7,22 @@ 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.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import junit.framework.Assert; - import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; 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.binding.config.BindingRulesBuilder; 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; -import org.springframework.ui.format.number.IntegerFormatter; -import org.springframework.ui.message.MockMessageSource; - -import edu.emory.mathcs.backport.java.util.Arrays; public class GenericBinderTests { @@ -59,7 +44,6 @@ public class GenericBinderTests { } - /* @Test public void bindSingleValuesWithDefaultTypeConverterConversion() { GenericBinder binder = new GenericBinder(bean); @@ -71,6 +55,7 @@ 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()); @@ -99,9 +84,10 @@ public class GenericBinderTests { BindingResults results = binder.bind(values); assertEquals(3, results.size()); assertTrue(results.get(1).isFailure()); - assertEquals("conversionFailed", results.get(1).getAlert().getCode()); + assertEquals("typeMismatch", results.get(1).getAlert().getCode()); } + /* @Test public void bindSingleValuePropertyFormatter() throws ParseException { BindingRulesBuilder builder = new BindingRulesBuilder(TestBean.class);