binding status

This commit is contained in:
Keith Donald 2009-07-19 06:26:48 +00:00
parent 8d3fbc5df8
commit 09c5d0eb97
4 changed files with 226 additions and 84 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.ui.binding; package org.springframework.ui.binding;
import org.springframework.ui.alert.Alert; import org.springframework.ui.alert.Alert;
import org.springframework.ui.alert.Severity;
/** /**
* A binding between a source element and a model property. * A binding between a source element and a model property.
@ -25,9 +26,10 @@ import org.springframework.ui.alert.Alert;
public interface Binding { public interface Binding {
/** /**
* The bound value to display in the UI. * The value to display in the UI.
* Is the formatted model value if not dirty. * Is the formatted model value if {@link BindingStatus#CLEAN} or {@link BindingStatus#COMMITTED}.
* Is the buffered value if dirty. * 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(); Object getValue();
@ -39,39 +41,41 @@ public interface Binding {
/** /**
* Apply the source value to this binding. * Apply the source value to this binding.
* The source value is parsed, validated, and stored in the binding's value buffer. * The source value is parsed and stored in the binding's value buffer.
* Sets 'dirty' status to true. * Sets to {@link BindingStatus#DIRTY} if succeeds.
* Sets 'valid' status to false if the source value is not valid. * Sets to {@link BindingStatus#INVALID_SOURCE_VALUE} if fails.
* @param sourceValue * @param sourceValue
* @throws IllegalStateException if read only * @throws IllegalStateException if read only
*/ */
void applySourceValue(Object sourceValue); void applySourceValue(Object sourceValue);
/** /**
* True if there is an uncommitted value in the binding buffer. * The current binding status.
* Set to true after applying a source value. * Initially {@link BindingStatus#CLEAN clean}.
* Set to false after a commit. * 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. * An alert that communicates the details of a BindingStatus change to the user.
* False if dirty and the buffered value appears valid but could not be committed. * Returns <code>null</code> if the BindingStatus has never changed.
* True otherwise. * Returns a {@link Severity#INFO} Alert with code <code>bindSuccess</code> after a successful commit.
* Returns a {@link Severity#ERROR} Alert with code <code>typeMismatch</code> if the source value could not be converted to type required by the Model.
* Returns a {@link Severity#FATAL} Alert with code <code>internalError</code> 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. * 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(); void commit();
/**
* An Alert that communicates the current status of this Binding.
*/
Alert getStatusAlert();
/** /**
* Access raw model values. * Access raw model values.
*/ */
@ -139,4 +143,36 @@ public interface Binding {
*/ */
void setValue(Object value); 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
}
} }

View File

@ -25,6 +25,7 @@ import org.springframework.ui.binding.Binder;
import org.springframework.ui.binding.Binding; import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.BindingResult; import org.springframework.ui.binding.BindingResult;
import org.springframework.ui.binding.BindingResults; import org.springframework.ui.binding.BindingResults;
import org.springframework.ui.binding.Binding.BindingStatus;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -138,7 +139,7 @@ public class GenericBinder implements Binder {
return new PropertyNotWriteableResult(property, value, messageSource); return new PropertyNotWriteableResult(property, value, messageSource);
} else { } else {
binding.applySourceValue(value); binding.applySourceValue(value);
if (binding.isValid()) { if (binding.getStatus() == BindingStatus.DIRTY) {
binding.commit(); binding.commit();
} }
return new BindingStatusResult(property, value, binding.getStatusAlert()); return new BindingStatusResult(property, value, binding.getStatusAlert());

View File

@ -20,9 +20,11 @@ import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.core.GenericTypeResolver; import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeConverter; import org.springframework.core.convert.TypeConverter;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.ui.alert.Alert; import org.springframework.ui.alert.Alert;
import org.springframework.ui.alert.Severity;
import org.springframework.ui.binding.Binding; import org.springframework.ui.binding.Binding;
import org.springframework.ui.format.Formatter; import org.springframework.ui.format.Formatter;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@ -46,39 +48,49 @@ public class PropertyBinding implements Binding {
private Object sourceValue; private Object sourceValue;
// TODO make a ValueBuffer @SuppressWarnings("unused")
private Object bufferedValue; private ParseException sourceValueParseException;
private ValueBuffer buffer;
private BindingStatus bindingStatus;
public PropertyBinding(String property, Object model, TypeConverter typeConverter) { public PropertyBinding(String property, Object model, TypeConverter typeConverter) {
this.propertyDescriptor = findPropertyDescriptor(property, model); this.propertyDescriptor = findPropertyDescriptor(property, model);
this.property = property; this.property = property;
this.model = model; this.model = model;
this.typeConverter = typeConverter; this.typeConverter = typeConverter;
this.buffer = new ValueBuffer(getModel());
bindingStatus = BindingStatus.CLEAN;
} }
public Object getValue() { public Object getValue() {
if (isDirty()) { if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) {
// TODO null check isn't good enough return sourceValue;
if (bufferedValue != null) { } else if (bindingStatus == BindingStatus.DIRTY || bindingStatus == BindingStatus.COMMIT_FAILURE) {
return formatValue(bufferedValue); return formatValue(buffer.getValue());
} else {
return sourceValue;
}
} else { } else {
return formatValue(getModel().getValue()); return formatValue(getModel().getValue());
} }
} }
public boolean isReadOnly() {
return propertyDescriptor.getWriteMethod() == null || markedNotEditable();
}
public void applySourceValue(Object sourceValue) { public void applySourceValue(Object sourceValue) {
if (isReadOnly()) { if (isReadOnly()) {
throw new IllegalStateException("Property is read-only"); throw new IllegalStateException("Property is read only");
} }
this.sourceValue = sourceValue;
if (sourceValue instanceof String) { if (sourceValue instanceof String) {
try { try {
this.bufferedValue = valueFormatter.parse((String) sourceValue, getLocale()); buffer.setValue(valueFormatter.parse((String) sourceValue, getLocale()));
sourceValue = null;
bindingStatus = BindingStatus.DIRTY;
} catch (ParseException e) { } catch (ParseException e) {
this.sourceValue = sourceValue;
sourceValueParseException = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
} }
} else if (sourceValue instanceof String[]) { } else if (sourceValue instanceof String[]) {
String[] sourceValues = (String[]) sourceValue; String[] sourceValues = (String[]) sourceValue;
@ -87,50 +99,103 @@ public class PropertyBinding implements Binding {
parsedType = String.class; parsedType = String.class;
} }
Object parsed = Array.newInstance(parsedType, sourceValues.length); Object parsed = Array.newInstance(parsedType, sourceValues.length);
boolean parseError = false;
for (int i = 0; i < sourceValues.length; i++) { for (int i = 0; i < sourceValues.length; i++) {
Object parsedValue; Object parsedValue;
try { try {
parsedValue = indexedValueFormatter.parse(sourceValues[i], LocaleContextHolder.getLocale()); parsedValue = indexedValueFormatter.parse(sourceValues[i], LocaleContextHolder.getLocale());
Array.set(parsed, i, parsedValue); Array.set(parsed, i, parsedValue);
} catch (ParseException e) { } catch (ParseException e) {
parseError = true; this.sourceValue = sourceValue;
sourceValueParseException = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
break;
} }
} }
if (!parseError) { if (bindingStatus != BindingStatus.INVALID_SOURCE_VALUE) {
bufferedValue = parsed; buffer.setValue(parsed);
sourceValue = null;
bindingStatus = BindingStatus.DIRTY;
} }
} }
} }
public boolean isDirty() { public BindingStatus getStatus() {
return sourceValue != null || bufferedValue != null; return bindingStatus;
} }
public boolean isValid() { public Alert getStatusAlert() {
if (!isDirty()) { if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) {
return true; return new Alert() {
} else { public String getCode() {
if (bufferedValue == null) { return "typeMismatch";
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 { } 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() { public void commit() {
if (!isDirty()) { if (bindingStatus != BindingStatus.DIRTY) {
throw new IllegalStateException("Binding not dirty; nothing to commit"); throw new IllegalStateException("Binding not dirty; nothing to commit");
} }
if (!isValid()) { buffer.flush();
throw new IllegalStateException("Binding is invalid; only commit valid bindings"); if (buffer.flushFailed()) {
} bindingStatus = BindingStatus.COMMIT_FAILURE;
try { } else {
getModel().setValue(bufferedValue); bindingStatus = BindingStatus.COMMITTED;
this.bufferedValue = null;
} catch (Exception e) {
} }
} }
@ -157,10 +222,6 @@ public class PropertyBinding implements Binding {
}; };
} }
public Alert getStatusAlert() {
return null;
}
public Binding getBinding(String nestedProperty) { public Binding getBinding(String nestedProperty) {
assertScalarProperty(); assertScalarProperty();
if (getValue() == null) { if (getValue() == null) {
@ -196,14 +257,16 @@ public class PropertyBinding implements Binding {
return null; return null;
} }
public boolean isReadOnly() {
return propertyDescriptor.getWriteMethod() != null && !markedNotEditable();
}
public String formatValue(Object value) { 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); value = typeConverter.convert(value, formattedType);
return valueFormatter.format(value, getLocale()); return formatter.format(value, getLocale());
} }
// internal helpers // internal helpers
@ -304,4 +367,60 @@ public class PropertyBinding implements Binding {
return false; 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;
}
}
} }

View File

@ -7,37 +7,22 @@ import static org.junit.Assert.assertTrue;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import junit.framework.Assert;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.springframework.context.i18n.LocaleContextHolder; 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.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.AnnotationFormatterFactory;
import org.springframework.ui.format.Formatted; import org.springframework.ui.format.Formatted;
import org.springframework.ui.format.Formatter; 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.CurrencyFormat;
import org.springframework.ui.format.number.CurrencyFormatter; 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 { public class GenericBinderTests {
@ -59,7 +44,6 @@ public class GenericBinderTests {
} }
/*
@Test @Test
public void bindSingleValuesWithDefaultTypeConverterConversion() { public void bindSingleValuesWithDefaultTypeConverterConversion() {
GenericBinder binder = new GenericBinder(bean); GenericBinder binder = new GenericBinder(bean);
@ -71,6 +55,7 @@ public class GenericBinderTests {
BindingResults results = binder.bind(values); BindingResults results = binder.bind(values);
assertEquals(3, results.size()); assertEquals(3, results.size());
System.out.println(results);
assertEquals("string", results.get(0).getProperty()); assertEquals("string", results.get(0).getProperty());
assertFalse(results.get(0).isFailure()); assertFalse(results.get(0).isFailure());
assertEquals("test", results.get(0).getSourceValue()); assertEquals("test", results.get(0).getSourceValue());
@ -99,9 +84,10 @@ public class GenericBinderTests {
BindingResults results = binder.bind(values); BindingResults results = binder.bind(values);
assertEquals(3, results.size()); assertEquals(3, results.size());
assertTrue(results.get(1).isFailure()); assertTrue(results.get(1).isFailure());
assertEquals("conversionFailed", results.get(1).getAlert().getCode()); assertEquals("typeMismatch", results.get(1).getAlert().getCode());
} }
/*
@Test @Test
public void bindSingleValuePropertyFormatter() throws ParseException { public void bindSingleValuePropertyFormatter() throws ParseException {
BindingRulesBuilder builder = new BindingRulesBuilder(TestBean.class); BindingRulesBuilder builder = new BindingRulesBuilder(TestBean.class);