additional binding metadata

This commit is contained in:
Keith Donald 2009-07-20 03:48:32 +00:00
parent 263d502f51
commit 9c78616e11
5 changed files with 157 additions and 131 deletions

View File

@ -34,10 +34,25 @@ public interface Binding {
Object getValue(); Object getValue();
/** /**
* If this Binding is read-only. * If this Binding is editable.
* A read-only Binding cannot have source values applied and cannot be committed. * 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. * 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#DIRTY} if succeeds.
* Sets to {@link BindingStatus#INVALID_SOURCE_VALUE} if fails. * Sets to {@link BindingStatus#INVALID_SOURCE_VALUE} if fails.
* @param sourceValue * @param sourceValue
* @throws IllegalStateException if {@link #isReadOnly()} * @throws IllegalStateException if {@link #isEditable()}
*/ */
void applySourceValue(Object sourceValue); void applySourceValue(Object sourceValue);
@ -60,11 +75,12 @@ public interface Binding {
BindingStatus getStatus(); BindingStatus getStatus();
/** /**
* An alert that communicates the details of a BindingStatus change to the user. * An alert that communicates current status to the user.
* Returns <code>null</code> if the BindingStatus has never changed. * Returns <code>null</code> if {@link BindingStatus#CLEAN} and {@link ValidationStatus#NOT_VALIDATED}.
* Returns a {@link Severity#INFO} Alert with code <code>bindSuccess</code> after a successful commit. * Returns a {@link Severity#INFO} Alert with code <code>bindSuccess</code> when {@link BindingStatus#COMMITTED}.
* 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#ERROR} Alert with code <code>typeMismatch</code> 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 <code>internalError</code> if the buffered value could not be committed due to a unexpected runtime exception. * Returns a {@link Severity#FATAL} Alert with code <code>internalError</code> 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(); Alert getStatusAlert();
@ -72,10 +88,16 @@ public interface Binding {
* Commit the buffered value to the model. * Commit the buffered value to the model.
* Sets to {@link BindingStatus#COMMITTED} if succeeds. * Sets to {@link BindingStatus#COMMITTED} if succeeds.
* Sets to {@link BindingStatus#COMMIT_FAILURE} if fails. * 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(); void commit();
/**
* Clear the buffered value without committing.
* @throws IllegalStateException if BindingStatus is CLEAN or COMMITTED.
*/
void revert();
/** /**
* Access raw model values. * 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. * 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. * If a List, get a Binding to a element in the List.
* @param index the element index * @param index the element index
* @return the indexed binding * @return the indexed binding
*/ */
Binding getIndexedBinding(int index); Binding getListElementBinding(int index);
/** /**
* If bound to a {@link java.util.Map}. * If bound to a {@link java.util.Map}.
@ -110,11 +132,11 @@ public interface Binding {
* @param key the map key * @param key the map key
* @return the keyed binding * @return the keyed binding
*/ */
Binding getKeyedBinding(Object key); Binding getMapValueBinding(Object key);
/** /**
* Format a potential model value for display. * 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. * 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 * @param potentialValue the potential value
* @return the formatted string * @return the formatted string
@ -145,7 +167,7 @@ public interface Binding {
} }
/** /**
* The states of a Binding. * Binding states.
* @author Keith Donald * @author Keith Donald
*/ */
public enum BindingStatus { public enum BindingStatus {
@ -175,4 +197,26 @@ public interface Binding {
*/ */
COMMIT_FAILURE 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
}
} }

View File

@ -18,7 +18,7 @@ package org.springframework.ui.binding.config;
import org.springframework.ui.format.Formatter; 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 * @author Keith Donald
*/ */
public interface BindingRuleConfiguration { public interface BindingRuleConfiguration {
@ -31,23 +31,21 @@ public interface BindingRuleConfiguration {
BindingRuleConfiguration formatWith(Formatter<?> formatter); 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. * Default is null.
*/ */
BindingRuleConfiguration formatElementsWith(Formatter<?> formatter); BindingRuleConfiguration formatElementsWith(Formatter<?> formatter);
/** /**
* Mark the binding as required. * Mark the binding as read only.
* A required binding will generate an exception if no sourceValue is provided to a bind invocation. * A read-only binding cannot have source values applied and cannot be committed.
* This attribute is used to detect client configuration errors.
* It is not intended to be used as a user data validation constraint.
* Examples:
* <pre>
* 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].
* </pre>
*/ */
BindingRuleConfiguration required(); BindingRuleConfiguration readOnly();
} }

View File

@ -26,6 +26,7 @@ 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.ui.binding.Binding.BindingStatus;
import org.springframework.ui.binding.config.BindingRuleConfiguration;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -77,6 +78,15 @@ public class GenericBinder implements Binder {
this.typeConverter = typeConverter; 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 // implementing Binder
public Object getModel() { public Object getModel() {
@ -93,9 +103,9 @@ public class GenericBinder implements Binder {
for (PropertyPathElement element : path.getNestedElements()) { for (PropertyPathElement element : path.getNestedElements()) {
if (element.isIndex()) { if (element.isIndex()) {
if (binding.isMap()) { if (binding.isMap()) {
binding = binding.getKeyedBinding(element.getValue()); binding = binding.getMapValueBinding(element.getValue());
} else if (binding.isIndexable()) { } else if (binding.isList()) {
binding = binding.getIndexedBinding(element.getIntValue()); binding = binding.getListElementBinding(element.getIntValue());
} else { } else {
throw new IllegalArgumentException("Attempted to index a property that is not a Collection or Map"); 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<String, ? extends Object> sourceValue, Binding binding) { private BindingResult bind(Map.Entry<String, ? extends Object> sourceValue, Binding binding) {
String property = sourceValue.getKey(); String property = sourceValue.getKey();
Object value = sourceValue.getValue(); Object value = sourceValue.getValue();
if (binding.isReadOnly()) { if (binding.isEditable()) {
return new PropertyNotWriteableResult(property, value, messageSource); return new PropertyNotWriteableResult(property, value, messageSource);
} else { } else {
binding.applySourceValue(value); binding.applySourceValue(value);
@ -145,5 +155,5 @@ public class GenericBinder implements Binder {
return new BindingStatusResult(property, value, binding.getStatusAlert()); return new BindingStatusResult(property, value, binding.getStatusAlert());
} }
} }
} }

View File

@ -53,7 +53,7 @@ public class PropertyBinding implements Binding {
private ValueBuffer buffer; private ValueBuffer buffer;
private BindingStatus bindingStatus; private BindingStatus status;
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);
@ -61,36 +61,43 @@ public class PropertyBinding implements Binding {
this.model = model; this.model = model;
this.typeConverter = typeConverter; this.typeConverter = typeConverter;
this.buffer = new ValueBuffer(getModel()); this.buffer = new ValueBuffer(getModel());
bindingStatus = BindingStatus.CLEAN; status = BindingStatus.CLEAN;
} }
public Object getValue() { public Object getValue() {
if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) { if (status == BindingStatus.INVALID_SOURCE_VALUE) {
return sourceValue; return sourceValue;
} else if (bindingStatus == BindingStatus.DIRTY || bindingStatus == BindingStatus.COMMIT_FAILURE) { } else if (status == BindingStatus.DIRTY || status == BindingStatus.COMMIT_FAILURE) {
return formatValue(buffer.getValue()); return formatValue(buffer.getValue());
} else { } else {
return formatValue(getModel().getValue()); return formatValue(getModel().getValue());
} }
} }
public boolean isReadOnly() { public boolean isEditable() {
return propertyDescriptor.getWriteMethod() == null || markedNotEditable(); return propertyDescriptor.getWriteMethod() != null || !markedNotEditable();
}
public boolean isEnabled() {
return true;
}
public boolean isVisible() {
return true;
} }
public void applySourceValue(Object sourceValue) { public void applySourceValue(Object sourceValue) {
if (isReadOnly()) { assertEditable();
throw new IllegalStateException("Property is read only"); assertEnabled();
}
if (sourceValue instanceof String) { if (sourceValue instanceof String) {
try { try {
buffer.setValue(valueFormatter.parse((String) sourceValue, getLocale())); buffer.setValue(valueFormatter.parse((String) sourceValue, getLocale()));
sourceValue = null; sourceValue = null;
bindingStatus = BindingStatus.DIRTY; status = BindingStatus.DIRTY;
} catch (ParseException e) { } catch (ParseException e) {
this.sourceValue = sourceValue; this.sourceValue = sourceValue;
sourceValueParseException = e; sourceValueParseException = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE; status = BindingStatus.INVALID_SOURCE_VALUE;
} }
} else if (sourceValue instanceof String[]) { } else if (sourceValue instanceof String[]) {
String[] sourceValues = (String[]) sourceValue; String[] sourceValues = (String[]) sourceValue;
@ -107,24 +114,24 @@ public class PropertyBinding implements Binding {
} catch (ParseException e) { } catch (ParseException e) {
this.sourceValue = sourceValue; this.sourceValue = sourceValue;
sourceValueParseException = e; sourceValueParseException = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE; status = BindingStatus.INVALID_SOURCE_VALUE;
break; break;
} }
} }
if (bindingStatus != BindingStatus.INVALID_SOURCE_VALUE) { if (status != BindingStatus.INVALID_SOURCE_VALUE) {
buffer.setValue(parsed); buffer.setValue(parsed);
sourceValue = null; sourceValue = null;
bindingStatus = BindingStatus.DIRTY; status = BindingStatus.DIRTY;
} }
} }
} }
public BindingStatus getStatus() { public BindingStatus getStatus() {
return bindingStatus; return status;
} }
public Alert getStatusAlert() { public Alert getStatusAlert() {
if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) { if (status == BindingStatus.INVALID_SOURCE_VALUE) {
return new Alert() { return new Alert() {
public String getCode() { public String getCode() {
return "typeMismatch"; return "typeMismatch";
@ -138,7 +145,7 @@ public class PropertyBinding implements Binding {
return Severity.ERROR; return Severity.ERROR;
} }
}; };
} else if (bindingStatus == BindingStatus.COMMIT_FAILURE) { } else if (status == BindingStatus.COMMIT_FAILURE) {
if (buffer.getFlushException() instanceof ConversionFailedException) { if (buffer.getFlushException() instanceof ConversionFailedException) {
return new Alert() { return new Alert() {
public String getCode() { 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() { return new Alert() {
public String getCode() { public String getCode() {
return "bindSucces"; return "bindSucces";
@ -188,14 +195,30 @@ public class PropertyBinding implements Binding {
} }
public void commit() { public void commit() {
if (bindingStatus != BindingStatus.DIRTY) { assertEditable();
throw new IllegalStateException("Binding not dirty; nothing to commit"); assertEnabled();
} if (status == BindingStatus.DIRTY) {
buffer.flush(); buffer.flush();
if (buffer.flushFailed()) { if (buffer.flushFailed()) {
bindingStatus = BindingStatus.COMMIT_FAILURE; status = BindingStatus.COMMIT_FAILURE;
} else {
status = BindingStatus.COMMITTED;
}
} else { } 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) { public void setValue(Object value) {
if (isReadOnly()) {
throw new IllegalStateException("Property is read-only");
}
TypeDescriptor targetType = new TypeDescriptor(new MethodParameter(propertyDescriptor.getWriteMethod(), 0)); TypeDescriptor targetType = new TypeDescriptor(new MethodParameter(propertyDescriptor.getWriteMethod(), 0));
if (value != null && typeConverter.canConvert(value.getClass(), targetType)) { if (value != null && typeConverter.canConvert(value.getClass(), targetType)) {
value = typeConverter.convert(value, targetType); value = typeConverter.convert(value, targetType);
@ -230,11 +250,11 @@ public class PropertyBinding implements Binding {
return new PropertyBinding(nestedProperty, getValue(), typeConverter); return new PropertyBinding(nestedProperty, getValue(), typeConverter);
} }
public boolean isIndexable() { public boolean isList() {
return getModel().getValueType().isArray() || List.class.isAssignableFrom(getModel().getValueType()); return getModel().getValueType().isArray() || List.class.isAssignableFrom(getModel().getValueType());
} }
public Binding getIndexedBinding(int index) { public Binding getListElementBinding(int index) {
assertListProperty(); assertListProperty();
//return new IndexedBinding(index, (List) getValue(), getCollectionTypeDescriptor(), typeConverter); //return new IndexedBinding(index, (List) getValue(), getCollectionTypeDescriptor(), typeConverter);
return null; return null;
@ -244,7 +264,7 @@ public class PropertyBinding implements Binding {
return Map.class.isAssignableFrom(getModel().getValueType()); return Map.class.isAssignableFrom(getModel().getValueType());
} }
public Binding getKeyedBinding(Object key) { public Binding getMapValueBinding(Object key) {
assertMapProperty(); assertMapProperty();
if (key instanceof String) { if (key instanceof String) {
try { try {
@ -259,7 +279,7 @@ public class PropertyBinding implements Binding {
public String formatValue(Object value) { public String formatValue(Object value) {
Formatter formatter; Formatter formatter;
if (isIndexable() || isMap()) { if (isList() || isMap()) {
formatter = indexedValueFormatter; formatter = indexedValueFormatter;
} else { } else {
formatter = valueFormatter; formatter = valueFormatter;
@ -343,7 +363,7 @@ public class PropertyBinding implements Binding {
} }
private void assertScalarProperty() { private void assertScalarProperty() {
if (isIndexable()) { if (isList()) {
throw new IllegalArgumentException("Is a Collection but should be a scalar"); throw new IllegalArgumentException("Is a Collection but should be a scalar");
} }
if (isMap()) { if (isMap()) {
@ -352,75 +372,30 @@ public class PropertyBinding implements Binding {
} }
private void assertListProperty() { private void assertListProperty() {
if (!isIndexable()) { if (!isList()) {
throw new IllegalStateException("Not a List property binding"); throw new IllegalStateException("Not a List property binding");
} }
} }
private void assertMapProperty() { private void assertMapProperty() {
if (!isIndexable()) { if (!isList()) {
throw new IllegalStateException("Not a Map property binding"); 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() { private boolean markedNotEditable() {
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,6 +7,7 @@ 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.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; 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.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;
@ -55,7 +57,6 @@ 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());
@ -87,17 +88,15 @@ public class GenericBinderTests {
assertEquals("typeMismatch", 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); GenericBinder binder = new GenericBinder(bean);
builder.bind("date").formatWith(new DateFormatter());; binder.bindingRule("date").formatWith(new DateFormatter());
GenericBinder binder = new GenericBinder(bean, builder.getBindingRules());
binder.bind(Collections.singletonMap("date", "2009-06-01")); binder.bind(Collections.singletonMap("date", "2009-06-01"));
assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate()); assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate());
} }
/*
@Test @Test
public void bindSingleValuePropertyFormatterParseException() { public void bindSingleValuePropertyFormatterParseException() {
BindingRulesBuilder builder = new BindingRulesBuilder(TestBean.class); BindingRulesBuilder builder = new BindingRulesBuilder(TestBean.class);