Refactored class naming to capture ui.binding system as a general PresentationModel framework; PresentationModel and FieldModel are the key API elements now

This commit is contained in:
Keith Donald 2009-07-24 15:50:37 +00:00
parent c7b019cd5c
commit 5cb5169237
33 changed files with 635 additions and 590 deletions

View File

@ -16,9 +16,10 @@
package org.springframework.ui.binding;
/**
* Binding states.
* FieldModel binding states.
* @author Keith Donald
* @since 3.0
* @see FieldModel#getBindingStatus()
*/
public enum BindingStatus {
@ -28,9 +29,9 @@ public enum BindingStatus {
CLEAN,
/**
* An invalid source value is applied.
* An invalid submitted value is applied.
*/
INVALID_SOURCE_VALUE,
INVALID_SUBMITTED_VALUE,
/**
* The binding buffer contains a valid value that has not been committed.

View File

@ -19,11 +19,11 @@ import org.springframework.ui.alert.Alert;
import org.springframework.ui.alert.Severity;
/**
* A binding between one or more UI components and a model property.
* A model for a single data field containing dynamic information to display in the view.
* @author Keith Donald
* @since 3.0
*/
public interface Binding {
public interface FieldModel {
/**
* The model value formatted for display in a single field in the UI.
@ -33,47 +33,47 @@ public interface Binding {
String getRenderValue();
/**
* The bound model value.
* The field model value.
*/
Object getValue();
/**
* The bound model value type.
* The field model value type.
*/
Class<?> getValueType();
/**
* If this Binding is editable.
* If editable.
* Used to determine if the user can edit the field value.
* A Binding that is not editable cannot have source values applied and cannot be committed.
* A Binding that is not editable cannot have submitted values applied and cannot be committed.
*/
boolean isEditable();
/**
* If this Binding is enabled.
* If enabled.
* Used to determine if the user can interact with the field at all.
* A Binding that is not enabled cannot have source values applied and cannot be committed.
* A Binding that is not enabled cannot have submitted values applied and cannot be committed.
*/
boolean isEnabled();
/**
* If this Binding is visible.
* If visible.
* Used to determine if the user can see the field.
*/
boolean isVisible();
/**
* The current binding status.
* The current field binding status.
* Initially {@link BindingStatus#CLEAN clean}.
* Is {@link BindingStatus#DIRTY} after applying a source value to the value buffer.
* Is {@link BindingStatus#DIRTY} after applying a submitted 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#INVALID_SUBMITTED_VALUE} if a submitted value could not be applied.
* Is {@link BindingStatus#COMMIT_FAILURE} if a buffered value could not be committed.
*/
BindingStatus getBindingStatus();
/**
* The current validation status.
* The current field validation status.
* Initially {@link ValidationStatus#NOT_VALIDATED}.
* Is {@link ValidationStatus#VALID} after value is successfully validated.
* Is {@link ValidationStatus#INVALID} after value fails validation.
@ -82,31 +82,31 @@ public interface Binding {
ValidationStatus getValidationStatus();
/**
* An alert that communicates current status to the user.
* An alert that communicates current FieldModel status to the user.
* Returns <code>null</code> if {@link BindingStatus#CLEAN} and {@link ValidationStatus#NOT_VALIDATED}.
* 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> when {@link BindingStatus#INVALID_SOURCE_VALUE} or {@link BindingStatus#COMMIT_FAILURE} due to a value parse / type conversion error.
* Returns a {@link Severity#ERROR} Alert with code <code>typeMismatch</code> when {@link BindingStatus#INVALID_SUBMITTED_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> 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();
/**
* Apply the source value to this binding.
* The source value is parsed and stored in the binding's value buffer.
* Apply a submitted value to this FieldModel.
* The submitted value is parsed and stored in the value buffer.
* Sets to {@link BindingStatus#DIRTY} if succeeds.
* Sets to {@link BindingStatus#INVALID_SOURCE_VALUE} if fails.
* @param sourceValue
* Sets to {@link BindingStatus#INVALID_SUBMITTED_VALUE} if fails.
* @param submittedValue
* @throws IllegalStateException if not editable or not enabled
*/
void applySourceValue(Object sourceValue);
void applySubmittedValue(Object submittedValue);
/**
* If {@link BindingStatus#INVALID_SOURCE_VALUE}, returns the invalid source value.
* If {@link BindingStatus#INVALID_SUBMITTED_VALUE}, returns the invalid submitted value.
* Returns null otherwise.
* @return the invalid source value
* @return the invalid submitted value
*/
Object getInvalidSourceValue();
Object getInvalidSubmittedValue();
/**
* Validate the model value.
@ -130,45 +130,46 @@ public interface Binding {
void revert();
/**
* 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"
* @return the binding to the nested property
* @throws IllegalStateException if not a bean
* Get a model for a nested field.
* @param fieldName the nested field name, such as "foo"; should not be a property path like "foo.bar"
* @return the nested field model
* @throws IllegalStateException if {@link #isList()}
* @throws FieldNotFoundException if no such nested field exists
*/
Binding getNestedBinding(String property);
FieldModel getNested(String fieldName);
/**
* If bound to an indexable collection, either a {@link java.util.List} or an array.
* If an indexable {@link java.util.List} or array.
*/
boolean isList();
/**
* If a list, get a Binding to a element in the list..
* If {@link #isList()}, get a FieldModel for a element in the list..
* @param index the element index
* @return the indexed binding
* @throws IllegalStateException if not a list
*/
Binding getListElementBinding(int index);
FieldModel getListElement(int index);
/**
* If bound to a Map.
* If a Map.
*/
boolean isMap();
/**
* If a Map, get a Binding to a value in the Map.
* If {@link #isMap()}, get FieldModel for a value in the Map.
* @param key the map key
* @return the keyed binding
* @throws IllegalStateException if not a map
*/
Binding getMapValueBinding(Object key);
FieldModel getMapValue(Object key);
/**
* Format a potential model value for display.
* 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 {@link #isList()}, expects the value to be a potential list element & uses the configured element formatter.
* If {@link #isMap()}, expects the value to be a potential map value & uses the configured map value formatter.
* @param potentialValue the potential value
* @return the formatted string
*/
String formatValue(Object potentialModelValue);
String formatValue(Object potentialValue);
}

View File

@ -16,22 +16,27 @@
package org.springframework.ui.binding;
/**
* A factory for model property bindings.
* Thrown when a PresentationModel field cannot be found.
* @author Keith Donald
* @since 3.0
* @see PresentationModel#getFieldModel(String)
* @see FieldModel#getNested(String)
*/
public interface BindingFactory {
@SuppressWarnings("serial")
public class FieldNotFoundException extends RuntimeException {
private String field;
/**
* The model object upon which bindings may be accessed.
* Creates a new FieldNotFoundException.
* @param fieldName the field not found exception
*/
Object getModel();
public FieldNotFoundException(String fieldName) {
super("No field '" + fieldName + "' found");
}
/**
* Get a binding to a model property.
* @param property the property path
* @throws NoSuchBindingException if no binding to the property exists
*/
Binding getBinding(String property);
}
public String getField() {
return field;
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2004-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ui.binding;
/**
* Represents the state and behavior of a presentation independently of the GUI controls used in the interface.
* Pulls the state and behavior of a view out into a model class that is part of the presentation.
* Coordinates with the domain layer and provides an interface to the view that minimizes decision making in the view.
* @author Keith Donald
* @since 3.0
*/
public interface PresentationModel {
/**
* Get the model for the field.
* @param fieldName the field name.
* @throws FieldNotFoundException if no such field exists
*/
FieldModel getFieldModel(String fieldName);
}

View File

@ -16,19 +16,19 @@
package org.springframework.ui.binding;
/**
* A locator for BindingFactories indexed by their models.
* Makes it easy for clients to lookup BindingFactories for models the need to bind to.
* A factory for domain object PresentationModels.
* Makes it easy for clients to lookup PresentationModels for domain objects they need to bind to.
* @author Keith Donald
* @since 3.0
*/
public interface BindingFactoryLocator {
public interface PresentationModelFactory {
/**
* Get the BindingFactory for the model object.
* If no such BindingFactory exists, one is created and cached.
* Get the PresentationModel for the domain object.
* If none exists, one is created and cached.
* Never returns <code>null</code>.
* @param model the model object
* @return the binding Factory
* @param domainObject the model object
* @return the presentation model
*/
public BindingFactory getBindingFactory(Object model);
public PresentationModel getPresentationModel(Object domainObject);
}

View File

@ -16,9 +16,10 @@
package org.springframework.ui.binding;
/**
* Validation states.
* FieldModel Validation states.
* @author Keith Donald
* @since 3.0
* @see FieldModel#getValidationStatus()
*/
public enum ValidationStatus {

View File

@ -70,7 +70,7 @@ class ArrayListBindingResults implements BindingResults {
public List<String> properties() {
List<String> properties = new ArrayList<String>(results.size());
for (BindingResult result : this) {
properties.add(result.getProperty());
properties.add(result.getFieldName());
}
return properties;
}

View File

@ -17,7 +17,7 @@ package org.springframework.ui.binding.binder;
import java.util.Map;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.FieldModel;
/**
* Binds user-entered values to properties of a model object.
@ -28,18 +28,13 @@ import org.springframework.ui.binding.Binding;
*/
public interface Binder {
/**
* The model this binder binds to.
*/
public Object getModel();
/**
* Bind the source values to the properties of the model.
* A result is returned for each registered {@link Binding}.
* @param sourceValues the source values to bind
* A result is returned for each registered {@link FieldModel}.
* @param fieldValues the field values to bind
* @return the results of the binding operation
* @throws MissingSourceValuesException when the sourceValues Map is missing entries for required bindings
* @throws MissingFieldException when the fieldValues Map is missing entries for required bindings
*/
BindingResults bind(Map<String, ? extends Object> sourceValues);
BindingResults bind(Map<String, ? extends Object> fieldValues);
}

View File

@ -15,7 +15,7 @@
*/
package org.springframework.ui.binding.binder;
import org.springframework.ui.binding.config.BindingRuleConfiguration;
import org.springframework.ui.binding.config.FieldModelConfiguration;
/**
* A SPI interface that lets you configure a model binder, then execute it.
@ -37,7 +37,7 @@ public interface BinderExecutor<M> {
* @param property the model property
* @return a builder API for configuring the rule
*/
BindingRuleConfiguration bindingRule(String property);
FieldModelConfiguration bindingRule(String property);
// TODO allow injection of pre-created BindingRules

View File

@ -26,17 +26,17 @@ import org.springframework.ui.alert.Alert;
public interface BindingResult {
/**
* The model property this binding result is for.
* @see Binder#getNestedBinding(String)
* The name of the field this binding result is for.
* @see Binder#getNested(String)
*/
String getProperty();
String getFieldName();
/**
* The raw source value for which binding was attempted.
* The raw submitted value for which binding was attempted.
* If not a failure, this value was successfully bound to the model.
* @see #isFailure()
*/
Object getSourceValue();
Object getSubmittedValue();
/**
* Indicates if the binding failed.

View File

@ -20,23 +20,23 @@ import org.springframework.ui.alert.Severity;
class BindingStatusResult implements BindingResult {
private String property;
private String fieldName;
private Object sourceValue;
private Alert bindingStatusAlert;
public BindingStatusResult(String property, Object sourceValue, Alert alert) {
this.property = property;
public BindingStatusResult(String fieldName, Object sourceValue, Alert alert) {
this.fieldName = fieldName;
this.sourceValue = sourceValue;
this.bindingStatusAlert = alert;
}
public String getProperty() {
return property;
public String getFieldName() {
return fieldName;
}
public Object getSourceValue() {
public Object getSubmittedValue() {
return sourceValue;
}

View File

@ -22,7 +22,7 @@ import org.springframework.ui.alert.Severity;
import org.springframework.ui.message.MessageBuilder;
import org.springframework.ui.message.ResolvableArgument;
class PropertyNotEditableResult implements BindingResult {
class FieldNotEditableResult implements BindingResult {
private String property;
@ -30,17 +30,17 @@ class PropertyNotEditableResult implements BindingResult {
private MessageSource messageSource;
public PropertyNotEditableResult(String property, Object sourceValue, MessageSource messageSource) {
public FieldNotEditableResult(String property, Object sourceValue, MessageSource messageSource) {
this.property = property;
this.sourceValue = sourceValue;
this.messageSource = messageSource;
}
public String getProperty() {
public String getFieldName() {
return property;
}
public Object getSourceValue() {
public Object getSubmittedValue() {
return sourceValue;
}

View File

@ -22,7 +22,7 @@ import org.springframework.ui.alert.Severity;
import org.springframework.ui.message.MessageBuilder;
import org.springframework.ui.message.ResolvableArgument;
class PropertyNotFoundResult implements BindingResult {
class FieldNotFoundResult implements BindingResult {
private String property;
@ -30,17 +30,17 @@ class PropertyNotFoundResult implements BindingResult {
private MessageSource messageSource;
public PropertyNotFoundResult(String property, Object sourceValue, MessageSource messageSource) {
public FieldNotFoundResult(String property, Object sourceValue, MessageSource messageSource) {
this.property = property;
this.sourceValue = sourceValue;
this.messageSource = messageSource;
}
public String getProperty() {
public String getFieldName() {
return property;
}
public Object getSourceValue() {
public Object getSubmittedValue() {
return sourceValue;
}

View File

@ -21,10 +21,10 @@ import java.util.Map;
import org.springframework.context.MessageSource;
import org.springframework.core.convert.TypeConverter;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.BindingFactory;
import org.springframework.ui.binding.FieldModel;
import org.springframework.ui.binding.FieldNotFoundException;
import org.springframework.ui.binding.PresentationModel;
import org.springframework.ui.binding.BindingStatus;
import org.springframework.ui.binding.support.PropertyNotFoundException;
import org.springframework.util.Assert;
/**
@ -37,15 +37,15 @@ import org.springframework.util.Assert;
*/
public class GenericBinder implements Binder {
private BindingFactory bindingFactory;
private PresentationModel presentationModel;
private String[] requiredProperties;
private String[] requiredFields;
private MessageSource messageSource;
public GenericBinder(BindingFactory bindingFactory) {
Assert.notNull(bindingFactory, "The BindingFactory is required");
this.bindingFactory = bindingFactory;
public GenericBinder(PresentationModel presentationModel) {
Assert.notNull(presentationModel, "The PresentationModel is required");
this.presentationModel = presentationModel;
}
/**
@ -58,34 +58,38 @@ public class GenericBinder implements Binder {
}
/**
* Configure the properties for which source values must be present in each bind attempt.
* @param propertyPaths the property path expressions
* @see MissingSourceValuesException
* Configure the fields for which values must be present in each bind attempt.
* @param fieldNames the field names
* @see MissingFieldException
*/
public void setRequired(String[] propertyPaths) {
this.requiredProperties = propertyPaths;
public void setRequiredFields(String[] fieldNames) {
this.requiredFields = fieldNames;
}
public Object getModel() {
return bindingFactory.getModel();
}
// subclassing hooks
protected Binding getBinding(String property) {
return bindingFactory.getBinding(property);
/**
* Get the model for the field.
* @param fieldName
* @return the field model
* @throws NoSuchFieldException if no such field exists
*/
protected FieldModel getFieldModel(String fieldName) {
return presentationModel.getFieldModel(fieldName);
}
// implementing Binder
public BindingResults bind(Map<String, ? extends Object> sourceValues) {
sourceValues = filter(sourceValues);
checkRequired(sourceValues);
ArrayListBindingResults results = new ArrayListBindingResults(sourceValues.size());
for (Map.Entry<String, ? extends Object> sourceValue : sourceValues.entrySet()) {
public BindingResults bind(Map<String, ? extends Object> fieldValues) {
fieldValues = filter(fieldValues);
checkRequired(fieldValues);
ArrayListBindingResults results = new ArrayListBindingResults(fieldValues.size());
for (Map.Entry<String, ? extends Object> fieldValue : fieldValues.entrySet()) {
try {
Binding binding = getBinding(sourceValue.getKey());
results.add(bind(sourceValue, binding));
} catch (PropertyNotFoundException e) {
results.add(new PropertyNotFoundResult(sourceValue.getKey(), sourceValue.getValue(), messageSource));
FieldModel field = getFieldModel(fieldValue.getKey());
results.add(bind(fieldValue, field));
} catch (FieldNotFoundException e) {
results.add(new FieldNotFoundResult(fieldValue.getKey(), fieldValue.getValue(), messageSource));
}
}
return results;
@ -95,26 +99,26 @@ public class GenericBinder implements Binder {
/**
* 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 field values before binding occurs.
* For example, a Binder might insert empty or default values for fields that are not present.
* As another example, a Binder might collapse multiple source values into a single source value.
* @param sourceValues the original source values map provided by the caller
* @return the filtered source values map that will be used to bind
* @param fieldValues the original fieldValues map provided by the caller
* @return the filtered fieldValues map that will be used to bind
*/
protected Map<String, ? extends Object> filter(Map<String, ? extends Object> sourceValues) {
return sourceValues;
protected Map<String, ? extends Object> filter(Map<String, ? extends Object> fieldValues) {
return fieldValues;
}
// internal helpers
private void checkRequired(Map<String, ? extends Object> sourceValues) {
if (requiredProperties == null) {
private void checkRequired(Map<String, ? extends Object> fieldValues) {
if (requiredFields == null) {
return;
}
List<String> missingRequired = new ArrayList<String>();
for (String required : requiredProperties) {
for (String required : requiredFields) {
boolean found = false;
for (String property : sourceValues.keySet()) {
for (String property : fieldValues.keySet()) {
if (property.equals(required)) {
found = true;
}
@ -124,21 +128,21 @@ public class GenericBinder implements Binder {
}
}
if (!missingRequired.isEmpty()) {
throw new MissingSourceValuesException(missingRequired, sourceValues);
throw new MissingFieldException(missingRequired, fieldValues);
}
}
private BindingResult bind(Map.Entry<String, ? extends Object> sourceValue, Binding binding) {
String property = sourceValue.getKey();
Object value = sourceValue.getValue();
if (!binding.isEditable()) {
return new PropertyNotEditableResult(property, value, messageSource);
private BindingResult bind(Map.Entry<String, ? extends Object> fieldValue, FieldModel field) {
String fieldName = fieldValue.getKey();
Object value = fieldValue.getValue();
if (!field.isEditable()) {
return new FieldNotEditableResult(fieldName, value, messageSource);
} else {
binding.applySourceValue(value);
if (binding.getBindingStatus() == BindingStatus.DIRTY) {
binding.commit();
field.applySubmittedValue(value);
if (field.getBindingStatus() == BindingStatus.DIRTY) {
field.commit();
}
return new BindingStatusResult(property, value, binding.getStatusAlert());
return new BindingStatusResult(fieldName, value, field.getStatusAlert());
}
}

View File

@ -26,7 +26,7 @@ import java.util.Map;
* @see Binder#bind(java.util.Map)
*/
@SuppressWarnings("serial")
public class MissingSourceValuesException extends RuntimeException {
public class MissingFieldException extends RuntimeException {
private List<String> missing;
@ -35,7 +35,7 @@ public class MissingSourceValuesException extends RuntimeException {
* @param missing
* @param sourceValues
*/
public MissingSourceValuesException(List<String> missing, Map<String, ? extends Object> sourceValues) {
public MissingFieldException(List<String> missing, Map<String, ? extends Object> sourceValues) {
super(getMessage(missing, sourceValues));
this.missing = missing;
}
@ -49,9 +49,9 @@ public class MissingSourceValuesException extends RuntimeException {
private static String getMessage(List<String> missingRequired, Map<String, ? extends Object> sourceValues) {
if (missingRequired.size() == 1) {
return "Missing a source value for required propertyPath [" + missingRequired.get(0) + "]; sourceValues map contained " + sourceValues.keySet();
return "Missing a field [" + missingRequired.get(0) + "]; fieldValues map contained " + sourceValues.keySet();
} else {
return "Missing source values to required propertyPaths " + missingRequired + "; sourceValues map contained " + sourceValues.keySet();
return "Missing fields " + missingRequired + "; fieldValues map contained " + sourceValues.keySet();
}
}

View File

@ -18,8 +18,8 @@ package org.springframework.ui.binding.binder;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.BindingFactory;
import org.springframework.ui.binding.FieldModel;
import org.springframework.ui.binding.PresentationModel;
/**
* A binder designed for use in HTTP (web) environments.
@ -39,13 +39,12 @@ public class WebBinder extends GenericBinder {
* Creates a new web binder for the model object.
* @param model the model object containing properties this binder will bind to
*/
public WebBinder(BindingFactory bindingFactory) {
public WebBinder(PresentationModel bindingFactory) {
super(bindingFactory);
}
/**
* Configure the prefix to detect the default user value for a property when no value was submitted.
* This is used to configure a <i>default</i> {@link UserValue} for binding when no value is submitted by the client.
* Configure the prefix used to detect the default value for a field when no value is submitted.
* Default is '!'.
*/
public void setDefaultPrefix(String defaultPrefix) {
@ -53,8 +52,8 @@ public class WebBinder extends GenericBinder {
}
/**
* Configure the prefix to detect the presence of a property on the web UI when no user value for the property was actually submitted.
* This is used to configure a <i>empty</i> {@link UserValue} for binding when no other {@link #setDefaultPrefix(String) default value} is specified by the client.
* Configure the prefix used to detect the presence of a field on the web UI when no value was actually submitted.
* This is used to configure a <i>empty</i> field when no other {@link #setDefaultPrefix(String) default value} is specified by the client.
* Default is '_'.
*/
public void setPresentPrefix(String presentPrefix) {
@ -62,20 +61,20 @@ public class WebBinder extends GenericBinder {
}
@Override
protected Map<String, ? extends Object> filter(Map<String, ? extends Object> sourceValues) {
protected Map<String, ? extends Object> filter(Map<String, ? extends Object> fieldValues) {
LinkedHashMap<String, Object> filteredValues = new LinkedHashMap<String, Object>();
for (Map.Entry<String, ? extends Object> entry : sourceValues.entrySet()) {
for (Map.Entry<String, ? extends Object> entry : fieldValues.entrySet()) {
String field = entry.getKey();
Object value = entry.getValue();
if (field.startsWith(defaultPrefix)) {
field = field.substring(defaultPrefix.length());
if (!sourceValues.containsKey(field)) {
if (!fieldValues.containsKey(field)) {
filteredValues.put(field, value);
}
} else if (field.startsWith(presentPrefix)) {
field = field.substring(presentPrefix.length());
if (!sourceValues.containsKey(field) && !sourceValues.containsKey(defaultPrefix + field)) {
value = getEmptyValue(getBinding(field));
if (!fieldValues.containsKey(field) && !fieldValues.containsKey(defaultPrefix + field)) {
value = getEmptyValue(getFieldModel(field));
filteredValues.put(field, value);
}
} else {
@ -85,7 +84,7 @@ public class WebBinder extends GenericBinder {
return filteredValues;
}
protected Object getEmptyValue(Binding binding) {
protected Object getEmptyValue(FieldModel binding) {
Class<?> type = binding.getValueType();
if (boolean.class.equals(type) || Boolean.class.equals(type)) {
return Boolean.FALSE;

View File

@ -21,35 +21,35 @@ import org.springframework.ui.format.Formatter;
* A fluent interface for configuring a newly added binding rule.
* @author Keith Donald
*/
public interface BindingRuleConfiguration {
public interface FieldModelConfiguration {
/**
* Set the Formatter to use to format bound property values.
*/
BindingRuleConfiguration formatWith(Formatter<?> formatter);
FieldModelConfiguration formatWith(Formatter<?> formatter);
/**
* If a map property, set the Formatter to use to format map keys.
*/
BindingRuleConfiguration formatKeysWith(Formatter<?> formatter);
FieldModelConfiguration formatKeysWith(Formatter<?> formatter);
/**
* If an list or map property, set the Formatter to use to format indexed elements.
*/
BindingRuleConfiguration formatElementsWith(Formatter<?> formatter);
FieldModelConfiguration formatElementsWith(Formatter<?> formatter);
/**
* Set when the binding is editable.
*/
BindingRuleConfiguration editableWhen(Condition condition);
FieldModelConfiguration editableWhen(Condition condition);
/**
* Set when the binding is enabled.
*/
BindingRuleConfiguration enabledWhen(Condition condition);
FieldModelConfiguration enabledWhen(Condition condition);
/**
* Set when the binding is visible.
*/
BindingRuleConfiguration visibleWhen(Condition condition);
FieldModelConfiguration visibleWhen(Condition condition);
}

View File

@ -15,29 +15,26 @@
*/
package org.springframework.ui.binding.support;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
public class CollectionTypeDescriptor {
private Class<?> collectionType;
private Class<?> type;
private Class<?> elementType;
/**
* Creates a new generic collection property type
* @param collectionType the collection type
* @param elementType the element type
*/
public CollectionTypeDescriptor(Class<?> collectionType, Class<?> elementType) {
this.collectionType = collectionType;
public CollectionTypeDescriptor(Class<?> type, Class<?> elementType) {
Assert.notNull(type, "The collection type is required");
this.type = type;
this.elementType = elementType;
}
/**
* The collection type.
*/
public Class<?> getCollectionType() {
return collectionType;
public Class<?> getType() {
return type;
}
/**
@ -52,11 +49,11 @@ public class CollectionTypeDescriptor {
return false;
}
CollectionTypeDescriptor type = (CollectionTypeDescriptor) o;
return collectionType.equals(type.collectionType)
return type.equals(type.type)
&& ObjectUtils.nullSafeEquals(elementType, type.elementType);
}
public int hashCode() {
return collectionType.hashCode() + elementType.hashCode();
return type.hashCode() + elementType.hashCode();
}
}

View File

@ -34,38 +34,38 @@ 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.BindingStatus;
import org.springframework.ui.binding.FieldModel;
import org.springframework.ui.binding.ValidationStatus;
import org.springframework.ui.format.Formatter;
import org.springframework.ui.message.MessageBuilder;
import org.springframework.ui.message.ResolvableArgument;
public class GenericBinding implements Binding {
public class DefaultFieldModel implements FieldModel {
private ValueModel valueModel;
private BindingContext bindingContext;
private FieldModelContext context;
private ValueBuffer buffer;
private BindingStatus bindingStatus;
private Object sourceValue;
private Object submittedValue;
private Exception invalidSourceValueCause;
private Exception invalidSubmittedValueCause;
public GenericBinding(ValueModel valueModel, BindingContext bindingContext) {
public DefaultFieldModel(ValueModel valueModel, FieldModelContext context) {
this.valueModel = valueModel;
this.bindingContext = bindingContext;
this.context = context;
buffer = new ValueBuffer(valueModel);
bindingStatus = BindingStatus.CLEAN;
}
// implementing Binding
// implementing FieldModel
public String getRenderValue() {
return format(getValue(), bindingContext.getFormatter());
return format(getValue(), context.getFormatter());
}
public Object getValue() {
@ -81,42 +81,42 @@ public class GenericBinding implements Binding {
}
public boolean isEditable() {
return valueModel.isWriteable() && bindingContext.getEditableCondition().isTrue();
return valueModel.isWriteable() && context.getEditableCondition().isTrue();
}
public boolean isEnabled() {
return bindingContext.getEnabledCondition().isTrue();
return context.getEnabledCondition().isTrue();
}
public boolean isVisible() {
return bindingContext.getVisibleCondition().isTrue();
return context.getVisibleCondition().isTrue();
}
@SuppressWarnings("unchecked")
public void applySourceValue(Object sourceValue) {
public void applySubmittedValue(Object submittedValue) {
assertEditable();
assertEnabled();
if (sourceValue instanceof String) {
if (submittedValue instanceof String) {
try {
Object parsed = bindingContext.getFormatter().parse((String) sourceValue, getLocale());
Object parsed = context.getFormatter().parse((String) submittedValue, getLocale());
buffer.setValue(coerseToValueType(parsed));
sourceValue = null;
submittedValue = null;
bindingStatus = BindingStatus.DIRTY;
} catch (ParseException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
this.submittedValue = submittedValue;
invalidSubmittedValueCause = e;
bindingStatus = BindingStatus.INVALID_SUBMITTED_VALUE;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
this.submittedValue = submittedValue;
invalidSubmittedValueCause = e;
bindingStatus = BindingStatus.INVALID_SUBMITTED_VALUE;
}
} else if (sourceValue instanceof String[]) {
} else if (submittedValue instanceof String[]) {
Object parsed;
if (isMap()) {
String[] sourceValues = (String[]) sourceValue;
Formatter keyFormatter = bindingContext.getKeyFormatter();
Formatter valueFormatter = bindingContext.getElementFormatter();
String[] sourceValues = (String[]) submittedValue;
Formatter keyFormatter = context.getKeyFormatter();
Formatter valueFormatter = context.getElementFormatter();
Map map = new LinkedHashMap(sourceValues.length);
for (int i = 0; i < sourceValues.length; i++) {
String entryString = sourceValues[i];
@ -126,59 +126,59 @@ public class GenericBinding implements Binding {
Object parsedMapValue = valueFormatter.parse(keyValue[1], getLocale());
map.put(parsedMapKey, parsedMapValue);
} catch (ParseException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
this.submittedValue = submittedValue;
invalidSubmittedValueCause = e;
bindingStatus = BindingStatus.INVALID_SUBMITTED_VALUE;
break;
}
}
parsed = map;
} else {
String[] sourceValues = (String[]) sourceValue;
String[] sourceValues = (String[]) submittedValue;
List list = new ArrayList(sourceValues.length);
for (int i = 0; i < sourceValues.length; i++) {
Object parsedValue;
try {
parsedValue = bindingContext.getElementFormatter().parse(sourceValues[i], getLocale());
parsedValue = context.getElementFormatter().parse(sourceValues[i], getLocale());
list.add(parsedValue);
} catch (ParseException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
this.submittedValue = submittedValue;
invalidSubmittedValueCause = e;
bindingStatus = BindingStatus.INVALID_SUBMITTED_VALUE;
break;
}
}
parsed = list;
}
if (bindingStatus != BindingStatus.INVALID_SOURCE_VALUE) {
if (bindingStatus != BindingStatus.INVALID_SUBMITTED_VALUE) {
try {
buffer.setValue(coerseToValueType(parsed));
sourceValue = null;
submittedValue = null;
bindingStatus = BindingStatus.DIRTY;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
this.submittedValue = submittedValue;
invalidSubmittedValueCause = e;
bindingStatus = BindingStatus.INVALID_SUBMITTED_VALUE;
}
}
} else {
try {
buffer.setValue(coerseToValueType(sourceValue));
sourceValue = null;
buffer.setValue(coerseToValueType(submittedValue));
submittedValue = null;
bindingStatus = BindingStatus.DIRTY;
} catch (ConversionFailedException e) {
this.sourceValue = sourceValue;
invalidSourceValueCause = e;
bindingStatus = BindingStatus.INVALID_SOURCE_VALUE;
this.submittedValue = submittedValue;
invalidSubmittedValueCause = e;
bindingStatus = BindingStatus.INVALID_SUBMITTED_VALUE;
}
}
}
public Object getInvalidSourceValue() {
if (bindingStatus != BindingStatus.INVALID_SOURCE_VALUE) {
throw new IllegalStateException("No invalid source value");
public Object getInvalidSubmittedValue() {
if (bindingStatus != BindingStatus.INVALID_SUBMITTED_VALUE) {
throw new IllegalStateException("No invalid submitted value applied to this field");
}
return sourceValue;
return submittedValue;
}
public BindingStatus getBindingStatus() {
@ -186,32 +186,33 @@ public class GenericBinding implements Binding {
}
public ValidationStatus getValidationStatus() {
// TODO implementation
return ValidationStatus.NOT_VALIDATED;
}
public Alert getStatusAlert() {
if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) {
if (bindingStatus == BindingStatus.INVALID_SUBMITTED_VALUE) {
return new AbstractAlert() {
public String getCode() {
return "typeMismatch";
}
public String getMessage() {
MessageBuilder builder = new MessageBuilder(bindingContext.getMessageSource());
MessageBuilder builder = new MessageBuilder(context.getMessageSource());
builder.code(getCode());
if (invalidSourceValueCause instanceof ParseException) {
ParseException e = (ParseException) invalidSourceValueCause;
builder.arg("label", bindingContext.getLabel());
builder.arg("value", sourceValue);
if (invalidSubmittedValueCause instanceof ParseException) {
ParseException e = (ParseException) invalidSubmittedValueCause;
builder.arg("label", context.getLabel());
builder.arg("value", submittedValue);
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");
builder.defaultMessage("Failed to bind '" + context.getLabel() + "'; the submitted value "
+ StylerUtils.style(submittedValue) + " 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 "
ConversionFailedException e = (ConversionFailedException) invalidSubmittedValueCause;
builder.arg("label", new ResolvableArgument(context.getLabel()));
builder.arg("value", submittedValue);
builder.defaultMessage("Failed to bind '" + context.getLabel() + "'; the submitted value "
+ StylerUtils.style(submittedValue) + " has could not be converted to "
+ e.getTargetType().getName());
}
@ -257,6 +258,7 @@ public class GenericBinding implements Binding {
}
public void validate() {
// TODO implementation
}
public void commit() {
@ -270,57 +272,57 @@ public class GenericBinding implements Binding {
bindingStatus = BindingStatus.COMMITTED;
}
} else {
throw new IllegalStateException("Binding is not dirty; nothing to commit");
throw new IllegalStateException("Field is not dirty; nothing to commit");
}
}
public void revert() {
if (bindingStatus == BindingStatus.INVALID_SOURCE_VALUE) {
sourceValue = null;
invalidSourceValueCause = null;
if (bindingStatus == BindingStatus.INVALID_SUBMITTED_VALUE) {
submittedValue = null;
invalidSubmittedValueCause = null;
bindingStatus = BindingStatus.CLEAN;
} else if (bindingStatus == BindingStatus.DIRTY || bindingStatus == BindingStatus.COMMIT_FAILURE) {
buffer.clear();
bindingStatus = BindingStatus.CLEAN;
} else {
throw new IllegalStateException("Nothing to revert");
throw new IllegalStateException("Field is clean or committed; nothing to revert");
}
}
public Binding getNestedBinding(String property) {
return bindingContext.getNestedBinding(property);
public FieldModel getNested(String fieldName) {
return context.getNested(fieldName);
}
public boolean isList() {
return List.class.isAssignableFrom(getValueType());
return getValueType().isArray() || List.class.isAssignableFrom(getValueType());
}
public Binding getListElementBinding(int index) {
return bindingContext.getListElementBinding(index);
public FieldModel getListElement(int index) {
return context.getListElement(index);
}
public boolean isMap() {
return Map.class.isAssignableFrom(getValueType());
}
public Binding getMapValueBinding(Object key) {
public FieldModel getMapValue(Object key) {
if (key instanceof String) {
try {
key = bindingContext.getKeyFormatter().parse((String) key, getLocale());
key = context.getKeyFormatter().parse((String) key, getLocale());
} catch (ParseException e) {
throw new IllegalArgumentException("Unable to parse map key '" + key + "'", e);
}
}
return bindingContext.getMapValueBinding(key);
return context.getMapValue(key);
}
@SuppressWarnings("unchecked")
public String formatValue(Object value) {
Formatter formatter;
if (Collection.class.isAssignableFrom(getValueType()) || getValueType().isArray() || isMap()) {
formatter = bindingContext.getElementFormatter();
formatter = context.getElementFormatter();
} else {
formatter = bindingContext.getFormatter();
formatter = context.getFormatter();
}
return format(value, formatter);
}
@ -330,7 +332,7 @@ public class GenericBinding implements Binding {
@SuppressWarnings("unchecked")
private String format(Object value, Formatter formatter) {
Class<?> formattedType = getFormattedObjectType(formatter.getClass());
value = bindingContext.getTypeConverter().convert(value, formattedType);
value = context.getTypeConverter().convert(value, formattedType);
return formatter.format(value, getLocale());
}
@ -370,7 +372,7 @@ public class GenericBinding implements Binding {
@SuppressWarnings("unchecked")
private Object coerseToValueType(Object parsed) {
TypeDescriptor targetType = valueModel.getValueTypeDescriptor();
TypeConverter converter = bindingContext.getTypeConverter();
TypeConverter converter = context.getTypeConverter();
if (parsed != null && converter.canConvert(parsed.getClass(), targetType)) {
return converter.convert(parsed, targetType);
} else {
@ -380,13 +382,13 @@ public class GenericBinding implements Binding {
private void assertEditable() {
if (!isEditable()) {
throw new IllegalStateException("Binding is not editable");
throw new IllegalStateException("Field is not editable");
}
}
private void assertEnabled() {
if (!isEditable()) {
throw new IllegalStateException("Binding is not enabled");
throw new IllegalStateException("Field is not enabled");
}
}

View File

@ -29,11 +29,11 @@ import org.springframework.context.MessageSource;
import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.core.convert.TypeConverter;
import org.springframework.core.convert.support.DefaultTypeConverter;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.BindingFactory;
import org.springframework.ui.binding.FieldModel;
import org.springframework.ui.binding.FieldNotFoundException;
import org.springframework.ui.binding.PresentationModel;
import org.springframework.ui.binding.binder.Binder;
import org.springframework.ui.binding.binder.BindingResult;
import org.springframework.ui.binding.config.BindingRuleConfiguration;
import org.springframework.ui.binding.config.FieldModelConfiguration;
import org.springframework.ui.binding.config.Condition;
import org.springframework.ui.format.Formatter;
import org.springframework.util.Assert;
@ -42,15 +42,16 @@ import org.springframework.util.Assert;
* A generic {@link Binder binder} suitable for use in most environments.
* @author Keith Donald
* @since 3.0
* @see #setFormatterRegistry(FormatterRegistry)
* @see #setMessageSource(MessageSource)
* @see #setTypeConverter(TypeConverter)
* @see #bind(Map)
*/
public class GenericBindingFactory implements BindingFactory {
public class DefaultPresentationModel implements PresentationModel {
private Object model;
private Object domainModel;
private Map<String, GenericBindingRule> bindingRules;
private Map<String, PropertyFieldModelRule> fieldModelRules;
private FormatterRegistry formatterRegistry;
@ -59,19 +60,19 @@ public class GenericBindingFactory implements BindingFactory {
private MessageSource messageSource;
/**
* Creates a new binder for the model object.
* @param model the model object containing properties this binder will bind to
* Creates a new presentation model for the domain model.
* @param domainModel the domain model object
*/
public GenericBindingFactory(Object model) {
Assert.notNull(model, "The model to bind to is required");
this.model = model;
bindingRules = new HashMap<String, GenericBindingRule>();
public DefaultPresentationModel(Object domainModel) {
Assert.notNull(domainModel, "The domain model to bind to is required");
this.domainModel = domainModel;
fieldModelRules = new HashMap<String, PropertyFieldModelRule>();
formatterRegistry = new GenericFormatterRegistry();
typeConverter = new DefaultTypeConverter();
}
/**
* Configures the registry of Formatters to query when no explicit Formatter has been registered for a Binding.
* Configures the registry of Formatters to query when no explicit Formatter has been registered for a field.
* Allows Formatters to be applied by property type and by property annotation.
* @param registry the formatter registry
*/
@ -81,7 +82,7 @@ public class GenericBindingFactory implements BindingFactory {
}
/**
* Configure the MessageSource that resolves localized {@link BindingResult} alert messages.
* Configure the MessageSource that resolves localized UI alert messages.
* @param messageSource the message source
*/
public void setMessageSource(MessageSource messageSource) {
@ -90,10 +91,11 @@ public class GenericBindingFactory implements BindingFactory {
}
/**
* Configure the TypeConverter that converts values as required by Binding setValue and getValue attempts.
* For a setValue attempt, the TypeConverter will be asked to perform a conversion if the value parsed by the Binding's Formatter is not assignable to the target property type.
* For a getValue attempt, the TypeConverter will be asked to perform a conversion if the property type does not match the type T required by the Binding's Formatter.
* @param typeConverter the type converter used by the binding system, which is based on Spring EL
* Configure the TypeConverter that converts values as required by the binding system.
* For a {@link FieldModel#applySubmittedValue(Object) applySubmittedValue call}, this TypeConverter will be asked to perform a conversion if the value parsed by the field's Formatter is not assignable to the target value type.
* For a {@link FieldModel#getRenderValue() getRenderValue call}, this TypeConverter will be asked to perform a conversion if the value type does not match the type T required by the field's Formatter.
* For a {@link FieldModel#getMapValue(Object) getMapValue call} this TypeConverter will be asked to convert the Map key to the type required if there is no keyFormatter registered for the field.
* @param typeConverter the type converter used by the binding system
*/
public void setTypeConverter(TypeConverter typeConverter) {
Assert.notNull(typeConverter, "The TypeConverter is required");
@ -101,57 +103,57 @@ public class GenericBindingFactory implements BindingFactory {
}
/**
* Add a new binding rule for the property at the path specified.
* Add a a new FieldModelRule for the property at the path specified.
* @param propertyPath binding rule property path in format prop.nestedProp
* @return a builder for the binding rule
*/
public BindingRuleConfiguration bindingRule(String propertyPath) {
PropertyPath path = new PropertyPath(propertyPath);
GenericBindingRule rule = getBindingRule(path.getFirstElement().getValue());
for (PropertyPathElement element : path.getNestedElements()) {
rule = rule.getBindingRule(element.getValue());
public FieldModelConfiguration field(String propertyPath) {
FieldPath path = new FieldPath(propertyPath);
PropertyFieldModelRule rule = getRule(path.getFirstElement().getValue());
for (FieldPathElement element : path.getNestedElements()) {
rule = rule.getNestedRule(element.getValue());
}
return rule;
}
// implementing Binder
public Object getModel() {
return model;
public Object getDomainModel() {
return domainModel;
}
public Binding getBinding(String property) {
PropertyPath path = new PropertyPath(property);
Binding binding = getBindingRule(path.getFirstElement().getValue()).getBinding(model);
for (PropertyPathElement element : path.getNestedElements()) {
// implementing PresentationModel
public FieldModel getFieldModel(String fieldName) {
FieldPath path = new FieldPath(fieldName);
FieldModel field = getRule(path.getFirstElement().getValue()).getFieldModel(domainModel);
for (FieldPathElement element : path.getNestedElements()) {
if (element.isIndex()) {
if (binding.isMap()) {
binding = binding.getMapValueBinding(element.getValue());
} else if (binding.isList()) {
binding = binding.getListElementBinding(element.getIntValue());
if (field.isMap()) {
field = field.getMapValue(element.getValue());
} else if (field.isList()) {
field = field.getListElement(element.getIntValue());
} else {
throw new IllegalArgumentException("Attempted to index a property that is not a List or Map");
throw new IllegalArgumentException("Attempted to index a field that is not a List, Array, or a Map");
}
} else {
binding = binding.getNestedBinding(element.getValue());
field = field.getNested(element.getValue());
}
}
return binding;
return field;
}
private GenericBindingRule getBindingRule(String property) {
GenericBindingRule rule = bindingRules.get(property);
private PropertyFieldModelRule getRule(String fieldName) {
PropertyFieldModelRule rule = fieldModelRules.get(fieldName);
if (rule == null) {
rule = new GenericBindingRule(property, model.getClass());
bindingRules.put(property, rule);
rule = new PropertyFieldModelRule(fieldName, domainModel.getClass());
fieldModelRules.put(fieldName, rule);
}
return rule;
}
@SuppressWarnings("unchecked")
class GenericBindingRule implements BindingRuleConfiguration, BindingContext {
class PropertyFieldModelRule implements FieldModelConfiguration, FieldModelContext {
private Class<?> modelClass;
private Class<?> domainModelClass;
private PropertyDescriptor property;
@ -167,20 +169,20 @@ public class GenericBindingFactory implements BindingFactory {
private Condition visibleCondition = Condition.ALWAYS_TRUE;
private Map<String, GenericBindingRule> nestedBindingRules;
private Map<String, PropertyFieldModelRule> nestedFieldModelRules;
private Binding binding;
private FieldModel fieldModel;
private Map<Integer, Binding> listElementBindings;
private Map<Integer, FieldModel> listElements;
private Map<Object, Binding> mapValueBindings;
private Map<Object, FieldModel> mapValues;
public GenericBindingRule(String property, Class modelClass) {
this.modelClass = modelClass;
public PropertyFieldModelRule(String property, Class domainModelClass) {
this.domainModelClass = domainModelClass;
this.property = findPropertyDescriptor(property);
}
// implementing BindingContext
// implementing FieldModelContext
public MessageSource getMessageSource() {
return messageSource;
@ -226,84 +228,103 @@ public class GenericBindingFactory implements BindingFactory {
return visibleCondition;
}
public Binding getNestedBinding(String property) {
createValueIfNecessary();
return getBindingRule(property, binding.getValueType()).getBinding(binding.getValue());
}
public Binding getListElementBinding(int index) {
if (listElementBindings == null) {
listElementBindings = new HashMap<Integer, Binding>();
}
growListIfNecessary(index);
Binding binding = listElementBindings.get(index);
if (binding == null) {
BindingContext bindingContext = new ListElementBindingContext(index, this);
ValueModel valueModel = new ListElementValueModel(index, getElementType(), (List) this.binding.getValue());
binding = new GenericBinding(valueModel, bindingContext);
listElementBindings.put(index, binding);
}
return binding;
}
public Binding getMapValueBinding(Object key) {
if (mapValueBindings == null) {
mapValueBindings = new HashMap<Object, Binding>();
}
createMapValueIfNecessary();
Binding binding = mapValueBindings.get(key);
if (binding == null) {
BindingContext bindingContext = new MapValueBindingContext(key, this);
ValueModel valueModel = new MapValueValueModel(key, getElementType(), (Map) this.binding.getValue(), bindingContext);
binding = new GenericBinding(valueModel, bindingContext);
mapValueBindings.put(key, binding);
}
return binding;
}
public String getLabel() {
return property.getName();
}
public FieldModel getNested(String fieldName) {
createValueIfNecessary();
return getNestedRule(fieldName, fieldModel.getValueType()).getFieldModel(fieldModel.getValue());
}
// implementing BindingRuleConfiguration
public FieldModel getListElement(int index) {
// TODO array support
if (listElements == null) {
listElements = new HashMap<Integer, FieldModel>();
}
growListIfNecessary(index);
FieldModel field = listElements.get(index);
if (field == null) {
FieldModelContext context = new ListElementContext(index, this);
ValueModel valueModel = new ListElementValueModel(index, getElementType(), (List) fieldModel.getValue());
field = new DefaultFieldModel(valueModel, context);
listElements.put(index, field);
}
return field;
}
public BindingRuleConfiguration formatWith(Formatter<?> formatter) {
public FieldModel getMapValue(Object key) {
if (mapValues == null) {
mapValues = new HashMap<Object, FieldModel>();
}
createMapValueIfNecessary();
FieldModel field = mapValues.get(key);
if (field == null) {
FieldModelContext context = new MapValueContext(key, this);
ValueModel valueModel = new MapValueValueModel(key, getElementType(), (Map) fieldModel.getValue(), context);
field = new DefaultFieldModel(valueModel, context);
mapValues.put(key, field);
}
return field;
}
// implementing FieldModelConfiguration
public FieldModelConfiguration formatWith(Formatter<?> formatter) {
this.formatter = formatter;
return this;
}
public BindingRuleConfiguration formatElementsWith(Formatter<?> formatter) {
if (!List.class.isAssignableFrom(modelClass) || modelClass.isArray()) {
public FieldModelConfiguration formatElementsWith(Formatter<?> formatter) {
if (!List.class.isAssignableFrom(domainModelClass) || domainModelClass.isArray()) {
throw new IllegalStateException(
"Bound property is not a List or an array; cannot set a element formatter");
"Field is not a List or an Array; cannot set a element formatter");
}
elementFormatter = formatter;
return this;
}
public BindingRuleConfiguration formatKeysWith(Formatter<?> formatter) {
if (!Map.class.isAssignableFrom(modelClass)) {
throw new IllegalStateException("Bound property is not a Map; cannot set a key formatter");
public FieldModelConfiguration formatKeysWith(Formatter<?> formatter) {
if (!Map.class.isAssignableFrom(domainModelClass)) {
throw new IllegalStateException("Field is not a Map; cannot set a key formatter");
}
keyFormatter = formatter;
return this;
}
public BindingRuleConfiguration editableWhen(Condition condition) {
public FieldModelConfiguration editableWhen(Condition condition) {
editableCondition = condition;
return this;
}
public BindingRuleConfiguration enabledWhen(Condition condition) {
public FieldModelConfiguration enabledWhen(Condition condition) {
enabledCondition = condition;
return this;
}
public BindingRuleConfiguration visibleWhen(Condition condition) {
public FieldModelConfiguration visibleWhen(Condition condition) {
visibleCondition = condition;
return this;
}
// package private helpers
PropertyFieldModelRule getNestedRule(String propertyName) {
return getNestedRule(propertyName, this.property.getPropertyType());
}
PropertyFieldModelRule getNestedRule(String propertyName, Class<?> domainModelClass) {
if (nestedFieldModelRules == null) {
nestedFieldModelRules = new HashMap<String, PropertyFieldModelRule>();
}
PropertyFieldModelRule rule = nestedFieldModelRules.get(propertyName);
if (rule == null) {
rule = new PropertyFieldModelRule(propertyName, domainModelClass);
nestedFieldModelRules.put(propertyName, rule);
}
return rule;
}
// internal helpers
private Class<?> getElementType() {
@ -318,40 +339,22 @@ public class GenericBindingFactory implements BindingFactory {
return GenericCollectionTypeResolver.getMapKeyReturnType(property.getReadMethod());
}
GenericBindingRule getBindingRule(String property) {
return getBindingRule(property, this.property.getPropertyType());
}
GenericBindingRule getBindingRule(String property, Class<?> modelClass) {
if (nestedBindingRules == null) {
nestedBindingRules = new HashMap<String, GenericBindingRule>();
FieldModel getFieldModel(Object domainObject) {
if (fieldModel == null) {
PropertyValueModel valueModel = new PropertyValueModel(property, domainObject);
fieldModel = new DefaultFieldModel(valueModel, this);
}
GenericBindingRule rule = nestedBindingRules.get(property);
if (rule == null) {
rule = new GenericBindingRule(property, modelClass);
nestedBindingRules.put(property, rule);
}
return rule;
}
// internal helpers
Binding getBinding(Object model) {
if (binding == null) {
PropertyValueModel valueModel = new PropertyValueModel(property, model);
binding = new GenericBinding(valueModel, this);
}
return binding;
return fieldModel;
}
private PropertyDescriptor findPropertyDescriptor(String property) {
PropertyDescriptor[] propDescs = getBeanInfo(modelClass).getPropertyDescriptors();
PropertyDescriptor[] propDescs = getBeanInfo(domainModelClass).getPropertyDescriptors();
for (PropertyDescriptor propDesc : propDescs) {
if (propDesc.getName().equals(property)) {
return propDesc;
}
}
throw new PropertyNotFoundException(property, modelClass);
throw new FieldNotFoundException(property);
}
private BeanInfo getBeanInfo(Class<?> clazz) {
@ -363,30 +366,30 @@ public class GenericBindingFactory implements BindingFactory {
}
private void createValueIfNecessary() {
Object value = binding.getValue();
Object value = fieldModel.getValue();
if (value == null) {
value = newValue(binding.getValueType());
binding.applySourceValue(value);
binding.commit();
value = newValue(fieldModel.getValueType());
fieldModel.applySubmittedValue(value);
fieldModel.commit();
}
}
private void createMapValueIfNecessary() {
Object value = binding.getValue();
Object value = fieldModel.getValue();
if (value == null) {
value = newMapValue(binding.getValueType());
binding.applySourceValue(value);
binding.commit();
value = newMapValue(fieldModel.getValueType());
fieldModel.applySubmittedValue(value);
fieldModel.commit();
}
}
private void growListIfNecessary(int index) {
List list = (List) binding.getValue();
List list = (List) fieldModel.getValue();
if (list == null) {
list = newListValue(binding.getValueType());
binding.applySourceValue(list);
binding.commit();
list = (List) binding.getValue();
list = newListValue(fieldModel.getValueType());
fieldModel.applySubmittedValue(list);
fieldModel.commit();
list = (List) fieldModel.getValue();
}
if (index >= list.size()) {
for (int i = list.size(); i <= index; i++) {
@ -423,15 +426,15 @@ public class GenericBindingFactory implements BindingFactory {
}
private static class ListElementBindingContext implements BindingContext {
private static class ListElementContext implements FieldModelContext {
private int index;
private GenericBindingRule listBindingContext;
private PropertyFieldModelRule listBindingContext;
final Map<String, Binding> nestedBindings = new HashMap<String, Binding>();
final Map<String, FieldModel> nestedBindings = new HashMap<String, FieldModel>();
public ListElementBindingContext(int index, GenericBindingRule listBindingContext) {
public ListElementContext(int index, PropertyFieldModelRule listBindingContext) {
this.index = index;
this.listBindingContext = listBindingContext;
}
@ -444,22 +447,6 @@ public class GenericBindingFactory implements BindingFactory {
return listBindingContext.getTypeConverter();
}
public Binding getNestedBinding(String property) {
Object model = ((List<?>) listBindingContext.binding.getValue()).get(index);
Class<?> elementType = listBindingContext.getElementType();
if (elementType == null) {
elementType = model.getClass();
}
GenericBindingRule rule = listBindingContext.getBindingRule(property, elementType);
Binding binding = nestedBindings.get(property);
if (binding == null) {
PropertyValueModel valueModel = new PropertyValueModel(rule.property, model);
binding = new GenericBinding(valueModel, rule);
nestedBindings.put(property, binding);
}
return binding;
}
@SuppressWarnings("unchecked")
public Formatter getFormatter() {
return listBindingContext.getElementFormatter();
@ -467,11 +454,13 @@ public class GenericBindingFactory implements BindingFactory {
@SuppressWarnings("unchecked")
public Formatter getElementFormatter() {
// TODO multi-dimensional support
return null;
}
@SuppressWarnings("unchecked")
public Formatter getKeyFormatter() {
// TODO multi-dimensional support
return null;
}
@ -487,95 +476,116 @@ public class GenericBindingFactory implements BindingFactory {
return listBindingContext.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 listBindingContext.getLabel() + "[" + index + "]";
}
};
private static class MapValueBindingContext implements BindingContext {
private Object key;
private GenericBindingRule mapBindingContext;
final Map<String, Binding> nestedBindings = new HashMap<String, Binding>();
public MapValueBindingContext(Object key, GenericBindingRule mapBindingContext) {
this.key = key;
this.mapBindingContext = mapBindingContext;
}
public MessageSource getMessageSource() {
return mapBindingContext.getMessageSource();
}
public TypeConverter getTypeConverter() {
return mapBindingContext.getTypeConverter();
}
@SuppressWarnings("unchecked")
public Binding getNestedBinding(String property) {
Object model = ((Map) mapBindingContext.binding.getValue()).get(key);
Class<?> elementType = mapBindingContext.getElementType();
public FieldModel getNested(String property) {
Object model = ((List<?>) listBindingContext.fieldModel.getValue()).get(index);
Class<?> elementType = listBindingContext.getElementType();
if (elementType == null) {
elementType = model.getClass();
}
GenericBindingRule rule = mapBindingContext.getBindingRule(property, elementType);
Binding binding = nestedBindings.get(property);
PropertyFieldModelRule rule = listBindingContext.getNestedRule(property, elementType);
FieldModel binding = nestedBindings.get(property);
if (binding == null) {
PropertyValueModel valueModel = new PropertyValueModel(rule.property, model);
binding = new GenericBinding(valueModel, rule);
binding = new DefaultFieldModel(valueModel, rule);
nestedBindings.put(property, binding);
}
return binding;
}
public FieldModel getListElement(int index) {
// TODO multi-dimensional support
throw new IllegalArgumentException("Not yet supported");
}
public FieldModel getMapValue(Object key) {
// TODO multi-dimensional support
throw new IllegalArgumentException("Not yet supported");
}
};
private static class MapValueContext implements FieldModelContext {
private Object key;
private PropertyFieldModelRule mapContext;
final Map<String, FieldModel> nestedBindings = new HashMap<String, FieldModel>();
public MapValueContext(Object key, PropertyFieldModelRule mapContext) {
this.key = key;
this.mapContext = mapContext;
}
public MessageSource getMessageSource() {
return mapContext.getMessageSource();
}
public TypeConverter getTypeConverter() {
return mapContext.getTypeConverter();
}
@SuppressWarnings("unchecked")
public Formatter getFormatter() {
return mapBindingContext.getElementFormatter();
return mapContext.getElementFormatter();
}
@SuppressWarnings("unchecked")
public Formatter getElementFormatter() {
// TODO multi-dimensional support
return null;
}
@SuppressWarnings("unchecked")
public Formatter getKeyFormatter() {
// TODO multi-dimensional support
return null;
}
public Condition getEditableCondition() {
return mapBindingContext.getEditableCondition();
return mapContext.getEditableCondition();
}
public Condition getEnabledCondition() {
return mapBindingContext.getEnabledCondition();
return mapContext.getEnabledCondition();
}
public Condition getVisibleCondition() {
return mapBindingContext.getVisibleCondition();
return mapContext.getVisibleCondition();
}
public Binding getListElementBinding(int index) {
@SuppressWarnings("unchecked")
public FieldModel getNested(String property) {
Object model = ((Map) mapContext.fieldModel.getValue()).get(key);
Class<?> elementType = mapContext.getElementType();
if (elementType == null) {
elementType = model.getClass();
}
PropertyFieldModelRule rule = mapContext.getNestedRule(property, elementType);
FieldModel binding = nestedBindings.get(property);
if (binding == null) {
PropertyValueModel valueModel = new PropertyValueModel(rule.property, model);
binding = new DefaultFieldModel(valueModel, rule);
nestedBindings.put(property, binding);
}
return binding;
}
public FieldModel getListElement(int index) {
// TODO multi-dimensional support
throw new IllegalArgumentException("Not yet supported");
}
public Binding getMapValueBinding(Object key) {
public FieldModel getMapValue(Object key) {
// TODO multi-dimensional support
throw new IllegalArgumentException("Not yet supported");
}
public String getLabel() {
return mapBindingContext.getLabel() + "[" + key + "]";
return mapContext.getLabel() + "[" + key + "]";
}
};

View File

@ -18,26 +18,26 @@ package org.springframework.ui.binding.support;
import java.util.IdentityHashMap;
import java.util.Map;
import org.springframework.ui.binding.BindingFactory;
import org.springframework.ui.binding.BindingFactoryLocator;
import org.springframework.ui.binding.PresentationModel;
import org.springframework.ui.binding.PresentationModelFactory;
/**
* BindingFactoryLocator implementation that uses a {@link IdentityHashMap} to map models to BindingFactories.
* @author Keith Donald
*/
public class GenericBindingFactoryLocator implements BindingFactoryLocator {
public class DefaultPresentationModelFactory implements PresentationModelFactory {
private Map<Object, BindingFactory> bindingFactories = new IdentityHashMap<Object, BindingFactory>();
private Map<Object, PresentationModel> presentationModels = new IdentityHashMap<Object, PresentationModel>();
public void put(BindingFactory bindingFactory) {
bindingFactories.put(bindingFactory.getModel(), bindingFactory);
public void put(Object domainObject, PresentationModel presentationModel) {
presentationModels.put(domainObject, presentationModel);
}
public BindingFactory getBindingFactory(Object model) {
BindingFactory factory = bindingFactories.get(model);
public PresentationModel getPresentationModel(Object domainObject) {
PresentationModel factory = presentationModels.get(domainObject);
if (factory == null) {
factory = new GenericBindingFactory(model);
bindingFactories.put(model, factory);
factory = new DefaultPresentationModel(domainObject);
presentationModels.put(domainObject, factory);
}
return factory;
}

View File

@ -17,16 +17,16 @@ package org.springframework.ui.binding.support;
import org.springframework.context.MessageSource;
import org.springframework.core.convert.TypeConverter;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.FieldModel;
import org.springframework.ui.binding.config.Condition;
import org.springframework.ui.format.Formatter;
/**
* A context that allows a Binding to query its BindingRule.
* A context that allows a FieldModel to access its external configuration.
* @author Keith Donald
* @since 3.0
*/
public interface BindingContext {
public interface FieldModelContext {
MessageSource getMessageSource();
@ -38,8 +38,6 @@ public interface BindingContext {
Condition getVisibleCondition();
Binding getNestedBinding(String property);
@SuppressWarnings("unchecked")
Formatter getFormatter();
@ -49,10 +47,12 @@ public interface BindingContext {
@SuppressWarnings("unchecked")
Formatter getKeyFormatter();
Binding getListElementBinding(int index);
Binding getMapValueBinding(Object key);
String getLabel();
FieldModel getNested(String fieldName);
FieldModel getListElement(int index);
FieldModel getMapValue(Object key);
}

View File

@ -20,11 +20,11 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class PropertyPath implements Iterable<PropertyPathElement> {
class FieldPath implements Iterable<FieldPathElement> {
private List<PropertyPathElement> elements = new ArrayList<PropertyPathElement>();
private List<FieldPathElement> elements = new ArrayList<FieldPathElement>();
public PropertyPath(String propertyPath) {
public FieldPath(String propertyPath) {
// a.b.c[i].d[key].e
String[] props = propertyPath.split("\\.");
if (props.length == 0) {
@ -35,19 +35,19 @@ public class PropertyPath implements Iterable<PropertyPathElement> {
int start = prop.indexOf('[');
int end = prop.indexOf(']', start);
String index = prop.substring(start + 1, end);
elements.add(new PropertyPathElement(prop.substring(0, start), false));
elements.add(new PropertyPathElement(index, true));
elements.add(new FieldPathElement(prop.substring(0, start), false));
elements.add(new FieldPathElement(index, true));
} else {
elements.add(new PropertyPathElement(prop, false));
elements.add(new FieldPathElement(prop, false));
}
}
}
public PropertyPathElement getFirstElement() {
public FieldPathElement getFirstElement() {
return elements.get(0);
}
public List<PropertyPathElement> getNestedElements() {
public List<FieldPathElement> getNestedElements() {
if (elements.size() > 1) {
return elements.subList(1, elements.size());
} else {
@ -55,12 +55,11 @@ public class PropertyPath implements Iterable<PropertyPathElement> {
}
}
public Iterator<PropertyPathElement> iterator() {
public Iterator<FieldPathElement> iterator() {
return elements.iterator();
}
public String toString() {
return elements.toString();
}
}

View File

@ -15,13 +15,13 @@
*/
package org.springframework.ui.binding.support;
public class PropertyPathElement {
class FieldPathElement {
private String value;
private boolean index;
public PropertyPathElement(String value, boolean index) {
public FieldPathElement(String value, boolean index) {
this.value = value;
this.index = index;
}

View File

@ -28,6 +28,10 @@ import org.springframework.ui.format.Formatter;
*/
public interface FormatterRegistry {
/**
* Get the Formatter for the property.
* @return the Formatter, or <code>null</code> if none is registered
*/
Formatter<?> getFormatter(PropertyDescriptor property);
/**

View File

@ -263,7 +263,7 @@ public class GenericFormatterRegistry implements FormatterRegistry {
public Object parse(String formatted, Locale locale) throws ParseException {
String[] fields = StringUtils.commaDelimitedListToStringArray(formatted);
if (collectionType.getCollectionType().isArray()) {
if (collectionType.getType().isArray()) {
Object array = Array.newInstance(getElementType(), fields.length);
for (int i = 0; i < fields.length; i++) {
Array.set(array, i, elementFormatter.parse(fields[i], locale));
@ -289,7 +289,7 @@ public class GenericFormatterRegistry implements FormatterRegistry {
private Collection newCollection() {
try {
Class<? extends Collection> implType = ConversionUtils
.getCollectionImpl((Class<? extends Collection>) collectionType.getCollectionType());
.getCollectionImpl((Class<? extends Collection>) collectionType.getType());
return (Collection) implType.newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("Should not happen", e);

View File

@ -19,7 +19,12 @@ import java.util.List;
import org.springframework.core.convert.TypeDescriptor;
class ListElementValueModel implements ValueModel {
/**
* A ValueModel for a element in a List.
* @author Keith Donald
* @since 3.0
*/
public class ListElementValueModel implements ValueModel {
@SuppressWarnings("unchecked")
private List list;

View File

@ -19,6 +19,11 @@ import java.util.Map;
import org.springframework.core.convert.TypeDescriptor;
/**
* A ValueModel for a element in a Map.
* @author Keith Donald
* @since 3.0
*/
public class MapValueValueModel implements ValueModel {
private Object key;
@ -29,7 +34,7 @@ public class MapValueValueModel implements ValueModel {
private Map map;
@SuppressWarnings("unchecked")
public MapValueValueModel(Object key, Class<?> elementType, Map map, BindingContext bindingContext) {
public MapValueValueModel(Object key, Class<?> elementType, Map map, FieldModelContext bindingContext) {
this.key = key;
this.elementType = elementType;
this.map = map;

View File

@ -1,39 +0,0 @@
/*
* Copyright 2004-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ui.binding.support;
@SuppressWarnings("serial")
public class PropertyNotFoundException extends RuntimeException {
private String property;
private Class<?> modelClass;
public PropertyNotFoundException(String property, Class<?> modelClass) {
super("No property '" + property + "' found on model [" + modelClass.getName() + "]");
this.property = property;
this.modelClass = modelClass;
}
public String getProperty() {
return property;
}
public Class<?> getModelClass() {
return modelClass;
}
}

View File

@ -21,6 +21,11 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.ReflectionUtils;
/**
* A ValueModel for a bean property.
* @author Keith Donald
* @since 3.0
*/
public class PropertyValueModel implements ValueModel {
private PropertyDescriptor property;

View File

@ -18,8 +18,9 @@ package org.springframework.ui.binding.support;
import org.springframework.core.convert.TypeDescriptor;
/**
* For accessing the raw bound model object.
* A interface for reading and writing a value.
* @author Keith Donald
* @since 3.0
*/
public interface ValueModel {

View File

@ -1,4 +1,4 @@
package org.springframework.ui.binding.support;
package org.springframework.ui.binding.binder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -22,11 +22,14 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.style.ToStringCreator;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.binding.FieldModel;
import org.springframework.ui.binding.BindingStatus;
import org.springframework.ui.binding.binder.BindingResults;
import org.springframework.ui.binding.binder.GenericBinder;
import org.springframework.ui.binding.binder.MissingSourceValuesException;
import org.springframework.ui.binding.binder.MissingFieldException;
import org.springframework.ui.binding.support.CollectionTypeDescriptor;
import org.springframework.ui.binding.support.DefaultPresentationModel;
import org.springframework.ui.binding.support.GenericFormatterRegistry;
import org.springframework.ui.format.AnnotationFormatterFactory;
import org.springframework.ui.format.Formatted;
import org.springframework.ui.format.Formatter;
@ -40,15 +43,15 @@ public class GenericBinderTests {
private GenericBinder binder;
private GenericBindingFactory bindingFactory;
private DefaultPresentationModel presentationModel;
private TestBean bean;
@Before
public void setUp() {
bean = new TestBean();
bindingFactory = new GenericBindingFactory(bean);
binder = new GenericBinder(bindingFactory);
presentationModel = new DefaultPresentationModel(bean);
binder = new GenericBinder(presentationModel);
LocaleContextHolder.setLocale(Locale.US);
}
@ -66,17 +69,17 @@ public class GenericBinderTests {
BindingResults results = binder.bind(values);
assertEquals(3, results.size());
assertEquals("string", results.get(0).getProperty());
assertEquals("string", results.get(0).getFieldName());
assertFalse(results.get(0).isFailure());
assertEquals("test", results.get(0).getSourceValue());
assertEquals("test", results.get(0).getSubmittedValue());
assertEquals("integer", results.get(1).getProperty());
assertEquals("integer", results.get(1).getFieldName());
assertFalse(results.get(1).isFailure());
assertEquals("3", results.get(1).getSourceValue());
assertEquals("3", results.get(1).getSubmittedValue());
assertEquals("foo", results.get(2).getProperty());
assertEquals("foo", results.get(2).getFieldName());
assertFalse(results.get(2).isFailure());
assertEquals("BAR", results.get(2).getSourceValue());
assertEquals("BAR", results.get(2).getSubmittedValue());
assertEquals("test", bean.getString());
assertEquals(3, bean.getInteger());
@ -98,14 +101,14 @@ public class GenericBinderTests {
@Test
public void bindSingleValuePropertyFormatter() throws ParseException {
bindingFactory.bindingRule("date").formatWith(new DateFormatter());
presentationModel.field("date").formatWith(new DateFormatter());
binder.bind(Collections.singletonMap("date", "2009-06-01"));
assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate());
}
@Test
public void bindSingleValuePropertyFormatterParseException() {
bindingFactory.bindingRule("date").formatWith(new DateFormatter());
presentationModel.field("date").formatWith(new DateFormatter());
BindingResults results = binder.bind(Collections.singletonMap("date", "bogus"));
assertEquals(1, results.size());
assertTrue(results.get(0).isFailure());
@ -116,7 +119,7 @@ public class GenericBinderTests {
public void bindSingleValueWithFormatterRegistedByType() throws ParseException {
GenericFormatterRegistry formatterRegistry = new GenericFormatterRegistry();
formatterRegistry.add(Date.class, new DateFormatter());
bindingFactory.setFormatterRegistry(formatterRegistry);
presentationModel.setFormatterRegistry(formatterRegistry);
binder.bind(Collections.singletonMap("date", "2009-06-01"));
assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate());
@ -126,7 +129,7 @@ public class GenericBinderTests {
public void bindSingleValueWithAnnotationFormatterFactoryRegistered() throws ParseException {
GenericFormatterRegistry formatterRegistry = new GenericFormatterRegistry();
formatterRegistry.add(new CurrencyAnnotationFormatterFactory());
bindingFactory.setFormatterRegistry(formatterRegistry);
presentationModel.setFormatterRegistry(formatterRegistry);
binder.bind(Collections.singletonMap("currency", "$23.56"));
assertEquals(new BigDecimal("23.56"), bean.getCurrency());
@ -135,27 +138,27 @@ public class GenericBinderTests {
@Test
public void bindSingleValuePropertyNotFound() throws ParseException {
BindingResults results = binder.bind(Collections.singletonMap("bogus", "2009-06-01"));
assertEquals("bogus", results.get(0).getProperty());
assertEquals("bogus", results.get(0).getFieldName());
assertTrue(results.get(0).isFailure());
assertEquals("propertyNotFound", results.get(0).getAlert().getCode());
}
@Test(expected = MissingSourceValuesException.class)
@Test(expected = MissingFieldException.class)
public void bindMissingRequiredSourceValue() {
binder.setRequired(new String[] { "integer" });
binder.setRequiredFields(new String[] { "integer" });
// missing "integer" - violated bind contract
binder.bind(Collections.singletonMap("string", "test"));
}
@Test
public void getBindingCustomFormatter() {
bindingFactory.bindingRule("currency").formatWith(new CurrencyFormatter());
Binding b = bindingFactory.getBinding("currency");
presentationModel.field("currency").formatWith(new CurrencyFormatter());
FieldModel b = presentationModel.getFieldModel("currency");
assertFalse(b.isList());
assertFalse(b.isMap());
assertEquals(null, b.getValue());
assertEquals("", b.getRenderValue());
b.applySourceValue("$23.56");
b.applySubmittedValue("$23.56");
assertEquals(BindingStatus.DIRTY, b.getBindingStatus());
assertEquals(new BigDecimal("23.56"), b.getValue());
assertEquals("$23.56", b.getRenderValue());
@ -168,9 +171,9 @@ public class GenericBinderTests {
@Test
public void getBindingCustomFormatterRequiringTypeCoersion() {
// IntegerFormatter formats Longs, so conversion from Integer -> Long is performed
bindingFactory.bindingRule("integer").formatWith(new IntegerFormatter());
Binding b = bindingFactory.getBinding("integer");
b.applySourceValue("2,300");
presentationModel.field("integer").formatWith(new IntegerFormatter());
FieldModel b = presentationModel.getFieldModel("integer");
b.applySubmittedValue("2,300");
assertEquals("2,300", b.getRenderValue());
b.commit();
assertEquals(BindingStatus.COMMITTED, b.getBindingStatus());
@ -182,10 +185,10 @@ public class GenericBinderTests {
MockMessageSource messages = new MockMessageSource();
messages.addMessage("typeMismatch", Locale.US,
"Please enter an integer in format ### for the #{label} field; you entered #{value}");
bindingFactory.setMessageSource(messages);
bindingFactory.bindingRule("integer").formatWith(new IntegerFormatter());
Binding b = bindingFactory.getBinding("integer");
b.applySourceValue("bogus");
presentationModel.setMessageSource(messages);
presentationModel.field("integer").formatWith(new IntegerFormatter());
FieldModel b = presentationModel.getFieldModel("integer");
b.applySubmittedValue("bogus");
assertEquals("Please enter an integer in format ### for the integer field; you entered bogus", b
.getStatusAlert().getMessage());
}
@ -193,11 +196,11 @@ public class GenericBinderTests {
@SuppressWarnings("unchecked")
@Test
public void getBindingMultiValued() {
Binding b = bindingFactory.getBinding("foos");
FieldModel b = presentationModel.getFieldModel("foos");
assertTrue(b.isList());
assertEquals(null, b.getValue());
assertEquals("", b.getRenderValue());
b.applySourceValue(new String[] { "BAR", "BAZ", "BOOP" });
b.applySubmittedValue(new String[] { "BAR", "BAZ", "BOOP" });
b.commit();
assertEquals(FooEnum.BAR, bean.getFoos().get(0));
assertEquals(FooEnum.BAZ, bean.getFoos().get(1));
@ -213,22 +216,22 @@ public class GenericBinderTests {
@Test
public void getBindingMultiValuedIndexAccess() {
bean.setFoos(Arrays.asList(new FooEnum[] { FooEnum.BAR }));
Binding b = bindingFactory.getBinding("foos[0]");
FieldModel b = presentationModel.getFieldModel("foos[0]");
assertFalse(b.isList());
assertEquals(FooEnum.BAR, b.getValue());
assertEquals("BAR", b.getRenderValue());
b.applySourceValue("BAZ");
b.applySubmittedValue("BAZ");
assertEquals("BAZ", b.getRenderValue());
assertEquals(FooEnum.BAZ, b.getValue());
}
@Test
public void getBindingMultiValuedTypeConversionFailure() {
Binding b = bindingFactory.getBinding("foos");
FieldModel b = presentationModel.getFieldModel("foos");
assertTrue(b.isList());
assertEquals(null, b.getValue());
b.applySourceValue(new String[] { "BAR", "BOGUS", "BOOP" });
assertEquals(BindingStatus.INVALID_SOURCE_VALUE, b.getBindingStatus());
b.applySubmittedValue(new String[] { "BAR", "BOGUS", "BOOP" });
assertEquals(BindingStatus.INVALID_SUBMITTED_VALUE, b.getBindingStatus());
assertEquals("typeMismatch", b.getStatusAlert().getCode());
}
@ -263,7 +266,7 @@ public class GenericBinderTests {
public void bindToListSingleString() {
GenericFormatterRegistry formatterRegistry = new GenericFormatterRegistry();
formatterRegistry.add(new CollectionTypeDescriptor(List.class, Address.class), new AddressListFormatter());
bindingFactory.setFormatterRegistry(formatterRegistry);
presentationModel.setFormatterRegistry(formatterRegistry);
Map<String, String> values = new LinkedHashMap<String, String>();
values
.put("addresses",
@ -310,7 +313,7 @@ public class GenericBinderTests {
public void getListAsSingleString() {
GenericFormatterRegistry formatterRegistry = new GenericFormatterRegistry();
formatterRegistry.add(new CollectionTypeDescriptor(List.class, Address.class), new AddressListFormatter());
bindingFactory.setFormatterRegistry(formatterRegistry);
presentationModel.setFormatterRegistry(formatterRegistry);
Address address1 = new Address();
address1.setStreet("s1");
address1.setCity("c1");
@ -325,8 +328,8 @@ public class GenericBinderTests {
addresses.add(address1);
addresses.add(address2);
bean.addresses = addresses;
String value = bindingFactory.getBinding("addresses").getRenderValue();
assertEquals("s1:c1:st1:z1,s2:c2:st2:z2,", value);
String value = presentationModel.getFieldModel("addresses").getRenderValue();
assertEquals("s1:c1:st1:z1,s2:c2:st2:z2", value);
}
@Test
@ -345,7 +348,7 @@ public class GenericBinderTests {
addresses.add(address1);
addresses.add(address2);
bean.addresses = addresses;
String value = bindingFactory.getBinding("addresses").getRenderValue();
String value = presentationModel.getFieldModel("addresses").getRenderValue();
assertEquals("s1:c1:st1:z1,s2:c2:st2:z2", value);
}
@ -424,7 +427,7 @@ public class GenericBinderTests {
foods.put(FoodGroup.FRUIT, "Peaches");
foods.put(FoodGroup.MEAT, "Ham");
bean.favoriteFoodsByGroup = foods;
String value = bindingFactory.getBinding("favoriteFoodsByGroup").getRenderValue();
String value = presentationModel.getFieldModel("favoriteFoodsByGroup").getRenderValue();
// TODO this is inconsistent with previous test case
assertEquals("{DAIRY=Milk, FRUIT=Peaches, MEAT=Ham}", value);
}
@ -439,15 +442,15 @@ public class GenericBinderTests {
@Test
public void formatPossibleValue() {
bindingFactory.bindingRule("currency").formatWith(new CurrencyFormatter());
Binding b = bindingFactory.getBinding("currency");
presentationModel.field("currency").formatWith(new CurrencyFormatter());
FieldModel b = presentationModel.getFieldModel("currency");
assertEquals("$5.00", b.formatValue(new BigDecimal("5")));
}
@Test
public void formatPossibleValueDefault() {
bindingFactory.bindingRule("currency");
Binding b = bindingFactory.getBinding("currency");
presentationModel.field("currency");
FieldModel b = presentationModel.getFieldModel("currency");
assertEquals("5", b.formatValue(new BigDecimal("5")));
}

View File

@ -1,4 +1,4 @@
package org.springframework.ui.binding.support;
package org.springframework.ui.binding.binder;
import static org.junit.Assert.assertEquals;
@ -12,11 +12,12 @@ import java.util.Map;
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.binder.BindingResults;
import org.springframework.ui.binding.binder.WebBinder;
import org.springframework.ui.binding.support.DefaultPresentationModel;
import org.springframework.ui.binding.support.GenericFormatterRegistry;
import org.springframework.ui.format.date.DateFormatter;
import org.springframework.ui.format.number.CurrencyFormat;
import org.springframework.ui.format.number.CurrencyFormatter;
@ -25,11 +26,15 @@ public class WebBinderTests {
TestBean bean = new TestBean();
WebBinder binder = new WebBinder(new GenericBindingFactory(bean));
DefaultPresentationModel presentationModel;
WebBinder binder;
@Before
public void setUp() {
LocaleContextHolder.setLocale(Locale.US);
presentationModel = new DefaultPresentationModel(bean);
binder = new WebBinder(presentationModel);
}
@After
@ -38,11 +43,11 @@ public class WebBinderTests {
}
@Test
@Ignore
public void bindUserValuesCreatedFromUserMap() throws ParseException {
GenericFormatterRegistry registry = new GenericFormatterRegistry();
registry.add(Date.class, new DateFormatter());
registry.add(CurrencyFormat.class, new CurrencyFormatter());
//binder.setFormatterRegistry(registry);
presentationModel.setFormatterRegistry(registry);
Map<String, String> userMap = new LinkedHashMap<String, String>();
userMap.put("string", "test");
userMap.put("_integer", "doesn't matter");
@ -53,12 +58,12 @@ public class WebBinderTests {
userMap.put("_addresses", "doesn't matter");
BindingResults results = binder.bind(userMap);
assertEquals(6, results.size());
assertEquals("test", results.get(0).getSourceValue());
assertEquals(null, results.get(1).getSourceValue());
assertEquals(Boolean.FALSE, results.get(2).getSourceValue());
assertEquals("2009-06-10", results.get(3).getSourceValue());
assertEquals("$5.00", results.get(4).getSourceValue());
assertEquals(null, results.get(5).getSourceValue());
assertEquals("test", results.get(0).getSubmittedValue());
assertEquals(null, results.get(1).getSubmittedValue());
assertEquals(Boolean.FALSE, results.get(2).getSubmittedValue());
assertEquals("2009-06-10", results.get(3).getSubmittedValue());
assertEquals("$5.00", results.get(4).getSubmittedValue());
assertEquals(null, results.get(5).getSubmittedValue());
assertEquals("test", bean.getString());
assertEquals(0, bean.getInteger());
@ -73,13 +78,21 @@ public class WebBinderTests {
}
public static class TestBean {
private String string;
private int integer;
private boolean bool;
private Date date;
private FooEnum foo;
private BigDecimal currency;
private List<FooEnum> foos;
private List<Address> addresses;
public String getString() {