|
|
|
|
@ -22,11 +22,12 @@ import java.lang.reflect.TypeVariable;
@@ -22,11 +22,12 @@ import java.lang.reflect.TypeVariable;
|
|
|
|
|
import java.text.ParseException; |
|
|
|
|
import java.util.ArrayList; |
|
|
|
|
import java.util.Collection; |
|
|
|
|
import java.util.HashMap; |
|
|
|
|
import java.util.Iterator; |
|
|
|
|
import java.util.LinkedHashSet; |
|
|
|
|
import java.util.List; |
|
|
|
|
import java.util.Locale; |
|
|
|
|
import java.util.Map; |
|
|
|
|
import java.util.Set; |
|
|
|
|
|
|
|
|
|
import org.springframework.context.MessageSource; |
|
|
|
|
import org.springframework.context.expression.MapAccessor; |
|
|
|
|
@ -55,6 +56,8 @@ import org.springframework.ui.binding.Binder;
@@ -55,6 +56,8 @@ import org.springframework.ui.binding.Binder;
|
|
|
|
|
import org.springframework.ui.binding.Binding; |
|
|
|
|
import org.springframework.ui.binding.BindingResult; |
|
|
|
|
import org.springframework.ui.binding.BindingResults; |
|
|
|
|
import org.springframework.ui.binding.MissingSourceValuesException; |
|
|
|
|
import org.springframework.ui.binding.NoSuchBindingException; |
|
|
|
|
import org.springframework.ui.format.AnnotationFormatterFactory; |
|
|
|
|
import org.springframework.ui.format.Formatter; |
|
|
|
|
import org.springframework.ui.message.MessageBuilder; |
|
|
|
|
@ -65,10 +68,11 @@ import org.springframework.util.Assert;
@@ -65,10 +68,11 @@ import org.springframework.util.Assert;
|
|
|
|
|
* A generic {@link Binder binder} suitable for use in most environments. |
|
|
|
|
* @author Keith Donald |
|
|
|
|
* @since 3.0 |
|
|
|
|
* @see #configureBinding(BindingConfiguration) |
|
|
|
|
* @see #addBinding(String) |
|
|
|
|
* @see #registerFormatter(Class, Formatter) |
|
|
|
|
* @see #registerFormatterFactory(AnnotationFormatterFactory) |
|
|
|
|
* @see #setFormatterRegistry(FormatterRegistry) |
|
|
|
|
* @see #setMessageSource(MessageSource) |
|
|
|
|
* @see #setStrict(boolean) |
|
|
|
|
* @see #setTypeConverter(TypeConverter) |
|
|
|
|
* @see #bind(Map) |
|
|
|
|
*/ |
|
|
|
|
@ -79,7 +83,7 @@ public class GenericBinder implements Binder {
@@ -79,7 +83,7 @@ public class GenericBinder implements Binder {
|
|
|
|
|
|
|
|
|
|
private Object model; |
|
|
|
|
|
|
|
|
|
private Map<String, Binding> bindings; |
|
|
|
|
private Set<BindingFactory> bindingFactories; |
|
|
|
|
|
|
|
|
|
private FormatterRegistry formatterRegistry = new GenericFormatterRegistry(); |
|
|
|
|
|
|
|
|
|
@ -87,8 +91,6 @@ public class GenericBinder implements Binder {
@@ -87,8 +91,6 @@ public class GenericBinder implements Binder {
|
|
|
|
|
|
|
|
|
|
private TypeConverter typeConverter; |
|
|
|
|
|
|
|
|
|
private boolean strict = false; |
|
|
|
|
|
|
|
|
|
private static Formatter defaultFormatter = new DefaultFormatter(); |
|
|
|
|
|
|
|
|
|
private MessageSource messageSource; |
|
|
|
|
@ -98,36 +100,57 @@ public class GenericBinder implements Binder {
@@ -98,36 +100,57 @@ public class GenericBinder implements Binder {
|
|
|
|
|
* @param model the model object containing properties this binder will bind to |
|
|
|
|
*/ |
|
|
|
|
public GenericBinder(Object model) { |
|
|
|
|
Assert.notNull(model, "The model Object is required"); |
|
|
|
|
Assert.notNull(model, "The model to bind to is required"); |
|
|
|
|
this.model = model; |
|
|
|
|
bindings = new HashMap<String, Binding>(); |
|
|
|
|
bindingFactories = new LinkedHashSet<BindingFactory>(); |
|
|
|
|
int parserConfig = SpelExpressionParserConfiguration.CreateListsOnAttemptToIndexIntoNull |
|
|
|
|
| SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize; |
|
|
|
|
expressionParser = new SpelExpressionParser(parserConfig); |
|
|
|
|
typeConverter = new DefaultTypeConverter(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Object getModel() { |
|
|
|
|
return model; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Is this binder strict? |
|
|
|
|
* A strict binder requires all bindings to be registered explicitly using {@link #configureBinding(BindingConfiguration)}. |
|
|
|
|
* Add a binding to a model property. |
|
|
|
|
* The property may be a path to a member property like "name", or a nested property like "address.city" or "addresses.city". |
|
|
|
|
* If the property path is nested and traverses a collection or Map, do not use indexes. |
|
|
|
|
* The property path should express the model-class property structure like addresses.city, not an object structure like addresses[0].city. |
|
|
|
|
* Examples: |
|
|
|
|
* <pre> |
|
|
|
|
* name - bind to property 'name' |
|
|
|
|
* addresses - bind to property 'addresses', presumably a List<Address> e.g. allowing property expressions like addresses={ 12345 Macy Lane, 1977 Bel Aire Estates } and addresses[0]=12345 Macy Lane |
|
|
|
|
* addresses.city - bind to property 'addresses.city', for all indexed addresses in the collection e.g. allowing property expressions like addresses[0].city=Melbourne |
|
|
|
|
* address.city - bind to property 'address.city' |
|
|
|
|
* favoriteFoodByFoodGroup - bind to property 'favoriteFoodByFoodGroup', presumably a Map<FoodGroup, Food>; e.g. allowing favoriteFoodByFoodGroup={ DAIRY=Milk, MEAT=Steak } and favoriteFoodByFoodGroup['DAIRY']=Milk |
|
|
|
|
* favoriteFoodByFoodGroup.name - bind to property 'favoriteFoodByFoodGroup.name', for all keyed Foods in the map; e.g. allowing favoriteFoodByFoodGroup['DAIRY'].name=Milk |
|
|
|
|
* </pre> |
|
|
|
|
* @param propertyPath the model property path |
|
|
|
|
* @return a BindingConfiguration object, allowing additional configuration of the newly added binding |
|
|
|
|
* @throws IllegalArgumentException if no such property path exists on the model |
|
|
|
|
*/ |
|
|
|
|
public boolean isStrict() { |
|
|
|
|
return strict; |
|
|
|
|
public BindingConfiguration addBinding(String propertyPath) { |
|
|
|
|
DefaultBindingConfiguration configuration = new DefaultBindingConfiguration(propertyPath); |
|
|
|
|
bindingFactories.add(new BindingFactory(configuration)); |
|
|
|
|
return configuration; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Configures if this binder is <i>strict</i>. |
|
|
|
|
* A strict binder requires all bindings to be registered explicitly using {@link #configureBinding(BindingConfiguration)}. |
|
|
|
|
* An <i>optimistic</i> binder will implicitly create bindings as required to support {@link #bind(UserValues)} operations. |
|
|
|
|
* Default is optimistic. |
|
|
|
|
* @param strict strict binder status |
|
|
|
|
* Register a Formatter to format the model properties of a specific property type. |
|
|
|
|
* Convenience method that calls {@link FormatterRegistry#add(Class, Formatter)} internally. |
|
|
|
|
* The type may be a marker annotation type; if so, the Formatter will be used on properties having that marker annotation. |
|
|
|
|
* @param propertyType the model property type |
|
|
|
|
* @param formatter the formatter |
|
|
|
|
*/ |
|
|
|
|
public void registerFormatter(Class<?> propertyType, Formatter<?> formatter) { |
|
|
|
|
formatterRegistry.add(propertyType, formatter); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Register a FormatterFactory that creates Formatter instances as required to format model properties annotated with a specific annotation. |
|
|
|
|
* Convenience method that calls {@link FormatterRegistry#add(AnnotationFormatterFactory)} internally. |
|
|
|
|
* @param factory the formatter factory |
|
|
|
|
*/ |
|
|
|
|
public void setStrict(boolean strict) { |
|
|
|
|
this.strict = strict; |
|
|
|
|
public void registerFormatterFactory(AnnotationFormatterFactory<?, ?> factory) { |
|
|
|
|
formatterRegistry.add(factory); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
@ -148,7 +171,7 @@ public class GenericBinder implements Binder {
@@ -148,7 +171,7 @@ public class GenericBinder implements Binder {
|
|
|
|
|
Assert.notNull(messageSource, "The MessageSource is required"); |
|
|
|
|
this.messageSource = messageSource; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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. |
|
|
|
|
@ -161,64 +184,31 @@ public class GenericBinder implements Binder {
@@ -161,64 +184,31 @@ public class GenericBinder implements Binder {
|
|
|
|
|
this.typeConverter = typeConverter; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Configures a new binding on this binder. |
|
|
|
|
* @param configuration the binding configuration |
|
|
|
|
* @return the new binding created from the configuration provided |
|
|
|
|
*/ |
|
|
|
|
public Binding configureBinding(BindingConfiguration configuration) { |
|
|
|
|
Binding binding; |
|
|
|
|
try { |
|
|
|
|
// TODO should probably only allow binding to be created if property exists on model
|
|
|
|
|
binding = new BindingImpl(configuration); |
|
|
|
|
} catch (org.springframework.expression.ParseException e) { |
|
|
|
|
throw new IllegalArgumentException(e); |
|
|
|
|
} |
|
|
|
|
bindings.put(configuration.getProperty(), binding); |
|
|
|
|
return binding; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Register a Formatter to format the model properties of a specific property type. |
|
|
|
|
* Convenience method that calls {@link FormatterRegistry#add(Class, Formatter)} internally. |
|
|
|
|
* The type may be a marker annotation type; if so, the Formatter will be used on properties having that marker annotation. |
|
|
|
|
* @param propertyType the model property type |
|
|
|
|
* @param formatter the formatter |
|
|
|
|
*/ |
|
|
|
|
public void registerFormatter(Class<?> propertyType, Formatter<?> formatter) { |
|
|
|
|
formatterRegistry.add(propertyType, formatter); |
|
|
|
|
} |
|
|
|
|
// implementing BindingFactory
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Register a FormatterFactory that creates Formatter instances as required to format model properties annotated with a specific annotation. |
|
|
|
|
* Convenience method that calls {@link FormatterRegistry#add(AnnotationFormatterFactory)} internally. |
|
|
|
|
* @param factory the formatter factory |
|
|
|
|
*/ |
|
|
|
|
public void registerFormatterFactory(AnnotationFormatterFactory<?, ?> factory) { |
|
|
|
|
formatterRegistry.add(factory); |
|
|
|
|
public Object getModel() { |
|
|
|
|
return model; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Binding getBinding(String property) { |
|
|
|
|
Binding binding = bindings.get(property); |
|
|
|
|
if (binding == null && !strict) { |
|
|
|
|
return configureBinding(new BindingConfiguration(property, null)); |
|
|
|
|
} else { |
|
|
|
|
return binding; |
|
|
|
|
for (BindingFactory factory: bindingFactories) { |
|
|
|
|
if (factory.hasBindingFor(property)) { |
|
|
|
|
return factory.getBinding(property); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
throw new NoSuchBindingException(property); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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()) { |
|
|
|
|
String property = sourceValue.getKey(); |
|
|
|
|
Object value = sourceValue.getValue(); |
|
|
|
|
BindingImpl binding = (BindingImpl) getBinding(property); |
|
|
|
|
if (binding != null) { |
|
|
|
|
results.add(binding.setValue(value)); |
|
|
|
|
} else { |
|
|
|
|
results.add(new NoSuchBindingResult(property, value)); |
|
|
|
|
} |
|
|
|
|
results.add(getBinding(property).setValue(value)); |
|
|
|
|
} |
|
|
|
|
return results; |
|
|
|
|
} |
|
|
|
|
@ -227,9 +217,9 @@ public class GenericBinder implements Binder {
@@ -227,9 +217,9 @@ 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. |
|
|
|
|
- * 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. |
|
|
|
|
* This hook allows the binder to pre-process the source 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 |
|
|
|
|
*/ |
|
|
|
|
@ -239,64 +229,84 @@ public class GenericBinder implements Binder {
@@ -239,64 +229,84 @@ public class GenericBinder implements Binder {
|
|
|
|
|
|
|
|
|
|
// internal helpers
|
|
|
|
|
|
|
|
|
|
static class ArrayListBindingResults implements BindingResults { |
|
|
|
|
|
|
|
|
|
private List<BindingResult> results; |
|
|
|
|
|
|
|
|
|
public ArrayListBindingResults() { |
|
|
|
|
results = new ArrayList<BindingResult>(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public ArrayListBindingResults(int size) { |
|
|
|
|
results = new ArrayList<BindingResult>(size); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public void add(BindingResult result) { |
|
|
|
|
results.add(result); |
|
|
|
|
private void checkRequired(Map<String, ? extends Object> sourceValues) { |
|
|
|
|
List<String> missingRequired = new ArrayList<String>(); |
|
|
|
|
for (BindingFactory factory : bindingFactories) { |
|
|
|
|
if (factory.configuration.isRequired()) { |
|
|
|
|
boolean found = false; |
|
|
|
|
for (String property : sourceValues.keySet()) { |
|
|
|
|
if (factory.hasBindingFor(property)) { |
|
|
|
|
found = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if (!found) { |
|
|
|
|
missingRequired.add(factory.getPropertyPath()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// implementing Iterable
|
|
|
|
|
|
|
|
|
|
public Iterator<BindingResult> iterator() { |
|
|
|
|
return results.iterator(); |
|
|
|
|
if (!missingRequired.isEmpty()) { |
|
|
|
|
throw new MissingSourceValuesException(missingRequired, sourceValues); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private EvaluationContext createEvaluationContext() { |
|
|
|
|
StandardEvaluationContext context = new StandardEvaluationContext(); |
|
|
|
|
context.setRootObject(model); |
|
|
|
|
context.addPropertyAccessor(new MapAccessor()); |
|
|
|
|
context.setTypeConverter(new StandardTypeConverter(typeConverter)); |
|
|
|
|
return context; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// implementing BindingResults
|
|
|
|
|
class BindingFactory { |
|
|
|
|
|
|
|
|
|
public BindingResults successes() { |
|
|
|
|
ArrayListBindingResults results = new ArrayListBindingResults(); |
|
|
|
|
for (BindingResult result : this) { |
|
|
|
|
if (!result.isFailure()) { |
|
|
|
|
results.add(result); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return results; |
|
|
|
|
private DefaultBindingConfiguration configuration; |
|
|
|
|
|
|
|
|
|
public BindingFactory(DefaultBindingConfiguration configuration) { |
|
|
|
|
this.configuration = configuration; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public BindingResults failures() { |
|
|
|
|
ArrayListBindingResults results = new ArrayListBindingResults(); |
|
|
|
|
for (BindingResult result : this) { |
|
|
|
|
if (result.isFailure()) { |
|
|
|
|
results.add(result); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return results; |
|
|
|
|
public String getPropertyPath() { |
|
|
|
|
return configuration.getPropertyPath(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public BindingResult get(int index) { |
|
|
|
|
return results.get(index); |
|
|
|
|
public boolean hasBindingFor(String property) { |
|
|
|
|
String propertyPath = propertyPath(property); |
|
|
|
|
return configuration.getPropertyPath().equals(propertyPath); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public List<String> properties() { |
|
|
|
|
List<String> properties = new ArrayList<String>(results.size()); |
|
|
|
|
for (BindingResult result : this) { |
|
|
|
|
properties.add(result.getProperty()); |
|
|
|
|
public Binding getBinding(String property) { |
|
|
|
|
Expression propertyExpression; |
|
|
|
|
try { |
|
|
|
|
propertyExpression = expressionParser.parseExpression(property); |
|
|
|
|
} catch (org.springframework.expression.ParseException e) { |
|
|
|
|
throw new IllegalArgumentException( |
|
|
|
|
"Unable to get model binding; cannot parse property expression from property string [" |
|
|
|
|
+ property + "]", e); |
|
|
|
|
} |
|
|
|
|
return properties; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public int size() { |
|
|
|
|
return results.size(); |
|
|
|
|
try { |
|
|
|
|
propertyExpression.getValueType(createEvaluationContext()); |
|
|
|
|
} catch (EvaluationException e) { |
|
|
|
|
throw new IllegalArgumentException("Unable to get model binding; cannot access property '" |
|
|
|
|
+ propertyExpression.getExpressionString() + "'", e); |
|
|
|
|
} |
|
|
|
|
return new BindingImpl(propertyExpression, configuration.getFormatter()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private String propertyPath(String property) { |
|
|
|
|
StringBuilder sb = new StringBuilder(property); |
|
|
|
|
int searchIndex = 0; |
|
|
|
|
while (searchIndex != -1) { |
|
|
|
|
int keyStart = sb.indexOf("[", searchIndex); |
|
|
|
|
searchIndex = -1; |
|
|
|
|
if (keyStart != -1) { |
|
|
|
|
int keyEnd = sb.indexOf("]", keyStart + 1); |
|
|
|
|
if (keyEnd != -1) { |
|
|
|
|
sb.delete(keyStart, keyEnd + 1); |
|
|
|
|
searchIndex = keyEnd + 1; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return sb.toString(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
@ -307,13 +317,17 @@ public class GenericBinder implements Binder {
@@ -307,13 +317,17 @@ public class GenericBinder implements Binder {
|
|
|
|
|
|
|
|
|
|
private Formatter formatter; |
|
|
|
|
|
|
|
|
|
public BindingImpl(BindingConfiguration config) throws org.springframework.expression.ParseException { |
|
|
|
|
property = expressionParser.parseExpression(config.getProperty()); |
|
|
|
|
formatter = config.getFormatter(); |
|
|
|
|
public BindingImpl(Expression property, Formatter formatter) { |
|
|
|
|
this.property = property; |
|
|
|
|
this.formatter = formatter; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// implementing Binding
|
|
|
|
|
|
|
|
|
|
public String getProperty() { |
|
|
|
|
return property.getExpressionString(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getValue() { |
|
|
|
|
Object value; |
|
|
|
|
try { |
|
|
|
|
@ -335,13 +349,7 @@ public class GenericBinder implements Binder {
@@ -335,13 +349,7 @@ public class GenericBinder implements Binder {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String format(Object selectableValue) { |
|
|
|
|
Formatter formatter; |
|
|
|
|
try { |
|
|
|
|
formatter = getFormatter(); |
|
|
|
|
} catch (EvaluationException e) { |
|
|
|
|
throw new IllegalStateException( |
|
|
|
|
"Failed to get property expression value type - this should not happen", e); |
|
|
|
|
} |
|
|
|
|
Formatter formatter = getFormatter(); |
|
|
|
|
Class<?> formattedType = getFormattedObjectType(formatter); |
|
|
|
|
selectableValue = typeConverter.convert(selectableValue, formattedType); |
|
|
|
|
return formatter.format(selectableValue, LocaleContextHolder.getLocale()); |
|
|
|
|
@ -394,32 +402,17 @@ public class GenericBinder implements Binder {
@@ -394,32 +402,17 @@ public class GenericBinder implements Binder {
|
|
|
|
|
// internal helpers
|
|
|
|
|
|
|
|
|
|
private BindingResult setStringValue(String formatted) { |
|
|
|
|
Formatter formatter; |
|
|
|
|
try { |
|
|
|
|
formatter = getFormatter(); |
|
|
|
|
} catch (EvaluationException e) { |
|
|
|
|
// could occur if 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()); |
|
|
|
|
parsed = getFormatter().parse(formatted, LocaleContextHolder.getLocale()); |
|
|
|
|
} catch (ParseException e) { |
|
|
|
|
return new InvalidFormatResult(property.getExpressionString(), formatted, e); |
|
|
|
|
return new InvalidFormat(property.getExpressionString(), formatted, e); |
|
|
|
|
} |
|
|
|
|
return setValue(parsed, formatted); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private BindingResult setStringValues(String[] formatted) { |
|
|
|
|
Formatter formatter; |
|
|
|
|
try { |
|
|
|
|
formatter = getFormatter(); |
|
|
|
|
} catch (EvaluationException e) { |
|
|
|
|
// could occur if 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); |
|
|
|
|
} |
|
|
|
|
Formatter formatter = getFormatter(); |
|
|
|
|
Class parsedType = getFormattedObjectType(formatter); |
|
|
|
|
if (parsedType == null) { |
|
|
|
|
parsedType = String.class; |
|
|
|
|
@ -430,7 +423,7 @@ public class GenericBinder implements Binder {
@@ -430,7 +423,7 @@ public class GenericBinder implements Binder {
|
|
|
|
|
try { |
|
|
|
|
parsedValue = formatter.parse(formatted[i], LocaleContextHolder.getLocale()); |
|
|
|
|
} catch (ParseException e) { |
|
|
|
|
return new InvalidFormatResult(property.getExpressionString(), formatted, e); |
|
|
|
|
return new InvalidFormat(property.getExpressionString(), formatted, e); |
|
|
|
|
} |
|
|
|
|
Array.set(parsed, i, parsedValue); |
|
|
|
|
} |
|
|
|
|
@ -441,12 +434,18 @@ public class GenericBinder implements Binder {
@@ -441,12 +434,18 @@ public class GenericBinder implements Binder {
|
|
|
|
|
return setValue(value, value); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private Formatter getFormatter() throws EvaluationException { |
|
|
|
|
private Formatter getFormatter() { |
|
|
|
|
if (formatter != null) { |
|
|
|
|
return formatter; |
|
|
|
|
} else { |
|
|
|
|
Formatter<?> formatter = formatterRegistry.getFormatter(property |
|
|
|
|
.getValueTypeDescriptor(createEvaluationContext())); |
|
|
|
|
TypeDescriptor type; |
|
|
|
|
try { |
|
|
|
|
type = property.getValueTypeDescriptor(createEvaluationContext()); |
|
|
|
|
} catch (EvaluationException e) { |
|
|
|
|
throw new IllegalArgumentException( |
|
|
|
|
"Failed to get property expression value type descriptor - this should not happen", e); |
|
|
|
|
} |
|
|
|
|
Formatter formatter = formatterRegistry.getFormatter(type); |
|
|
|
|
return formatter != null ? formatter : defaultFormatter; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -462,49 +461,41 @@ public class GenericBinder implements Binder {
@@ -462,49 +461,41 @@ public class GenericBinder implements Binder {
|
|
|
|
|
private BindingResult setValue(Object parsedValue, Object userValue) { |
|
|
|
|
try { |
|
|
|
|
property.setValue(createEvaluationContext(), parsedValue); |
|
|
|
|
return new SuccessResult(property.getExpressionString(), userValue); |
|
|
|
|
return new Success(property.getExpressionString(), userValue); |
|
|
|
|
} catch (EvaluationException e) { |
|
|
|
|
return new ExpressionEvaluationErrorResult(property.getExpressionString(), userValue, e); |
|
|
|
|
return new EvaluationError(property.getExpressionString(), userValue, e); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private EvaluationContext createEvaluationContext() { |
|
|
|
|
StandardEvaluationContext context = new StandardEvaluationContext(); |
|
|
|
|
context.setRootObject(model); |
|
|
|
|
context.addPropertyAccessor(new MapAccessor()); |
|
|
|
|
context.setTypeConverter(new StandardTypeConverter(typeConverter)); |
|
|
|
|
return context; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private Class getFormattedObjectType(Formatter formatter) { |
|
|
|
|
// TODO consider caching this info
|
|
|
|
|
Class classToIntrospect = formatter.getClass(); |
|
|
|
|
while (classToIntrospect != null) { |
|
|
|
|
Type[] genericInterfaces = classToIntrospect.getGenericInterfaces(); |
|
|
|
|
for (Type genericInterface : genericInterfaces) { |
|
|
|
|
if (genericInterface instanceof ParameterizedType) { |
|
|
|
|
ParameterizedType pInterface = (ParameterizedType) genericInterface; |
|
|
|
|
if (Formatter.class.isAssignableFrom((Class) pInterface.getRawType())) { |
|
|
|
|
return getParameterClass(pInterface.getActualTypeArguments()[0], formatter.getClass()); |
|
|
|
|
private Class getFormattedObjectType(Formatter formatter) { |
|
|
|
|
// TODO consider caching this info
|
|
|
|
|
Class classToIntrospect = formatter.getClass(); |
|
|
|
|
while (classToIntrospect != null) { |
|
|
|
|
Type[] genericInterfaces = classToIntrospect.getGenericInterfaces(); |
|
|
|
|
for (Type genericInterface : genericInterfaces) { |
|
|
|
|
if (genericInterface instanceof ParameterizedType) { |
|
|
|
|
ParameterizedType pInterface = (ParameterizedType) genericInterface; |
|
|
|
|
if (Formatter.class.isAssignableFrom((Class) pInterface.getRawType())) { |
|
|
|
|
return getParameterClass(pInterface.getActualTypeArguments()[0], formatter.getClass()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
classToIntrospect = classToIntrospect.getSuperclass(); |
|
|
|
|
} |
|
|
|
|
classToIntrospect = classToIntrospect.getSuperclass(); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private Class getParameterClass(Type parameterType, Class converterClass) { |
|
|
|
|
if (parameterType instanceof TypeVariable) { |
|
|
|
|
parameterType = GenericTypeResolver.resolveTypeVariable((TypeVariable) parameterType, converterClass); |
|
|
|
|
} |
|
|
|
|
if (parameterType instanceof Class) { |
|
|
|
|
return (Class) parameterType; |
|
|
|
|
private Class getParameterClass(Type parameterType, Class converterClass) { |
|
|
|
|
if (parameterType instanceof TypeVariable) { |
|
|
|
|
parameterType = GenericTypeResolver.resolveTypeVariable((TypeVariable) parameterType, converterClass); |
|
|
|
|
} |
|
|
|
|
if (parameterType instanceof Class) { |
|
|
|
|
return (Class) parameterType; |
|
|
|
|
} |
|
|
|
|
throw new IllegalArgumentException("Unable to obtain the java.lang.Class for parameterType [" + parameterType |
|
|
|
|
+ "] on Formatter [" + converterClass.getName() + "]"); |
|
|
|
|
} |
|
|
|
|
throw new IllegalArgumentException("Unable to obtain the java.lang.Class for parameterType [" + parameterType |
|
|
|
|
+ "] on Formatter [" + converterClass.getName() + "]"); |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static class DefaultFormatter implements Formatter { |
|
|
|
|
@ -525,15 +516,80 @@ public class GenericBinder implements Binder {
@@ -525,15 +516,80 @@ public class GenericBinder implements Binder {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class NoSuchBindingResult implements BindingResult { |
|
|
|
|
|
|
|
|
|
static class ArrayListBindingResults implements BindingResults { |
|
|
|
|
|
|
|
|
|
private List<BindingResult> results; |
|
|
|
|
|
|
|
|
|
public ArrayListBindingResults() { |
|
|
|
|
results = new ArrayList<BindingResult>(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public ArrayListBindingResults(int size) { |
|
|
|
|
results = new ArrayList<BindingResult>(size); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public void add(BindingResult result) { |
|
|
|
|
results.add(result); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// implementing Iterable
|
|
|
|
|
|
|
|
|
|
public Iterator<BindingResult> iterator() { |
|
|
|
|
return results.iterator(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// implementing BindingResults
|
|
|
|
|
|
|
|
|
|
public BindingResults successes() { |
|
|
|
|
ArrayListBindingResults results = new ArrayListBindingResults(); |
|
|
|
|
for (BindingResult result : this) { |
|
|
|
|
if (!result.isFailure()) { |
|
|
|
|
results.add(result); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return results; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public BindingResults failures() { |
|
|
|
|
ArrayListBindingResults results = new ArrayListBindingResults(); |
|
|
|
|
for (BindingResult result : this) { |
|
|
|
|
if (result.isFailure()) { |
|
|
|
|
results.add(result); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return results; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public BindingResult get(int index) { |
|
|
|
|
return results.get(index); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public List<String> properties() { |
|
|
|
|
List<String> properties = new ArrayList<String>(results.size()); |
|
|
|
|
for (BindingResult result : this) { |
|
|
|
|
properties.add(result.getProperty()); |
|
|
|
|
} |
|
|
|
|
return properties; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public int size() { |
|
|
|
|
return results.size(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class InvalidFormat implements BindingResult { |
|
|
|
|
|
|
|
|
|
private String property; |
|
|
|
|
|
|
|
|
|
private Object sourceValue; |
|
|
|
|
|
|
|
|
|
public NoSuchBindingResult(String property, Object sourceValue) { |
|
|
|
|
private ParseException cause; |
|
|
|
|
|
|
|
|
|
public InvalidFormat(String property, Object sourceValue, ParseException cause) { |
|
|
|
|
this.property = property; |
|
|
|
|
this.sourceValue = sourceValue; |
|
|
|
|
this.cause = cause; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getProperty() { |
|
|
|
|
@ -551,11 +607,11 @@ public class GenericBinder implements Binder {
@@ -551,11 +607,11 @@ public class GenericBinder implements Binder {
|
|
|
|
|
public Alert getAlert() { |
|
|
|
|
return new AbstractAlert() { |
|
|
|
|
public String getCode() { |
|
|
|
|
return "noSuchBinding"; |
|
|
|
|
return "invalidFormat"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Severity getSeverity() { |
|
|
|
|
return Severity.WARNING; |
|
|
|
|
return Severity.ERROR; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getMessage() { |
|
|
|
|
@ -563,26 +619,24 @@ public class GenericBinder implements Binder {
@@ -563,26 +619,24 @@ public class GenericBinder implements Binder {
|
|
|
|
|
builder.code(getCode()); |
|
|
|
|
builder.arg("label", new ResolvableArgument(property)); |
|
|
|
|
builder.arg("value", sourceValue); |
|
|
|
|
builder.defaultMessage("Failed to bind to property '" + property |
|
|
|
|
+ "'; no binding has been added for the property"); |
|
|
|
|
builder.arg("errorOffset", cause.getErrorOffset()); |
|
|
|
|
builder.defaultMessage("Failed to bind to property '" + property + "'; the user value " |
|
|
|
|
+ StylerUtils.style(sourceValue) + " has an invalid format and could no be parsed"); |
|
|
|
|
return builder.build(); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class InvalidFormatResult implements BindingResult { |
|
|
|
|
class Success implements BindingResult { |
|
|
|
|
|
|
|
|
|
private String property; |
|
|
|
|
|
|
|
|
|
private Object sourceValue; |
|
|
|
|
|
|
|
|
|
private ParseException cause; |
|
|
|
|
|
|
|
|
|
public InvalidFormatResult(String property, Object sourceValue, ParseException cause) { |
|
|
|
|
public Success(String property, Object sourceValue) { |
|
|
|
|
this.property = property; |
|
|
|
|
this.sourceValue = sourceValue; |
|
|
|
|
this.cause = cause; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getProperty() { |
|
|
|
|
@ -594,34 +648,34 @@ public class GenericBinder implements Binder {
@@ -594,34 +648,34 @@ public class GenericBinder implements Binder {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public boolean isFailure() { |
|
|
|
|
return true; |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Alert getAlert() { |
|
|
|
|
return new AbstractAlert() { |
|
|
|
|
public String getCode() { |
|
|
|
|
return "invalidFormat"; |
|
|
|
|
return "bindSuccess"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Severity getSeverity() { |
|
|
|
|
return Severity.ERROR; |
|
|
|
|
return Severity.INFO; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getMessage() { |
|
|
|
|
MessageBuilder builder = new MessageBuilder(messageSource); |
|
|
|
|
builder.code(getCode()); |
|
|
|
|
builder.code("bindSuccess"); |
|
|
|
|
builder.arg("label", new ResolvableArgument(property)); |
|
|
|
|
builder.arg("value", sourceValue); |
|
|
|
|
builder.arg("errorOffset", cause.getErrorOffset()); |
|
|
|
|
builder.defaultMessage("Failed to bind to property '" + property + "'; the user value " |
|
|
|
|
+ StylerUtils.style(sourceValue) + " has an invalid format and could no be parsed"); |
|
|
|
|
builder.defaultMessage("Successfully bound user value " + StylerUtils.style(sourceValue) |
|
|
|
|
+ "to property '" + property + "'"); |
|
|
|
|
return builder.build(); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class ExpressionEvaluationErrorResult implements BindingResult { |
|
|
|
|
class EvaluationError implements BindingResult { |
|
|
|
|
|
|
|
|
|
private String property; |
|
|
|
|
|
|
|
|
|
@ -629,7 +683,7 @@ public class GenericBinder implements Binder {
@@ -629,7 +683,7 @@ public class GenericBinder implements Binder {
|
|
|
|
|
|
|
|
|
|
private EvaluationException cause; |
|
|
|
|
|
|
|
|
|
public ExpressionEvaluationErrorResult(String property, Object sourceValue, EvaluationException cause) { |
|
|
|
|
public EvaluationError(String property, Object sourceValue, EvaluationException cause) { |
|
|
|
|
this.property = property; |
|
|
|
|
this.sourceValue = sourceValue; |
|
|
|
|
this.cause = cause; |
|
|
|
|
@ -653,23 +707,13 @@ public class GenericBinder implements Binder {
@@ -653,23 +707,13 @@ public class GenericBinder implements Binder {
|
|
|
|
|
SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode(); |
|
|
|
|
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { |
|
|
|
|
return "conversionFailed"; |
|
|
|
|
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { |
|
|
|
|
// TODO should probably force property exists before even creating binding
|
|
|
|
|
return "propertyNotFound"; |
|
|
|
|
} else { |
|
|
|
|
return "couldNotSetValue"; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Severity getSeverity() { |
|
|
|
|
SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode(); |
|
|
|
|
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { |
|
|
|
|
return Severity.FATAL; |
|
|
|
|
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { |
|
|
|
|
return Severity.WARNING; |
|
|
|
|
} else { |
|
|
|
|
return Severity.FATAL; |
|
|
|
|
} |
|
|
|
|
return Severity.FATAL; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getMessage() { |
|
|
|
|
@ -691,73 +735,20 @@ public class GenericBinder implements Binder {
@@ -691,73 +735,20 @@ public class GenericBinder implements Binder {
|
|
|
|
|
return builder.build(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) { |
|
|
|
|
MessageBuilder builder = new MessageBuilder(messageSource); |
|
|
|
|
builder.code(getCode()); |
|
|
|
|
builder.arg("label", new ResolvableArgument(property)); |
|
|
|
|
builder.arg("value", sourceValue); |
|
|
|
|
builder.defaultMessage("Failed to bind to property '" + property + "'; no such property exists on model"); |
|
|
|
|
return builder.build(); |
|
|
|
|
} |
|
|
|
|
MessageBuilder builder = new MessageBuilder(messageSource); |
|
|
|
|
builder.code("couldNotSetValue"); |
|
|
|
|
builder.arg("label", new ResolvableArgument(property)); |
|
|
|
|
builder.arg("value", sourceValue); |
|
|
|
|
builder.defaultMessage("Failed to bind to property '" + property + "'; reason = " + cause.getLocalizedMessage()); |
|
|
|
|
builder.defaultMessage("Failed to bind to property '" + property + "'; reason = " |
|
|
|
|
+ cause.getLocalizedMessage()); |
|
|
|
|
return builder.build(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class SuccessResult implements BindingResult { |
|
|
|
|
|
|
|
|
|
private String property; |
|
|
|
|
|
|
|
|
|
private Object sourceValue; |
|
|
|
|
|
|
|
|
|
public SuccessResult(String property, Object sourceValue) { |
|
|
|
|
this.property = property; |
|
|
|
|
this.sourceValue = sourceValue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getProperty() { |
|
|
|
|
return property; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Object getSourceValue() { |
|
|
|
|
return sourceValue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public boolean isFailure() { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Alert getAlert() { |
|
|
|
|
return new AbstractAlert() { |
|
|
|
|
public String getCode() { |
|
|
|
|
return "bindSuccess"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public Severity getSeverity() { |
|
|
|
|
return Severity.INFO; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public String getMessage() { |
|
|
|
|
MessageBuilder builder = new MessageBuilder(messageSource); |
|
|
|
|
builder.code("bindSuccess"); |
|
|
|
|
builder.arg("label", new ResolvableArgument(property)); |
|
|
|
|
builder.arg("value", sourceValue); |
|
|
|
|
builder.defaultMessage("Successfully bound user value " + StylerUtils.style(sourceValue) + "to property '" + property + "'"); |
|
|
|
|
return builder.build(); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static abstract class AbstractAlert implements Alert { |
|
|
|
|
public String toString() { |
|
|
|
|
return getCode() + " - " + getMessage(); |
|
|
|
|
|