binding result tracking

This commit is contained in:
Keith Donald
2009-06-09 15:21:34 +00:00
parent a1f1717d03
commit a437fdfc7d
5 changed files with 419 additions and 127 deletions

View File

@@ -21,8 +21,10 @@ 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.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -72,7 +74,7 @@ public class Binder<T> {
private boolean strict = false;
private static Formatter defaultFormatter = new Formatter() {
public String format(Object object, Locale locale) {
if (object == null) {
return "";
@@ -81,16 +83,15 @@ public class Binder<T> {
}
}
public Object parse(String formatted, Locale locale)
throws ParseException {
public Object parse(String formatted, Locale locale) throws ParseException {
if (formatted == "") {
return null;
} else {
return formatted;
}
}
}
};
/**
* Creates a new binder for the model object.
* @param model the model object containing properties this binder will bind to
@@ -98,13 +99,12 @@ public class Binder<T> {
public Binder(T model) {
this.model = model;
bindings = new HashMap<String, Binding>();
int parserConfig =
SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull |
SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize;
int parserConfig = SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull
| SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize;
expressionParser = new SpelExpressionParser(parserConfig);
typeConverter = new DefaultTypeConverter();
}
/**
* Configures if this binder is <i>strict</i>; a strict binder requires all bindings to be registered explicitly using {@link #add(BindingConfiguration)}.
* An <i>optimistic</i> binder will implicitly create bindings as required to support {@link #bind(Map)} operations.
@@ -140,10 +140,10 @@ public class Binder<T> {
if (propertyType.isAnnotation()) {
annotationFormatters.put(propertyType, new SimpleAnnotationFormatterFactory(formatter));
} else {
typeFormatters.put(propertyType, formatter);
typeFormatters.put(propertyType, formatter);
}
}
/**
* Adds a AnnotationFormatterFactory that will format values of properties annotated with a specific annotation.
* @param factory the annotation formatter factory
@@ -176,22 +176,22 @@ public class Binder<T> {
/**
* Bind values in the map to the properties of the model object.
* TODO return BindingResults with getSuccesses()/getErrors()/etc?
* @param propertyValues the property values map
*/
public void bind(Map<String, ? extends Object> propertyValues) {
for (Map.Entry<String, ? extends Object> entry : propertyValues
.entrySet()) {
Binding binding = getBinding(entry.getKey());
Object value = entry.getValue();
if (value instanceof String) {
binding.setValue((String)entry.getValue());
}
else if (value instanceof String[]) {
binding.setValues((String[])value);
public List<BindingResult> bind(List<UserValue> userValues) {
List<BindingResult> results = new ArrayList<BindingResult>(userValues.size());
for (UserValue value : userValues) {
Binding binding = getBinding(value.getProperty());
if (value.isString()) {
results.add(binding.setValue((String) value.getValue()));
} else if (value.isStringArray()) {
results.add(binding.setValues((String[]) value.getValue()));
} else {
throw new IllegalArgumentException("Illegal argument " + value);
}
}
return results;
}
class BindingImpl implements Binding {
@@ -200,34 +200,60 @@ public class Binder<T> {
private Formatter formatter;
public BindingImpl(BindingConfiguration config)
throws org.springframework.expression.ParseException {
public BindingImpl(BindingConfiguration config) throws org.springframework.expression.ParseException {
property = expressionParser.parseExpression(config.getProperty());
formatter = config.getFormatter();
}
public String getValue() {
Object value;
try {
return format(property.getValue(createEvaluationContext()));
value = property.getValue(createEvaluationContext());
} catch (ExpressionException e) {
throw new IllegalArgumentException(e);
throw new IllegalStateException("Failed to get property expression value - this should not happen", e);
}
return format(value);
}
public void setValue(String formatted) {
setValue(parse(formatted, getFormatter()));
public BindingResult setValue(String formatted) {
Formatter formatter;
try {
formatter = getFormatter();
} catch (EvaluationException e) {
// could occur the property was not found or is not readable
// TODO probably should not handle all EL failures, only type conversion & property not found?
return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e);
}
Object parsed;
try {
parsed = formatter.parse(formatted, LocaleContextHolder.getLocale());
} catch (ParseException e) {
return new InvalidFormatResult(property.getExpressionString(), formatted, e);
}
return setValue(parsed, formatted);
}
public String format(Object selectableValue) {
Formatter formatter = getFormatter();
Formatter formatter;
try {
formatter = getFormatter();
} catch (EvaluationException e) {
throw new IllegalStateException("Failed to get property expression value type - this should not happen", e);
}
Class<?> formattedType = getFormattedObjectType(formatter);
selectableValue = typeConverter.convert(selectableValue, formattedType);
return formatter.format(selectableValue, LocaleContextHolder.getLocale());
}
public boolean isCollection() {
TypeDescriptor<?> type = TypeDescriptor.valueOf(getValueType());
return type.isCollection() || type.isArray();
Class type;
try {
type = getValueType();
} catch (EvaluationException e) {
throw new IllegalArgumentException("Failed to get property expression value type - this should not happen", e);
}
TypeDescriptor<?> typeDesc = TypeDescriptor.valueOf(type);
return typeDesc.isCollection() || typeDesc.isArray();
}
public String[] getValues() {
@@ -235,7 +261,7 @@ public class Binder<T> {
try {
multiValue = property.getValue(createEvaluationContext());
} catch (EvaluationException e) {
throw new IllegalStateException(e);
throw new IllegalStateException("Failed to get property expression value - this should not happen", e);
}
if (multiValue == null) {
return EMPTY_STRING_ARRAY;
@@ -243,42 +269,47 @@ public class Binder<T> {
TypeDescriptor<?> type = TypeDescriptor.valueOf(multiValue.getClass());
String[] formattedValues;
if (type.isCollection()) {
Collection<?> values = ((Collection<?>)multiValue);
Collection<?> values = ((Collection<?>) multiValue);
formattedValues = (String[]) Array.newInstance(String.class, values.size());
copy(values, formattedValues);
} else if (type.isArray()) {
formattedValues = (String[]) Array.newInstance(String.class, Array.getLength(multiValue));
copy((Iterable<?>) multiValue, formattedValues);
copy((Iterable<?>) multiValue, formattedValues);
} else {
throw new IllegalStateException();
}
return formattedValues;
}
public void setValues(String[] formattedValues) {
Formatter formatter = getFormatter();
public BindingResult setValues(String[] formatted) {
Formatter formatter;
try {
formatter = getFormatter();
} catch (EvaluationException e) {
// could occur the property was not found or is not readable
// TODO probably should not handle all EL failures, only type conversion & property not found?
return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e);
}
Class parsedType = getFormattedObjectType(formatter);
if (parsedType == null) {
parsedType = String.class;
}
Object values = Array.newInstance(parsedType, formattedValues.length);
for (int i = 0; i < formattedValues.length; i++) {
Array.set(values, i, parse(formattedValues[i], formatter));
Object parsed = Array.newInstance(parsedType, formatted.length);
for (int i = 0; i < formatted.length; i++) {
Object parsedValue;
try {
parsedValue = formatter.parse(formatted[i], LocaleContextHolder.getLocale());
} catch (ParseException e) {
return new InvalidFormatResult(property.getExpressionString(), formatted, e);
}
Array.set(parsed, i, parsedValue);
}
setValue(values);
return setValue(parsed, formatted);
}
// internal helpers
private Object parse(String formatted, Formatter formatter) {
try {
return formatter.parse(formatted, LocaleContextHolder.getLocale());
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid format " + formatted, e);
}
}
private Formatter getFormatter() {
private Formatter getFormatter() throws EvaluationException {
if (formatter != null) {
return formatter;
} else {
@@ -299,21 +330,12 @@ public class Binder<T> {
}
}
private Class<?> getValueType() {
try {
return property.getValueType(createEvaluationContext());
} catch (EvaluationException e) {
throw new IllegalStateException(e);
}
private Class<?> getValueType() throws EvaluationException {
return property.getValueType(createEvaluationContext());
}
private Annotation[] getAnnotations() {
try {
return property.getValueTypeDescriptor(
createEvaluationContext()).getAnnotations();
} catch (EvaluationException e) {
throw new IllegalStateException(e);
}
private Annotation[] getAnnotations() throws EvaluationException {
return property.getValueTypeDescriptor(createEvaluationContext()).getAnnotations();
}
private void copy(Iterable<?> values, String[] formattedValues) {
@@ -323,15 +345,16 @@ public class Binder<T> {
i++;
}
}
private void setValue(Object values) {
private BindingResult setValue(Object parsed, Object formatted) {
try {
property.setValue(createEvaluationContext(), values);
} catch (ExpressionException e) {
throw new IllegalArgumentException(e);
property.setValue(createEvaluationContext(), parsed);
return new SuccessResult(property.getExpressionString(), formatted);
} catch (EvaluationException e) {
return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e);
}
}
}
private EvaluationContext createEvaluationContext() {
@@ -341,7 +364,7 @@ public class Binder<T> {
context.setTypeConverter(new StandardTypeConverter(typeConverter));
return context;
}
private Class getAnnotationType(AnnotationFormatterFactory factory) {
Class classToIntrospect = factory.getClass();
while (classToIntrospect != null) {
@@ -356,10 +379,11 @@ public class Binder<T> {
}
classToIntrospect = classToIntrospect.getSuperclass();
}
throw new IllegalArgumentException("Unable to extract Annotation type A argument from AnnotationFormatterFactory ["
+ factory.getClass().getName() + "]; does the factory parameterize the <A> generic type?");
throw new IllegalArgumentException(
"Unable to extract Annotation type A argument from AnnotationFormatterFactory ["
+ factory.getClass().getName() + "]; does the factory parameterize the <A> generic type?");
}
private Class getFormattedObjectType(Formatter formatter) {
// TODO consider caching this info
Class classToIntrospect = formatter.getClass();
@@ -377,7 +401,7 @@ public class Binder<T> {
}
return null;
}
private Class getParameterClass(Type parameterType, Class converterClass) {
if (parameterType instanceof TypeVariable) {
parameterType = GenericTypeResolver.resolveTypeVariable((TypeVariable) parameterType, converterClass);
@@ -392,7 +416,7 @@ public class Binder<T> {
static class SimpleAnnotationFormatterFactory implements AnnotationFormatterFactory {
private Formatter formatter;
public SimpleAnnotationFormatterFactory(Formatter formatter) {
this.formatter = formatter;
}
@@ -400,6 +424,115 @@ public class Binder<T> {
public Formatter getFormatter(Annotation annotation) {
return formatter;
}
}
static class InvalidFormatResult implements BindingResult {
private String property;
private Object formatted;
private ParseException e;
public InvalidFormatResult(String property, Object formatted, ParseException e) {
this.property = property;
this.formatted = formatted;
this.e = e;
}
public String getProperty() {
return property;
}
public boolean isError() {
return true;
}
public String getErrorCode() {
return "invalidFormat";
}
public Throwable getErrorCause() {
return e;
}
public Object getUserValue() {
return formatted;
}
}
static class ExpressionEvaluationErrorResult implements BindingResult {
private String property;
private Object formatted;
private EvaluationException e;
public ExpressionEvaluationErrorResult(String property, Object formatted, EvaluationException e) {
this.property = property;
this.formatted = formatted;
this.e = e;
}
public String getProperty() {
return property;
}
public boolean isError() {
return true;
}
public String getErrorCode() {
if (e.getMessage().startsWith("EL1034E")) {
return "typeConversionFailure";
} else if (e.getMessage().startsWith("EL1008E")) {
return "propertyNotFound";
} else {
// TODO return more specific code based on underlying EvaluationException error code
return "couldNotSetValue";
}
}
public Throwable getErrorCause() {
return e;
}
public Object getUserValue() {
return formatted;
}
}
static class SuccessResult implements BindingResult {
private String property;
private Object formatted;
public SuccessResult(String property, Object formatted) {
this.property = property;
this.formatted = formatted;
}
public String getProperty() {
return property;
}
public boolean isError() {
return false;
}
public String getErrorCode() {
return null;
}
public Throwable getErrorCause() {
return null;
}
public Object getUserValue() {
return formatted;
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ui.binding;
/**
* A binding between a user interface element and a model property.
* @author Keith Donald
@@ -32,7 +33,7 @@ public interface Binding {
* Sets the model property value a from user-entered value.
* @param formatted the value entered by the user
*/
void setValue(String formatted);
BindingResult setValue(String formatted);
/**
* Formats a candidate model property value for display in the user interface.
@@ -59,6 +60,6 @@ public interface Binding {
* When a collection binding, sets the model property values a from user-entered/selected values.
* @param formattedValues the values entered by the user
*/
void setValues(String[] formattedValues);
BindingResult setValues(String[] formatted);
}

View File

@@ -0,0 +1,52 @@
/*
* 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;
/**
* A data binding result.
*
* @author Keith Donald
*/
public interface BindingResult {
/**
* The name of the model property associated with this binding result.
*/
String getProperty();
/**
* Indicates if this result is an error result.
*/
boolean isError();
/**
* If an error result, the error code; for example, "invalidFormat", "propertyNotFound", or "evaluationException".
*/
String getErrorCode();
/**
* If an error result, the cause of the error.
* @return the cause, or <code>null</code> if this is not an error
*/
Throwable getErrorCause();
/**
* The raw user-entered value for which binding was attempted.
* If not an error result, this value was successfully bound to the model.
*/
Object getUserValue();
}

View File

@@ -0,0 +1,82 @@
/*
* 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;
import java.util.ArrayList;
import java.util.List;
/**
* Holds a user-entered value to bind to a model property.
* @author Keith Donald
* @see Binder#bind(List).
*/
public class UserValue {
private String property;
private Object value;
/**
* Create a new user value
* @param property the property associated with the value
* @param value the actual user-entered value
*/
public UserValue(String property, Object value) {
this.property = property;
this.value = value;
}
/**
* The property the user-entered value should bind to.
*/
public String getProperty() {
return property;
}
/**
* The actual user-entered value.
*/
public Object getValue() {
return value;
}
/**
* Is the user-entered value a String?
*/
public boolean isString() {
return value instanceof String;
}
/**
* Is the user-entered value a String[]?
*/
public boolean isStringArray() {
return value instanceof String[];
}
/**
* Creates a new UserValue list with a single element.
* @param property the property
* @param value the actual user-entered value
* @return the singleton user value list
*/
public static List<UserValue> singleton(String property, Object value) {
List<UserValue> values = new ArrayList<UserValue>(1);
values.add(new UserValue(property, value));
return values;
}
}