nested binding work; list binding still has kinks

This commit is contained in:
Keith Donald 2009-07-21 22:32:06 +00:00
parent f739c3fde1
commit 14dd30c5a5
10 changed files with 619 additions and 535 deletions

View File

@ -15,7 +15,6 @@
*/ */
package org.springframework.ui.binding; package org.springframework.ui.binding;
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.alert.Severity;
@ -114,33 +113,6 @@ public interface Binding {
*/ */
void revert(); void revert();
/**
* For accessing the raw bound model object.
* @author Keith Donald
*/
public interface Model {
/**
* The model value.
*/
Object getValue();
/**
* The model value type.
*/
Class<?> getValueType();
/**
* The model value type descriptor.
*/
TypeDescriptor<?> getValueTypeDescriptor();
/**
* Set the model value.
*/
void setValue(Object value);
}
/** /**
* Get a Binding to a nested property value. * Get a Binding to a nested property value.
* @param property the nested property name, such as "foo"; should not be a property path like "foo.bar" * @param property the nested property name, such as "foo"; should not be a property path like "foo.bar"

View File

@ -0,0 +1,375 @@
package org.springframework.ui.binding.support;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.text.ParseException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeConverter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.style.StylerUtils;
import org.springframework.ui.alert.Alert;
import org.springframework.ui.alert.Severity;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.support.GenericBinder.BindingContext;
import org.springframework.ui.format.Formatter;
import org.springframework.ui.message.MessageBuilder;
import org.springframework.ui.message.ResolvableArgument;
public abstract class AbstractBinding implements Binding {
private BindingContext bindingContext;
private ValueBuffer buffer;
private BindingStatus status;
private Object sourceValue;
private Exception invalidSourceValueCause;
public AbstractBinding(BindingContext bindingContext) {
this.bindingContext = bindingContext;
buffer = new ValueBuffer(getValueModel());
status = BindingStatus.CLEAN;
}
// implementing Binding
public String getRenderValue() {
return format(getValue(), bindingContext.getFormatter());
}
public Object getValue() {
if (status == BindingStatus.DIRTY || status == BindingStatus.COMMIT_FAILURE) {
return buffer.getValue();
} else {
return getValueModel().getValue();
}
}
public Class<?> getValueType() {
return getValueModel().getValueType();
}
public boolean isEditable() {
return bindingContext.getEditableCondition().isTrue();
}
public boolean isEnabled() {
return bindingContext.getEnabledCondition().isTrue();
}
public boolean isVisible() {
return bindingContext.getVisibleCondition().isTrue();
}
public void applySourceValue(Object sourceValue) {
assertEditable();
assertEnabled();
if (sourceValue instanceof String) {
try {
Object parsed = bindingContext.getFormatter().parse((String) sourceValue, getLocale());
buffer.setValue(coerseToValueType(parsed));
sourceValue = null;
status = BindingStatus.DIRTY;
} catch (ParseException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
}
} else if (sourceValue instanceof String[]) {
String[] sourceValues = (String[]) sourceValue;
Class<?> parsedType = getFormattedObjectType(bindingContext.getElementFormatter().getClass());
if (parsedType == null) {
parsedType = String.class;
}
Object parsed = Array.newInstance(parsedType, sourceValues.length);
for (int i = 0; i < sourceValues.length; i++) {
Object parsedValue;
try {
parsedValue = bindingContext.getElementFormatter().parse(sourceValues[i], getLocale());
Array.set(parsed, i, parsedValue);
} catch (ParseException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
break;
}
}
if (status != BindingStatus.INVALID_SOURCE_VALUE) {
try {
buffer.setValue(coerseToValueType(parsed));
sourceValue = null;
status = BindingStatus.DIRTY;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
}
}
} else {
try {
buffer.setValue(coerseToValueType(sourceValue));
sourceValue = null;
status = BindingStatus.DIRTY;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
}
}
}
public Object getInvalidSourceValue() {
if (status != BindingStatus.INVALID_SOURCE_VALUE) {
throw new IllegalStateException("No invalid source value");
}
return sourceValue;
}
public BindingStatus getStatus() {
return status;
}
public Alert getStatusAlert() {
if (status == BindingStatus.INVALID_SOURCE_VALUE) {
return new AbstractAlert() {
public String getCode() {
return "typeMismatch";
}
public String getMessage() {
MessageBuilder builder = new MessageBuilder(bindingContext.getMessageSource());
builder.code(getCode());
if (invalidSourceValueCause instanceof ParseException) {
ParseException e = (ParseException) invalidSourceValueCause;
builder.arg("label", bindingContext.getLabel());
builder.arg("value", sourceValue);
builder.arg("errorOffset", e.getErrorOffset());
builder.defaultMessage("Failed to bind '" + bindingContext.getLabel() + "'; the source value "
+ StylerUtils.style(sourceValue) + " has an invalid format and could no be parsed");
} else {
ConversionFailedException e = (ConversionFailedException) invalidSourceValueCause;
builder.arg("label", new ResolvableArgument(bindingContext.getLabel()));
builder.arg("value", sourceValue);
builder.defaultMessage("Failed to bind '" + bindingContext.getLabel() + "'; the source value "
+ StylerUtils.style(sourceValue) + " has could not be converted to "
+ e.getTargetType().getName());
}
return builder.build();
}
public Severity getSeverity() {
return Severity.ERROR;
}
};
} else if (status == BindingStatus.COMMIT_FAILURE) {
return new AbstractAlert() {
public String getCode() {
return "internalError";
}
public String getMessage() {
buffer.getFlushException().printStackTrace();
return "Internal error occurred; message = [" + buffer.getFlushException().getMessage() + "]";
}
public Severity getSeverity() {
return Severity.FATAL;
}
};
} else if (status == BindingStatus.COMMITTED) {
return new AbstractAlert() {
public String getCode() {
return "bindSuccess";
}
public String getMessage() {
return "Binding successful";
}
public Severity getSeverity() {
return Severity.INFO;
}
};
} else {
return null;
}
}
public void commit() {
assertEditable();
assertEnabled();
if (status == BindingStatus.DIRTY) {
buffer.flush();
if (buffer.flushFailed()) {
status = BindingStatus.COMMIT_FAILURE;
} else {
status = BindingStatus.COMMITTED;
}
} else {
throw new IllegalStateException("Binding is not dirty; nothing to commit");
}
}
public void revert() {
if (status == BindingStatus.INVALID_SOURCE_VALUE) {
sourceValue = null;
invalidSourceValueCause = 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");
}
}
public Binding getBinding(String property) {
return bindingContext.getBinding(property);
}
public boolean isList() {
return List.class.isAssignableFrom(getValueType());
}
public Binding getListElementBinding(int index) {
return bindingContext.getListElementBinding(index);
}
public boolean isMap() {
return Map.class.isAssignableFrom(getValueType());
}
public Binding getMapValueBinding(Object key) {
return bindingContext.getMapValueBinding(key);
}
@SuppressWarnings("unchecked")
public String formatValue(Object value) {
Formatter formatter;
if (isList() || isMap()) {
formatter = getBindingContext().getElementFormatter();
} else {
formatter = getBindingContext().getFormatter();
}
return format(value, formatter);
}
// subclassing hooks
protected BindingContext getBindingContext() {
return bindingContext;
}
protected abstract ValueModel getValueModel();
protected Locale getLocale() {
return LocaleContextHolder.getLocale();
}
protected String format(Object value, Formatter formatter) {
Class<?> formattedType = getFormattedObjectType(formatter.getClass());
value = bindingContext.getTypeConverter().convert(value, formattedType);
return formatter.format(value, getLocale());
}
private Class getFormattedObjectType(Class formatterClass) {
Class classToIntrospect = formatterClass;
while (classToIntrospect != null) {
Type[] ifcs = classToIntrospect.getGenericInterfaces();
for (Type ifc : ifcs) {
if (ifc instanceof ParameterizedType) {
ParameterizedType paramIfc = (ParameterizedType) ifc;
Type rawType = paramIfc.getRawType();
if (Formatter.class.equals(rawType)) {
Type arg = paramIfc.getActualTypeArguments()[0];
if (arg instanceof TypeVariable) {
arg = GenericTypeResolver.resolveTypeVariable((TypeVariable) arg, formatterClass);
}
if (arg instanceof Class) {
return (Class) arg;
}
} else if (Formatter.class.isAssignableFrom((Class) rawType)) {
return getFormattedObjectType((Class) rawType);
}
} else if (Formatter.class.isAssignableFrom((Class) ifc)) {
return getFormattedObjectType((Class) ifc);
}
}
classToIntrospect = classToIntrospect.getSuperclass();
}
return null;
}
private Object coerseToValueType(Object parsed) {
TypeDescriptor targetType = getValueModel().getValueTypeDescriptor();
TypeConverter converter = bindingContext.getTypeConverter();
if (parsed != null && converter.canConvert(parsed.getClass(), targetType)) {
return converter.convert(parsed, targetType);
} else {
return parsed;
}
}
private void assertEditable() {
if (!isEditable()) {
throw new IllegalStateException("Binding is not editable");
}
}
private void assertEnabled() {
if (!isEditable()) {
throw new IllegalStateException("Binding is not enabled");
}
}
// internal helpers
static abstract class AbstractAlert implements Alert {
public String toString() {
return getCode() + " - " + getMessage();
}
}
/**
* For accessing the raw bound model object.
* @author Keith Donald
*/
public interface ValueModel {
/**
* The model value.
*/
Object getValue();
/**
* The model value type.
*/
Class<?> getValueType();
/**
* The model value type descriptor.
*/
TypeDescriptor<?> getValueTypeDescriptor();
/**
* Set the model value.
*/
void setValue(Object value);
}
}

View File

@ -52,15 +52,15 @@ public class GenericBinder implements Binder {
private Object model; private Object model;
private Map<String, GenericBindingRule> bindingRules; private Map<String, GenericBindingRule> bindingRules;
private FormatterRegistry formatterRegistry; private FormatterRegistry formatterRegistry;
private TypeConverter typeConverter; private TypeConverter typeConverter;
private MessageSource messageSource; private MessageSource messageSource;
private String[] requiredProperties = new String[0]; private String[] requiredProperties = new String[0];
/** /**
* Creates a new binder for the model object. * Creates a new binder for the model object.
* @param model the model object containing properties this binder will bind to * @param model the model object containing properties this binder will bind to
@ -82,7 +82,7 @@ public class GenericBinder implements Binder {
Assert.notNull(formatterRegistry, "The FormatterRegistry is required"); Assert.notNull(formatterRegistry, "The FormatterRegistry is required");
this.formatterRegistry = formatterRegistry; this.formatterRegistry = formatterRegistry;
} }
/** /**
* Configure the MessageSource that resolves localized {@link BindingResult} alert messages. * Configure the MessageSource that resolves localized {@link BindingResult} alert messages.
* @param messageSource the message source * @param messageSource the message source
@ -116,7 +116,7 @@ public class GenericBinder implements Binder {
} }
return rule; return rule;
} }
// implementing Binder // implementing Binder
public Object getModel() { public Object getModel() {
@ -126,6 +126,7 @@ public class GenericBinder implements Binder {
public Binding getBinding(String property) { public Binding getBinding(String property) {
PropertyPath path = new PropertyPath(property); PropertyPath path = new PropertyPath(property);
Binding binding = getBindingRule(path.getFirstElement().getValue()).getBinding(model); Binding binding = getBindingRule(path.getFirstElement().getValue()).getBinding(model);
System.out.println(path);
for (PropertyPathElement element : path.getNestedElements()) { for (PropertyPathElement element : path.getNestedElements()) {
if (element.isIndex()) { if (element.isIndex()) {
if (binding.isMap()) { if (binding.isMap()) {
@ -158,7 +159,7 @@ public class GenericBinder implements Binder {
} }
// subclassing hooks // subclassing hooks
/** /**
* Hook subclasses may use to filter the source values to bind. * Hook subclasses may use to filter the source values to bind.
* This hook allows the binder to pre-process the source values before binding occurs. * This hook allows the binder to pre-process the source values before binding occurs.
@ -190,7 +191,7 @@ public class GenericBinder implements Binder {
throw new MissingSourceValuesException(missingRequired, sourceValues); throw new MissingSourceValuesException(missingRequired, sourceValues);
} }
} }
private GenericBindingRule getBindingRule(String property) { private GenericBindingRule getBindingRule(String property) {
GenericBindingRule rule = bindingRules.get(property); GenericBindingRule rule = bindingRules.get(property);
if (rule == null) { if (rule == null) {
@ -199,7 +200,7 @@ public class GenericBinder implements Binder {
} }
return rule; return rule;
} }
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();
@ -218,41 +219,42 @@ public class GenericBinder implements Binder {
class GenericBindingRule implements BindingRuleConfiguration, BindingContext { class GenericBindingRule implements BindingRuleConfiguration, BindingContext {
private Class<?> modelClass; private Class<?> modelClass;
private PropertyDescriptor property; private PropertyDescriptor property;
private Formatter formatter; private Formatter formatter;
private Formatter elementFormatter; private Formatter elementFormatter;
private Formatter keyFormatter; private Formatter keyFormatter;
private Condition editableCondition = Condition.ALWAYS_TRUE; private Condition editableCondition = Condition.ALWAYS_TRUE;
private Condition enabledCondition = Condition.ALWAYS_TRUE; private Condition enabledCondition = Condition.ALWAYS_TRUE;
private Condition visibleCondition = Condition.ALWAYS_TRUE; private Condition visibleCondition = Condition.ALWAYS_TRUE;
private Map<String, GenericBindingRule> nestedBindingRules; private Map<String, GenericBindingRule> nestedBindingRules;
private Binding binding; private Binding binding;
private Map<Integer, Binding> listElementBindings;
public GenericBindingRule(String property, Class modelClass) { public GenericBindingRule(String property, Class modelClass) {
this.modelClass = modelClass; this.modelClass = modelClass;
this.property = findPropertyDescriptor(property); this.property = findPropertyDescriptor(property);
nestedBindingRules = new HashMap<String, GenericBindingRule>();
} }
// implementing BindingContext // implementing BindingContext
public MessageSource getMessageSource() { public MessageSource getMessageSource() {
return messageSource; return messageSource;
} }
public TypeConverter getTypeConverter() { public TypeConverter getTypeConverter() {
return typeConverter; return typeConverter;
} }
public Formatter<?> getFormatter() { public Formatter<?> getFormatter() {
if (formatter != null) { if (formatter != null) {
return formatter; return formatter;
@ -288,9 +290,82 @@ public class GenericBinder implements Binder {
public Condition getVisibleCondition() { public Condition getVisibleCondition() {
return visibleCondition; return visibleCondition;
} }
public Binding getBinding(String property, Object model) { public Binding getBinding(String property) {
return getBindingRule(property, model.getClass()).getBinding(model); createValueIfNecessary();
return getBindingRule(property, binding.getValueType()).getBinding(binding.getValue());
}
public Binding getListElementBinding(final int index) {
if (listElementBindings == null) {
listElementBindings = new HashMap<Integer, Binding>();
}
growListIfNecessary(index);
Binding binding = listElementBindings.get(index);
if (binding == null) {
BindingContext listContext = new BindingContext() {
public MessageSource getMessageSource() {
return GenericBindingRule.this.getMessageSource();
}
public TypeConverter getTypeConverter() {
return GenericBindingRule.this.getTypeConverter();
}
public Binding getBinding(String property) {
Object model = ((List) GenericBindingRule.this.binding.getValue()).get(index);
return GenericBindingRule.this.getBindingRule(property, getElementType()).getBinding(model);
}
public Formatter getFormatter() {
return GenericBindingRule.this.getElementFormatter();
}
public Formatter getElementFormatter() {
return null;
}
public Formatter getKeyFormatter() {
return null;
}
public Condition getEditableCondition() {
return GenericBindingRule.this.getEditableCondition();
}
public Condition getEnabledCondition() {
return GenericBindingRule.this.getEnabledCondition();
}
public Condition getVisibleCondition() {
return GenericBindingRule.this.getVisibleCondition();
}
public Binding getListElementBinding(int index) {
throw new IllegalArgumentException("Not yet supported");
}
public Binding getMapValueBinding(Object key) {
throw new IllegalArgumentException("Not yet supported");
}
public String getLabel() {
return GenericBindingRule.this.getLabel() + "[" + index + "]";
}
};
binding = new ListElementBinding(index, getElementType(), (List) this.binding.getValue(), listContext);
listElementBindings.put(index, binding);
}
return binding;
}
public Binding getMapValueBinding(Object key) {
return null;
}
public String getLabel() {
return property.getName();
} }
// implementing BindingRuleConfiguration // implementing BindingRuleConfiguration
@ -302,7 +377,8 @@ public class GenericBinder implements Binder {
public BindingRuleConfiguration formatElementsWith(Formatter<?> formatter) { public BindingRuleConfiguration formatElementsWith(Formatter<?> formatter) {
if (!List.class.isAssignableFrom(modelClass) || modelClass.isArray()) { if (!List.class.isAssignableFrom(modelClass) || modelClass.isArray()) {
throw new IllegalStateException("Bound property is not a List or an array; cannot set a element formatter"); throw new IllegalStateException(
"Bound property is not a List or an array; cannot set a element formatter");
} }
elementFormatter = formatter; elementFormatter = formatter;
return this; return this;
@ -330,7 +406,7 @@ public class GenericBinder implements Binder {
visibleCondition = condition; visibleCondition = condition;
return this; return this;
} }
// internal helpers // internal helpers
private Class<?> getElementType() { private Class<?> getElementType() {
@ -344,8 +420,11 @@ public class GenericBinder implements Binder {
GenericBindingRule getBindingRule(String property) { GenericBindingRule getBindingRule(String property) {
return getBindingRule(property, this.property.getPropertyType()); return getBindingRule(property, this.property.getPropertyType());
} }
GenericBindingRule getBindingRule(String property, Class modelClass) { GenericBindingRule getBindingRule(String property, Class<?> modelClass) {
if (nestedBindingRules == null) {
nestedBindingRules = new HashMap<String, GenericBindingRule>();
}
GenericBindingRule rule = nestedBindingRules.get(property); GenericBindingRule rule = nestedBindingRules.get(property);
if (rule == null) { if (rule == null) {
rule = new GenericBindingRule(property, modelClass); rule = new GenericBindingRule(property, modelClass);
@ -353,12 +432,14 @@ public class GenericBinder implements Binder {
} }
return rule; return rule;
} }
// internal helpers
Binding getBinding(Object model) { Binding getBinding(Object model) {
if (binding == null) { if (binding == null) {
binding = new PropertyBinding(property, model, this); binding = new PropertyBinding(property, model, this);
} }
return binding; return binding;
} }
private PropertyDescriptor findPropertyDescriptor(String property) { private PropertyDescriptor findPropertyDescriptor(String property) {
@ -378,12 +459,55 @@ public class GenericBinder implements Binder {
throw new IllegalStateException("Unable to introspect model type " + clazz); throw new IllegalStateException("Unable to introspect model type " + clazz);
} }
} }
private void createValueIfNecessary() {
Object value = binding.getValue();
if (value == null) {
value = newValue(binding.getValueType());
binding.applySourceValue(value);
binding.commit();
}
}
private void growListIfNecessary(int index) {
List list = (List) binding.getValue();
if (list == null) {
list = newListValue(binding.getValueType());
binding.applySourceValue(list);
binding.commit();
list = (List) binding.getValue();
}
if (index >= list.size()) {
for (int i = list.size(); i <= index; i++) {
list.add(newValue(getElementType()));
}
}
}
private List newListValue(Class<?> type) {
if (type.isInterface()) {
return (List) newValue(ArrayList.class);
} else {
return (List) newValue(type);
}
}
private Object newValue(Class<?> type) {
try {
return type.newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("Could not instantiate element of type [" + type.getName() + "]", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Could not instantiate element of type [" + type.getName() + "]", e);
}
}
} }
public interface BindingContext { public interface BindingContext {
MessageSource getMessageSource(); MessageSource getMessageSource();
TypeConverter getTypeConverter(); TypeConverter getTypeConverter();
Condition getEditableCondition(); Condition getEditableCondition();
@ -392,17 +516,20 @@ public class GenericBinder implements Binder {
Condition getVisibleCondition(); Condition getVisibleCondition();
Binding getBinding(String property, Object model); Binding getBinding(String property);
@SuppressWarnings("unchecked")
Formatter getFormatter(); Formatter getFormatter();
@SuppressWarnings("unchecked")
Formatter getElementFormatter(); Formatter getElementFormatter();
@SuppressWarnings("unchecked")
Formatter getKeyFormatter(); Formatter getKeyFormatter();
Binding getListElementBinding(int index);
Binding getMapValueBinding(Object key);
String getLabel();
} }
public void setRequired(String[] propertyPaths) { public void setRequired(String[] propertyPaths) {

View File

@ -0,0 +1,51 @@
package org.springframework.ui.binding.support;
import java.util.List;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.ui.binding.support.GenericBinder.BindingContext;
public class ListElementBinding extends AbstractBinding {
private List list;
private int index;
private Class<?> elementType;
private BindingContext bindingContext;
public ListElementBinding(int index, Class<?> elementType, List list, BindingContext bindingContext) {
super(bindingContext);
this.index = index;
this.elementType = elementType;
this.list = list;
this.bindingContext = bindingContext;
}
@Override
protected ValueModel getValueModel() {
return new ValueModel() {
public Object getValue() {
return list.get(index);
}
public Class<?> getValueType() {
if (elementType != null) {
return elementType;
} else {
return getValue().getClass();
}
}
public TypeDescriptor<?> getValueTypeDescriptor() {
return TypeDescriptor.valueOf(getValueType());
}
public void setValue(Object value) {
list.set(index, value);
}
};
}
}

View File

@ -1,263 +1,36 @@
package org.springframework.ui.binding.support; package org.springframework.ui.binding.support;
import java.beans.PropertyDescriptor; import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
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.MethodParameter;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeConverter;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.style.StylerUtils;
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.binding.support.GenericBinder.BindingContext; import org.springframework.ui.binding.support.GenericBinder.BindingContext;
import org.springframework.ui.format.Formatter;
import org.springframework.ui.message.MessageBuilder;
import org.springframework.ui.message.ResolvableArgument;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@SuppressWarnings("unchecked") public class PropertyBinding extends AbstractBinding implements Binding {
public class PropertyBinding implements Binding {
private PropertyDescriptor property; private PropertyDescriptor property;
private Object object; private Object object;
private BindingContext bindingContext;
private Object sourceValue;
private Exception invalidSourceValueCause;
private ValueBuffer buffer;
private BindingStatus status;
private Map<Integer, ListElementBinding> listElementBindings;
private Class<?> elementType;
public PropertyBinding(PropertyDescriptor property, Object object, BindingContext bindingContext) { public PropertyBinding(PropertyDescriptor property, Object object, BindingContext bindingContext) {
super(bindingContext);
this.property = property; this.property = property;
this.object = object; this.object = object;
this.bindingContext = bindingContext;
buffer = new ValueBuffer(getModel());
status = BindingStatus.CLEAN;
if (isList()) {
listElementBindings = new HashMap<Integer, ListElementBinding>();
elementType = GenericCollectionTypeResolver.getCollectionReturnType(property.getReadMethod());
}
}
public String getRenderValue() {
return format(getValue(), getFormatter());
}
public Object getValue() {
if (status == BindingStatus.DIRTY || status == BindingStatus.COMMIT_FAILURE) {
return buffer.getValue();
} else {
return getModel().getValue();
}
}
public Class<?> getValueType() {
return getModel().getValueType();
} }
// Binding overrides
@Override
public boolean isEditable() { public boolean isEditable() {
return isWriteableProperty() && bindingContext.getEditableCondition().isTrue(); return isWriteable() && super.isEditable();
} }
public boolean isEnabled() { // implementing
return bindingContext.getEnabledCondition().isTrue();
} protected ValueModel getValueModel() {
return new ValueModel() {
public boolean isVisible() {
return bindingContext.getVisibleCondition().isTrue();
}
public void applySourceValue(Object sourceValue) {
assertEditable();
assertEnabled();
if (sourceValue instanceof String) {
try {
Object parsed = getFormatter().parse((String) sourceValue, getLocale());
buffer.setValue(coerseToValueType(parsed));
sourceValue = null;
status = BindingStatus.DIRTY;
} catch (ParseException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
}
} else if (sourceValue instanceof String[]) {
String[] sourceValues = (String[]) sourceValue;
Class<?> parsedType = getFormattedObjectType(bindingContext.getElementFormatter().getClass());
if (parsedType == null) {
parsedType = String.class;
}
Object parsed = Array.newInstance(parsedType, sourceValues.length);
for (int i = 0; i < sourceValues.length; i++) {
Object parsedValue;
try {
parsedValue = bindingContext.getElementFormatter().parse(sourceValues[i], getLocale());
Array.set(parsed, i, parsedValue);
} catch (ParseException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
break;
}
}
if (status != BindingStatus.INVALID_SOURCE_VALUE) {
try {
buffer.setValue(coerseToValueType(parsed));
sourceValue = null;
status = BindingStatus.DIRTY;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
}
}
} else {
try {
buffer.setValue(coerseToValueType(sourceValue));
sourceValue = null;
status = BindingStatus.DIRTY;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
status = BindingStatus.INVALID_SOURCE_VALUE;
}
}
}
public Object getInvalidSourceValue() {
if (status != BindingStatus.INVALID_SOURCE_VALUE) {
throw new IllegalStateException("No invalid source value");
}
return sourceValue;
}
public BindingStatus getStatus() {
return status;
}
public Alert getStatusAlert() {
if (status == BindingStatus.INVALID_SOURCE_VALUE) {
return new AbstractAlert() {
public String getCode() {
return "typeMismatch";
}
public String getMessage() {
MessageBuilder builder = new MessageBuilder(bindingContext.getMessageSource());
builder.code(getCode());
if (invalidSourceValueCause instanceof ParseException) {
ParseException e = (ParseException) invalidSourceValueCause;
builder.arg("label", new ResolvableArgument(property.getName()));
builder.arg("value", sourceValue);
builder.arg("errorOffset", e.getErrorOffset());
builder.defaultMessage("Failed to bind to property '" + property + "'; the user value "
+ StylerUtils.style(sourceValue) + " has an invalid format and could no be parsed");
} else {
ConversionFailedException e = (ConversionFailedException) invalidSourceValueCause;
builder.arg("label", new ResolvableArgument(property.getName()));
builder.arg("value", sourceValue);
builder.defaultMessage("Failed to bind to property '" + property + "'; the user value "
+ StylerUtils.style(sourceValue) + " has could not be converted to "
+ e.getTargetType().getName());
}
return builder.build();
}
public Severity getSeverity() {
return Severity.ERROR;
}
};
} else if (status == BindingStatus.COMMIT_FAILURE) {
return new AbstractAlert() {
public String getCode() {
return "internalError";
}
public String getMessage() {
return "Internal error occurred; message = [" + buffer.getFlushException().getMessage() + "]";
}
public Severity getSeverity() {
return Severity.FATAL;
}
};
} else if (status == BindingStatus.COMMITTED) {
return new AbstractAlert() {
public String getCode() {
return "bindSucces";
}
public String getMessage() {
return "Binding successful";
}
public Severity getSeverity() {
return Severity.INFO;
}
};
} else {
return null;
}
}
public void commit() {
assertEditable();
assertEnabled();
if (status == BindingStatus.DIRTY) {
buffer.flush();
if (buffer.flushFailed()) {
status = BindingStatus.COMMIT_FAILURE;
} else {
status = BindingStatus.COMMITTED;
}
} else {
throw new IllegalStateException("Binding is not dirty; nothing to commit");
}
}
public void revert() {
if (status == BindingStatus.INVALID_SOURCE_VALUE) {
sourceValue = null;
invalidSourceValueCause = 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");
}
}
public Model getModel() {
return new Model() {
public Object getValue() { public Object getValue() {
return ReflectionUtils.invokeMethod(property.getReadMethod(), object); return ReflectionUtils.invokeMethod(property.getReadMethod(), object);
} }
@ -266,6 +39,7 @@ public class PropertyBinding implements Binding {
return property.getPropertyType(); return property.getPropertyType();
} }
@SuppressWarnings("unchecked")
public TypeDescriptor<?> getValueTypeDescriptor() { public TypeDescriptor<?> getValueTypeDescriptor() {
return new TypeDescriptor(new MethodParameter(property.getReadMethod(), -1)); return new TypeDescriptor(new MethodParameter(property.getReadMethod(), -1));
} }
@ -275,232 +49,10 @@ public class PropertyBinding implements Binding {
} }
}; };
} }
public Binding getBinding(String property) {
assertScalarProperty();
if (getValue() == null) {
getModel().setValue(newValue(getValueType()));
}
return bindingContext.getBinding(property, getValue());
}
public boolean isList() {
return List.class.isAssignableFrom(getValueType());
}
public Binding getListElementBinding(int index) {
assertListProperty();
if (index < 0) {
throw new IllegalArgumentException("Invalid index " + index);
}
growListIfNecessary(index);
ListElementBinding binding = listElementBindings.get(index);
if (binding == null) {
binding = new ListElementBinding(index);
listElementBindings.put(index, binding);
}
return binding;
}
public boolean isMap() {
return Map.class.isAssignableFrom(getValueType());
}
public Binding getMapValueBinding(Object key) {
assertMapProperty();
if (key instanceof String) {
try {
key = bindingContext.getKeyFormatter().parse((String) key, getLocale());
} catch (ParseException e) {
throw new IllegalArgumentException("Invald key", e);
}
}
//TODO return new KeyedPropertyBinding(key, (Map) getValue(), getMapTypeDescriptor());
return null;
}
public String formatValue(Object value) {
Formatter formatter;
if (isList() || isMap()) {
formatter = bindingContext.getElementFormatter();
} else {
formatter = bindingContext.getFormatter();
}
return format(value, formatter);
}
// subclassing hooks
protected Formatter getFormatter() {
return bindingContext.getFormatter();
}
// internal helpers // internal helpers
private String format(Object value, Formatter formatter) { private boolean isWriteable() {
Class<?> formattedType = getFormattedObjectType(formatter.getClass());
value = bindingContext.getTypeConverter().convert(value, formattedType);
return formatter.format(value, getLocale());
}
private Locale getLocale() {
return LocaleContextHolder.getLocale();
}
private Class getFormattedObjectType(Class formatterClass) {
Class classToIntrospect = formatterClass;
while (classToIntrospect != null) {
Type[] ifcs = classToIntrospect.getGenericInterfaces();
for (Type ifc : ifcs) {
if (ifc instanceof ParameterizedType) {
ParameterizedType paramIfc = (ParameterizedType) ifc;
Type rawType = paramIfc.getRawType();
if (Formatter.class.equals(rawType)) {
Type arg = paramIfc.getActualTypeArguments()[0];
if (arg instanceof TypeVariable) {
arg = GenericTypeResolver.resolveTypeVariable((TypeVariable) arg, formatterClass);
}
if (arg instanceof Class) {
return (Class) arg;
}
} else if (Formatter.class.isAssignableFrom((Class) rawType)) {
return getFormattedObjectType((Class) rawType);
}
} else if (Formatter.class.isAssignableFrom((Class) ifc)) {
return getFormattedObjectType((Class) ifc);
}
}
classToIntrospect = classToIntrospect.getSuperclass();
}
return null;
}
private Object coerseToValueType(Object parsed) {
TypeDescriptor targetType = getModel().getValueTypeDescriptor();
TypeConverter converter = bindingContext.getTypeConverter();
if (parsed != null && converter.canConvert(parsed.getClass(), targetType)) {
return converter.convert(parsed, targetType);
} else {
return parsed;
}
}
private void assertScalarProperty() {
if (isList()) {
throw new IllegalArgumentException("Is a Collection but should be a scalar");
}
if (isMap()) {
throw new IllegalArgumentException("Is a Map but should be a scalar");
}
}
private void assertListProperty() {
if (!isList()) {
throw new IllegalStateException("Not a List property binding");
}
}
private void assertMapProperty() {
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 isWriteableProperty() {
return property.getWriteMethod() != null; return property.getWriteMethod() != null;
} }
private void growListIfNecessary(int index) {
List list = (List) getValue();
if (list == null) {
getModel().setValue(newListValue(getValueType()));
list = (List) getValue();
}
if (index >= list.size()) {
for (int i = list.size(); i <= index; i++) {
list.add(newValue(elementType));
}
}
}
private Object newListValue(Class<?> type) {
if (type.isInterface()) {
return newValue(ArrayList.class);
} else {
return newValue(type);
}
}
private Object newValue(Class<?> type) {
try {
return type.newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("Could not instantiate element of type [" + type.getName() + "]", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Could not instantiate element of type [" + type.getName() + "]", e);
}
}
static abstract class AbstractAlert implements Alert {
public String toString() {
return getCode() + " - " + getMessage();
}
}
class ListElementBinding extends PropertyBinding {
private int index;
public ListElementBinding(int index) {
super(property, object, bindingContext);
this.index = index;
}
protected Formatter getFormatter() {
return bindingContext.getElementFormatter();
}
public Model getModel() {
return new Model() {
public Object getValue() {
return getList().get(index);
}
public Class<?> getValueType() {
if (elementType != null) {
return elementType;
} else {
return getValue().getClass();
}
}
public TypeDescriptor<?> getValueTypeDescriptor() {
return TypeDescriptor.valueOf(getValueType());
}
public void setValue(Object value) {
getList().set(index, value);
}
};
}
// internal helpers
private List getList() {
return (List) PropertyBinding.this.getValue();
}
}
} }

View File

@ -46,5 +46,9 @@ public class PropertyPath implements Iterable<PropertyPathElement> {
public Iterator<PropertyPathElement> iterator() { public Iterator<PropertyPathElement> iterator() {
return elements.iterator(); return elements.iterator();
} }
public String toString() {
return elements.toString();
}
} }

View File

@ -25,4 +25,8 @@ public class PropertyPathElement {
public int getIntValue() { public int getIntValue() {
return Integer.parseInt(value); return Integer.parseInt(value);
} }
public String toString() {
return value + ";index=" + index;
}
} }

View File

@ -3,7 +3,7 @@
*/ */
package org.springframework.ui.binding.support; package org.springframework.ui.binding.support;
import org.springframework.ui.binding.Binding.Model; import org.springframework.ui.binding.support.AbstractBinding.ValueModel;
class ValueBuffer { class ValueBuffer {
@ -11,13 +11,13 @@ class ValueBuffer {
private boolean hasValue; private boolean hasValue;
private Model model; private ValueModel model;
private boolean flushFailed; private boolean flushFailed;
private Exception flushException; private Exception flushException;
public ValueBuffer(Model model) { public ValueBuffer(ValueModel model) {
this.model = model; this.model = model;
} }

View File

@ -83,7 +83,7 @@ public class WebBinder extends GenericBinder {
} }
protected Object getEmptyValue(PropertyBinding binding) { protected Object getEmptyValue(PropertyBinding binding) {
Class<?> type = binding.getModel().getValueType(); Class<?> type = binding.getValueModel().getValueType();
if (boolean.class.equals(type) || Boolean.class.equals(type)) { if (boolean.class.equals(type) || Boolean.class.equals(type)) {
return Boolean.FALSE; return Boolean.FALSE;
} else { } else {

View File

@ -63,7 +63,6 @@ public class GenericBinderTests {
values.put("integer", "3"); values.put("integer", "3");
values.put("foo", "BAR"); values.put("foo", "BAR");
BindingResults results = binder.bind(values); BindingResults results = binder.bind(values);
System.out.println(results);
assertEquals(3, results.size()); assertEquals(3, results.size());
assertEquals("string", results.get(0).getProperty()); assertEquals("string", results.get(0).getProperty());
@ -251,7 +250,7 @@ public class GenericBinderTests {
values.put("addresses[0]", "4655 Macy Lane:Melbourne:FL:35452"); values.put("addresses[0]", "4655 Macy Lane:Melbourne:FL:35452");
values.put("addresses[1]", "1234 Rostock Circle:Palm Bay:FL:32901"); values.put("addresses[1]", "1234 Rostock Circle:Palm Bay:FL:32901");
values.put("addresses[5]", "1977 Bel Aire Estates:Coker:AL:12345"); values.put("addresses[5]", "1977 Bel Aire Estates:Coker:AL:12345");
binder.bind(values); BindingResults results = binder.bind(values);
Assert.assertEquals(6, bean.addresses.size()); Assert.assertEquals(6, bean.addresses.size());
assertEquals("4655 Macy Lane", bean.addresses.get(0).street); assertEquals("4655 Macy Lane", bean.addresses.get(0).street);
assertEquals("Melbourne", bean.addresses.get(0).city); assertEquals("Melbourne", bean.addresses.get(0).city);