From f1b936515f1c35e218787ae6a605b9a9a8d11ff6 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Tue, 23 Jun 2009 17:53:16 +0000 Subject: [PATCH] @Model and @Bound annotations for configuring Binder instance from annotation model beans --- .../ui/alert/support/DefaultAlertContext.java | 10 +-- .../org/springframework/ui/binding/Bound.java | 2 +- .../org/springframework/ui/binding/Model.java | 2 +- .../ui/binding/support/GenericBinder.java | 63 +++++++++++++++++-- .../WebBindAndValidateLifecycle.java | 48 ++++++++++++-- .../binding/support/GenericBinderTests.java | 19 ++++++ ... => WebBindAndValidateLifecycleTests.java} | 62 +++++++++++++++++- 7 files changed, 190 insertions(+), 16 deletions(-) rename org.springframework.context/src/test/java/org/springframework/ui/lifecycle/{WebBindAndLifecycleTests.java => WebBindAndValidateLifecycleTests.java} (69%) diff --git a/org.springframework.context/src/main/java/org/springframework/ui/alert/support/DefaultAlertContext.java b/org.springframework.context/src/main/java/org/springframework/ui/alert/support/DefaultAlertContext.java index e02bdb79e56..7d3880b6255 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/alert/support/DefaultAlertContext.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/alert/support/DefaultAlertContext.java @@ -34,7 +34,7 @@ import org.springframework.util.CachingMapDecorator; public class DefaultAlertContext implements AlertContext { @SuppressWarnings("serial") - private Map> alertsByElement = new CachingMapDecorator>(new LinkedHashMap>()) { + private Map> alerts = new CachingMapDecorator>(new LinkedHashMap>()) { protected List create(String element) { return new ArrayList(); } @@ -43,11 +43,11 @@ public class DefaultAlertContext implements AlertContext { // implementing AlertContext public Map> getAlerts() { - return Collections.unmodifiableMap(alertsByElement); + return Collections.unmodifiableMap(alerts); } public List getAlerts(String element) { - List messages = alertsByElement.get(element); + List 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 alerts = alertsByElement.get(alert.getElement()); + List 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(); } } \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/Bound.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/Bound.java index 120af93adc8..dffefa0ca48 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/Bound.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/Bound.java @@ -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 { diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java index dde5a9df323..2759d0626e2 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/Model.java @@ -20,6 +20,6 @@ public @interface Model { * Configures strict model binding. * @see Binder#setStrict(boolean) */ - boolean strict() default false; + boolean strictBinding() default false; } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java index c964f4319c0..2bbf288f79b 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/GenericBinder.java @@ -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(); + } + } } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java b/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java index 235141e0b1e..600d5baf3ab 100644 --- a/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java +++ b/org.springframework.context/src/main/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycle.java @@ -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 + } + } + } diff --git a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java index 8c2c7aa231c..39ed44f5113 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/binding/support/GenericBinderTests.java @@ -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())); diff --git a/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java b/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycleTests.java similarity index 69% rename from org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java rename to org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycleTests.java index 6575bc83ca9..fad0457b8ba 100644 --- a/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndLifecycleTests.java +++ b/org.springframework.context/src/test/java/org/springframework/ui/lifecycle/WebBindAndValidateLifecycleTests.java @@ -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 userMap = new HashMap(); + 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 userMap = new HashMap(); + 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; + } + + + } }