numerous binding enhancements; removed lazy binding for time being

This commit is contained in:
Keith Donald
2009-07-08 21:43:35 +00:00
parent 5ff6191d72
commit 2bbf827d57
21 changed files with 805 additions and 757 deletions

View File

@@ -23,25 +23,14 @@ import java.util.Map;
* @since 3.0
* @see #bind(Map)
*/
public interface Binder {
/**
* The model object this binder binds to.
* @return the model object
*/
Object getModel();
/**
* Returns the binding for the property.
* @param property the property path
* @return the binding
*/
Binding getBinding(String property);
public interface Binder extends BindingFactory {
/**
* 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
* @return the results of the binding operation
* @throws MissingSourceValuesException when the sourceValues Map is missing entries for required bindings
*/
BindingResults bind(Map<String, ? extends Object> sourceValues);

View File

@@ -23,14 +23,21 @@ package org.springframework.ui.binding;
public interface Binding {
/**
* The formatted value to display in the user interface.
* The name of the bound model property.
*/
String getProperty();
/**
* The formatted property value to display in the user interface.
*/
String getValue();
/**
* Set the property associated with this binding to the value provided.
* Set the property to the value provided.
* The value may be a formatted String, a formatted String[] if a collection binding, or an Object of a type that can be coersed to the underlying property type.
* @param value the new value to bind
* @return a summary of the result of the binding
* @throws BindException if an unrecoverable exception occurs
*/
BindingResult setValue(Object value);
@@ -58,5 +65,4 @@ public interface Binding {
*/
Class<?> getType();
}

View File

@@ -16,16 +16,21 @@
package org.springframework.ui.binding;
/**
* A factory for model Binders.
* A factory for model property bindings.
* @author Keith Donald
* @since 3.0
*/
public interface BinderFactory {
public interface BindingFactory {
/**
* Get the Binder for the model
* @param model the model
* @return the binder
* The model object for which property bindings may be accessed.
*/
Binder getBinder(Object model);
}
Object getModel();
/**
* 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);
}

View File

@@ -27,7 +27,8 @@ import org.springframework.ui.alert.Alert;
public interface BindingResult {
/**
* The name of the model property associated with this binding result.
* The model property this binding result is for.
* @see Binder#getBinding(String)
*/
String getProperty();

View File

@@ -0,0 +1,56 @@
/*
* 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.List;
import java.util.Map;
/**
* Exception thrown by a Binder when a required source value is missing unexpectedly from the sourceValues map.
* Indicates a client configuration error.
* @author Keith Donald
* @see Binder#bind(Map)
*/
public class MissingSourceValuesException extends RuntimeException {
private List<String> missing;
/**
* Creates a new missing source values exeption.
* @param missing
* @param sourceValues
*/
public MissingSourceValuesException(List<String> missing, Map<String, ? extends Object> sourceValues) {
super(getMessage(missing, sourceValues));
this.missing = missing;
}
/**
* The property paths for which source values were missing.
*/
public List<String> getMissing() {
return missing;
}
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();
} else {
return "Missing source values to required propertyPaths " + missingRequired + "; sourceValues map contained " + sourceValues.keySet();
}
}
}

View File

@@ -31,10 +31,4 @@ public @interface Model {
*/
String value() default "";
/**
* Configures strict model binding.
* @see Binder#setStrict(boolean)
*/
boolean strictBinding() default false;
}

View File

@@ -0,0 +1,17 @@
package org.springframework.ui.binding;
/**
* Thrown by a BindingFactory when no binding to a property exists.
* @author Keith Donald
* @see BindingFactory#getBinding(String)
*/
public class NoSuchBindingException extends RuntimeException {
/**
* Creates a new no such binding exception.
* @param property the requested property for which there is no binding
*/
public NoSuchBindingException(String property) {
super("No binding to property '" + property + "' exists");
}
}

View File

@@ -8,33 +8,26 @@ import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.ui.binding.Bound;
import org.springframework.ui.binding.Model;
final class AnnotatedModelBinderConfigurer {
public void configure(GenericBinder binder) {
Class<?> modelClass = binder.getModel().getClass();
Model m = AnnotationUtils.findAnnotation(modelClass, Model.class);
if (m != null) {
binder.setStrict(m.strictBinding());
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(modelClass);
} catch (IntrospectionException e) {
throw new IllegalStateException("Unable to introspect model " + binder.getModel(), e);
}
if (binder.isStrict()) {
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(modelClass);
} catch (IntrospectionException e) {
throw new IllegalStateException("Unable to introspect model " + binder.getModel(), e);
}
// TODO do we have to still flush introspector cache here?
for (PropertyDescriptor prop : beanInfo.getPropertyDescriptors()) {
Method getter = prop.getReadMethod();
Bound b = AnnotationUtils.getAnnotation(getter, Bound.class);
if (b != null) {
// TODO should we wire formatter here if using a format annotation - an optimization?
binder.configureBinding(new BindingConfiguration(prop.getName(), null));
}
// TODO do we have to still flush introspector cache here?
for (PropertyDescriptor prop : beanInfo.getPropertyDescriptors()) {
Method getter = prop.getReadMethod();
Bound b = AnnotationUtils.getAnnotation(getter, Bound.class);
if (b != null) {
// TODO should we wire formatter here if using a format annotation - an optimization?
binder.addBinding(prop.getName());
}
}
}
}
}

View File

@@ -15,43 +15,33 @@
*/
package org.springframework.ui.binding.support;
import org.springframework.ui.binding.Binding;
import org.springframework.ui.format.Formatter;
/**
* Configuration used to create a new {@link Binding} registered with a {@link GenericBinder}.
* A fluent interface for configuring a newly added binding to a property path.
* @author Keith Donald
* @since 3.0
* @see GenericBinder#configureBinding(BindingConfiguration)
* @see GenericBinder#addBinding(String)
*/
public final class BindingConfiguration {
private String property;
private Formatter<?> formatter;
public interface BindingConfiguration {
/**
* Creates a new Binding configuration.
* @param property the property to bind to
* @param formatter the formatter to use to format property values
* Set the Formatter to use to format bound property values.
* If a collection property, the formatter is used to format collection element values.
* Default is null.
*/
public BindingConfiguration(String property, Formatter<?> formatter) {
this.property = property;
this.formatter = formatter;
}
BindingConfiguration formatWith(Formatter<?> formatter);
/**
* The name of the model property to bind to.
* Mark the binding as required.
* A required binding will generate an exception if no sourceValue is provided to a bind invocation.
* This attribute is used to detect client configuration errors.
* It is not intended to be used as a user data validation constraint.
* Examples:
* <pre>
* name=required - will generate an exception if 'name' is not contained in the source values map.
* addresses=required - will generate an exception if 'addresses[n]' is not contained in the source value map for at least one n.
* addresses.city=required - will generate an exception if 'addresses[n].city' is not present in the source values map, for every address[n].
* </pre>
*/
public String getProperty() {
return property;
}
/**
* The Formatter to use to format bound property values.
*/
public Formatter<?> getFormatter() {
return formatter;
}
}
BindingConfiguration required();
}

View File

@@ -0,0 +1,61 @@
/*
* 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;
import org.springframework.ui.format.Formatter;
final class DefaultBindingConfiguration implements BindingConfiguration {
private String propertyPath;
private Formatter<?> formatter;
private boolean required;
/**
* Creates a new Binding configuration.
* @param property the property to bind to
* @param formatter the formatter to use to format property values
*/
public DefaultBindingConfiguration(String propertyPath) {
this.propertyPath = propertyPath;
}
// implementing BindingConfiguration
public BindingConfiguration formatWith(Formatter<?> formatter) {
this.formatter = formatter;
return this;
}
public BindingConfiguration required() {
this.required = true;
return this;
}
public String getPropertyPath() {
return propertyPath;
}
public Formatter<?> getFormatter() {
return formatter;
}
public boolean isRequired() {
return required;
}
}

View File

@@ -26,7 +26,7 @@ import java.util.Map;
* @see #setDefaultPrefix(String)
* @see #setPresentPrefix(String)
*/
class WebBinder extends GenericBinder {
public class WebBinder extends GenericBinder {
private String defaultPrefix = "!";

View File

@@ -1,35 +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;
import org.springframework.ui.binding.Binder;
import org.springframework.ui.binding.BinderFactory;
/**
* Factory for Binders suited for use in web environments.
* TODO - BinderConfiguration objects indexed by model class?
* @author Keith Donald
* @since 3.0
*/
public class WebBinderFactory implements BinderFactory {
public Binder getBinder(Object model) {
WebBinder binder = new WebBinder(model);
new AnnotatedModelBinderConfigurer().configure(binder);
return binder;
}
}

View File

@@ -15,12 +15,15 @@
*/
package org.springframework.ui.lifecycle;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.ui.alert.AlertContext;
import org.springframework.ui.binding.Binder;
import org.springframework.ui.binding.BindingResult;
import org.springframework.ui.binding.BindingResults;
import org.springframework.ui.validation.ValidationFailure;
import org.springframework.ui.validation.Validator;
/**
@@ -58,17 +61,28 @@ public final class BindAndValidateLifecycle {
this.validationDecider = validationDecider;
}
/**
* Execute the bind and validate lifecycle.
* Any bind or validation errors are recorded as alerts against the {@link AlertContext}.
* @param sourceValues the source values to bind and validate
*/
public void execute(Map<String, ? extends Object> sourceValues) {
BindingResults bindingResults = binder.bind(sourceValues);
if (validator != null && validationDecider.shouldValidateAfter(bindingResults)) {
// TODO get validation results
validator.validate(binder.getModel(), bindingResults.successes().properties());
}
List<ValidationFailure> validationFailures = validate(bindingResults);
for (BindingResult result : bindingResults.failures()) {
// TODO - you may want to ignore some alerts like propertyNotFound
alertContext.add(result.getProperty(), result.getAlert());
}
// TODO translate validation results into messages
for (ValidationFailure failure : validationFailures) {
alertContext.add(failure.getProperty(), failure.getAlert());
}
}
private List<ValidationFailure> validate(BindingResults bindingResults) {
if (validator != null && validationDecider.shouldValidateAfter(bindingResults)) {
return validator.validate(binder.getModel(), bindingResults.successes().properties());
} else {
return Collections.emptyList();
}
}
}

View File

@@ -0,0 +1,21 @@
package org.springframework.ui.validation;
import org.springframework.ui.alert.Alert;
/**
* A single validation failure generated by a Validator.
* @author Keith Donald
* @see Validator#validate(Object, java.util.List)
*/
public interface ValidationFailure {
/**
* The name of the model property associated with this validation result.
*/
String getProperty();
/**
* Gets the alert for this validation failure, appropriate for rendering the failure to the user.
*/
Alert getAlert();
}

View File

@@ -1,23 +0,0 @@
package org.springframework.ui.validation;
import org.springframework.ui.alert.Alert;
public interface ValidationResult {
/**
* The name of the model property associated with this validation result.
*/
String getProperty();
/**
* Indicates if the validation failed.
*/
boolean isFailure();
/**
* Gets the alert for this validation result, appropriate for rendering the result to the user.
* An alert describing a successful validation will have info severity.
* An alert describing a failed validation will have either warning or error severity.
*/
Alert getAlert();
}

View File

@@ -1,20 +0,0 @@
/*
* Copyright 2002-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.validation;
public interface ValidationResults extends Iterable<ValidationResult> {
}

View File

@@ -17,6 +17,18 @@ package org.springframework.ui.validation;
import java.util.List;
public interface Validator<M> {
ValidationResults validate(M model, List<String> properties);
/**
* Validates a model object.
* @author Keith Donald
* @param <M> the type of model object this validator supports
*/
public interface Validator {
/**
* Validate the properties of the model object.
* @param model the model object
* @param properties the properties to validate
* @return a list of validation failures, empty if there were no failures
*/
List<ValidationFailure> validate(Object model, List<String> properties);
}