@Model and @Bound annotations for configuring Binder instance from annotation model beans

This commit is contained in:
Keith Donald 2009-06-23 17:53:16 +00:00
parent 13c3c577eb
commit f1b936515f
7 changed files with 190 additions and 16 deletions

View File

@ -34,7 +34,7 @@ import org.springframework.util.CachingMapDecorator;
public class DefaultAlertContext implements AlertContext {
@SuppressWarnings("serial")
private Map<String, List<Alert>> alertsByElement = new CachingMapDecorator<String, List<Alert>>(new LinkedHashMap<String, List<Alert>>()) {
private Map<String, List<Alert>> alerts = new CachingMapDecorator<String, List<Alert>>(new LinkedHashMap<String, List<Alert>>()) {
protected List<Alert> create(String element) {
return new ArrayList<Alert>();
}
@ -43,11 +43,11 @@ public class DefaultAlertContext implements AlertContext {
// implementing AlertContext
public Map<String, List<Alert>> getAlerts() {
return Collections.unmodifiableMap(alertsByElement);
return Collections.unmodifiableMap(alerts);
}
public List<Alert> getAlerts(String element) {
List<Alert> messages = alertsByElement.get(element);
List<Alert> messages = alerts.get(element);
if (messages.isEmpty()) {
return Collections.emptyList();
}
@ -55,12 +55,12 @@ public class DefaultAlertContext implements AlertContext {
}
public void add(Alert alert) {
List<Alert> alerts = alertsByElement.get(alert.getElement());
List<Alert> alerts = this.alerts.get(alert.getElement());
alerts.add(alert);
}
public String toString() {
return new ToStringCreator(this).append("alertsByElement", alertsByElement).toString();
return new ToStringCreator(this).append("alerts", alerts).toString();
}
}

View File

@ -6,7 +6,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bound {

View File

@ -20,6 +20,6 @@ public @interface Model {
* Configures strict model binding.
* @see Binder#setStrict(boolean)
*/
boolean strict() default false;
boolean strictBinding() default false;
}

View File

@ -107,6 +107,10 @@ public class GenericBinder implements Binder {
return model;
}
public boolean isStrict() {
return strict;
}
public void setStrict(boolean strict) {
this.strict = strict;
}
@ -148,7 +152,11 @@ public class GenericBinder implements Binder {
ArrayListBindingResults results = new ArrayListBindingResults(values.size());
for (UserValue value : values) {
BindingImpl binding = (BindingImpl) getBinding(value.getProperty());
results.add(binding.setValue(value.getValue()));
if (binding != null) {
results.add(binding.setValue(value.getValue()));
} else {
results.add(new NoSuchBindingResult(value));
}
}
return results;
}
@ -449,6 +457,47 @@ public class GenericBinder implements Binder {
}
}
static class NoSuchBindingResult implements BindingResult {
private UserValue userValue;
public NoSuchBindingResult(UserValue userValue) {
this.userValue = userValue;
}
public String getProperty() {
return userValue.getProperty();
}
public Object getUserValue() {
return userValue.getValue();
}
public boolean isFailure() {
return true;
}
public Alert getAlert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
}
public String getCode() {
return "noSuchBinding";
}
public Severity getSeverity() {
return Severity.WARNING;
}
public String getMessage() {
return "Failed to bind to property '" + userValue.getProperty() + "'; no binding has been added for the property";
}
};
}
}
static class InvalidFormatResult implements BindingResult {
private String property;
@ -473,7 +522,7 @@ public class GenericBinder implements Binder {
}
public Alert getAlert() {
return new Alert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
@ -523,7 +572,7 @@ public class GenericBinder implements Binder {
}
public Alert getAlert() {
return new Alert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
@ -611,7 +660,7 @@ public class GenericBinder implements Binder {
}
public Alert getAlert() {
return new Alert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
@ -632,4 +681,10 @@ public class GenericBinder implements Binder {
}
}
static abstract class AbstractAlert implements Alert {
public String toString() {
return getElement() + ":" + getCode() + " - " + getMessage();
}
}
}

View File

@ -15,15 +15,25 @@
*/
package org.springframework.ui.lifecycle;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.ui.alert.AlertContext;
import org.springframework.ui.binding.BindingConfiguration;
import org.springframework.ui.binding.BindingResult;
import org.springframework.ui.binding.BindingResults;
import org.springframework.ui.binding.Bound;
import org.springframework.ui.binding.FormatterRegistry;
import org.springframework.ui.binding.Model;
import org.springframework.ui.binding.UserValues;
import org.springframework.ui.binding.support.WebBinder;
import org.springframework.ui.validation.Validator;
import org.springframework.util.StringUtils;
/**
* Implementation of the bind and validate lifecycle for web (HTTP) environments.
@ -41,13 +51,12 @@ public class WebBindAndValidateLifecycle {
private Validator validator;
public WebBindAndValidateLifecycle(Object model, AlertContext alertContext) {
// TODO allow binder to be configured with bindings from @Model metadata
// TODO support @Bound property annotation?
// TODO support @StrictBinding class-level annotation?
this.binder = new WebBinder(model);
// TODO this doesn't belong in here
configure(binder, model);
this.alertContext = alertContext;
}
public void setFormatterRegistry(FormatterRegistry registry) {
binder.setFormatterRegistry(registry);
}
@ -76,4 +85,35 @@ public class WebBindAndValidateLifecycle {
};
}
// internal helpers
private void configure(WebBinder binder, Object model) {
Model m = AnnotationUtils.findAnnotation(model.getClass(), Model.class);
if (m != null) {
if (StringUtils.hasText(m.value())) {
// TODO model name setting
//binder.setModelName(m.value());
}
binder.setStrict(m.strictBinding());
}
if (binder.isStrict()) {
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(model.getClass());
} catch (IntrospectionException e) {
throw new IllegalStateException("Unable to introspect model " + model, 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.add(new BindingConfiguration(prop.getName(), null));
}
}
// TODO @Bound fields
}
}
}

View File

@ -169,6 +169,25 @@ public class GenericBinderTests {
assertFalse(result.isFailure());
}
@Test
public void bindStrictNoMappingBindings() {
binder.setStrict(true);
binder.add(new BindingConfiguration("integer", null));
UserValues values = new UserValues();
values.add("integer", "3");
values.add("foo", "BAR");
BindingResults results = binder.bind(values);
assertEquals(2, results.size());
assertEquals("integer", results.get(0).getProperty());
assertFalse(results.get(0).isFailure());
assertEquals("3", results.get(0).getUserValue());
assertEquals("foo", results.get(1).getProperty());
assertTrue(results.get(1).isFailure());
assertEquals("BAR", results.get(1).getUserValue());
}
@Test
public void getBindingCustomFormatter() {
binder.add(new BindingConfiguration("currency", new CurrencyFormatter()));

View File

@ -12,11 +12,13 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.ui.alert.Severity;
import org.springframework.ui.alert.support.DefaultAlertContext;
import org.springframework.ui.binding.Bound;
import org.springframework.ui.binding.Model;
import org.springframework.ui.binding.support.GenericFormatterRegistry;
import org.springframework.ui.format.number.CurrencyFormat;
import org.springframework.ui.format.number.IntegerFormatter;
public class WebBindAndLifecycleTests {
public class WebBindAndValidateLifecycleTests {
private WebBindAndValidateLifecycle lifecycle;
@ -67,6 +69,37 @@ public class WebBindAndLifecycleTests {
assertEquals(Severity.ERROR, alertContext.getAlerts("integer").get(0).getSeverity());
assertEquals("Failed to bind to property 'integer'; the user value 'bogus' has an invalid format and could no be parsed", alertContext.getAlerts("integer").get(0).getMessage());
}
@Test
public void testExecuteLifecycleAnnotatedModel() {
TestAnnotatedBean model = new TestAnnotatedBean();
lifecycle = new WebBindAndValidateLifecycle(model, alertContext);
Map<String, Object> userMap = new HashMap<String, Object>();
GenericFormatterRegistry registry = new GenericFormatterRegistry();
registry.add(new IntegerFormatter(), Integer.class);
lifecycle.setFormatterRegistry(registry);
userMap.put("editable", "foo");
lifecycle.execute(userMap);
assertEquals(0, alertContext.getAlerts().size());
assertEquals("foo", model.getEditable());
}
@Test
public void testExecuteLifecycleAnnotatedModelNonEditableBindingAttempt() {
TestAnnotatedBean model = new TestAnnotatedBean();
lifecycle = new WebBindAndValidateLifecycle(model, alertContext);
Map<String, Object> userMap = new HashMap<String, Object>();
GenericFormatterRegistry registry = new GenericFormatterRegistry();
registry.add(new IntegerFormatter(), Integer.class);
lifecycle.setFormatterRegistry(registry);
userMap.put("editable", "foo");
userMap.put("nonEditable", "whatev");
lifecycle.execute(userMap);
assertEquals(1, alertContext.getAlerts().size());
assertEquals("foo", model.getEditable());
assertEquals(null, model.getNotEditable());
assertEquals("noSuchBinding", alertContext.getAlerts("nonEditable").get(0).getCode());
}
public static enum FooEnum {
BAR, BAZ, BOOP;
@ -188,4 +221,31 @@ public class WebBindAndLifecycleTests {
}
}
@Model(value="testBean", strictBinding=true)
public class TestAnnotatedBean {
private String editable;
private String notEditable;
@Bound
public String getEditable() {
return editable;
}
public void setEditable(String editable) {
this.editable = editable;
}
public String getNotEditable() {
return notEditable;
}
public void setNotEditable(String notEditable) {
this.notEditable = notEditable;
}
}
}