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 ea7dfa02907..f38ba70adee 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 @@ -19,18 +19,20 @@ import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.convert.TypeConverter; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultTypeConverter; 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.Binding.BindingStatus; import org.springframework.ui.binding.config.BindingRuleConfiguration; import org.springframework.ui.binding.config.Condition; @@ -57,6 +59,8 @@ public class GenericBinder implements Binder { private MessageSource messageSource; + private String[] requiredProperties = new String[0]; + /** * Creates a new binder for the model object. * @param model the model object containing properties this binder will bind to @@ -140,10 +144,15 @@ public class GenericBinder implements Binder { public BindingResults bind(Map sourceValues) { sourceValues = filter(sourceValues); + checkRequired(sourceValues); ArrayListBindingResults results = new ArrayListBindingResults(sourceValues.size()); for (Map.Entry sourceValue : sourceValues.entrySet()) { - Binding binding = getBinding(sourceValue.getKey()); - results.add(bind(sourceValue, binding)); + try { + Binding binding = getBinding(sourceValue.getKey()); + results.add(bind(sourceValue, binding)); + } catch (PropertyNotFoundException e) { + results.add(new PropertyNotFoundResult(sourceValue.getKey(), sourceValue.getValue(), messageSource)); + } } return results; } @@ -163,6 +172,24 @@ public class GenericBinder implements Binder { } // internal helpers + + private void checkRequired(Map sourceValues) { + List missingRequired = new ArrayList(); + for (String required : requiredProperties) { + boolean found = false; + for (String property : sourceValues.keySet()) { + if (property.equals(required)) { + found = true; + } + } + if (!found) { + missingRequired.add(required); + } + } + if (!missingRequired.isEmpty()) { + throw new MissingSourceValuesException(missingRequired, sourceValues); + } + } private GenericBindingRule getBindingRule(String property) { GenericBindingRule rule = bindingRules.get(property); @@ -217,6 +244,10 @@ public class GenericBinder implements Binder { // implementing BindingContext + public MessageSource getMessageSource() { + return messageSource; + } + public TypeConverter getTypeConverter() { return typeConverter; } @@ -326,8 +357,7 @@ public class GenericBinder implements Binder { return propDesc; } } - throw new IllegalArgumentException("No property '" + property + "' found on model [" - + modelClass.getName() + "]"); + throw new PropertyNotFoundException(property, modelClass); } private BeanInfo getBeanInfo(Class clazz) { @@ -341,6 +371,8 @@ public class GenericBinder implements Binder { public interface BindingContext { + MessageSource getMessageSource(); + TypeConverter getTypeConverter(); Condition getEditableCondition(); @@ -362,4 +394,8 @@ public class GenericBinder implements Binder { } + public void setRequired(String[] propertyPaths) { + this.requiredProperties = propertyPaths; + } + } diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyNotFoundException.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyNotFoundException.java new file mode 100644 index 00000000000..5217ba503e2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyNotFoundException.java @@ -0,0 +1,23 @@ +package org.springframework.ui.binding.support; + +public class PropertyNotFoundException extends RuntimeException { + + private String property; + + private Class modelClass; + + public PropertyNotFoundException(String property, Class modelClass) { + super("No property '" + property + "' found on model [" + modelClass.getName() + "]"); + this.property = property; + this.modelClass = modelClass; + } + + public String getProperty() { + return property; + } + + public Class getModelClass() { + return modelClass; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyNotFoundResult.java b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyNotFoundResult.java new file mode 100644 index 00000000000..055ac75fc5d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/binding/support/PropertyNotFoundResult.java @@ -0,0 +1,64 @@ +package org.springframework.ui.binding.support; + +import org.springframework.context.MessageSource; +import org.springframework.core.style.StylerUtils; +import org.springframework.ui.alert.Alert; +import org.springframework.ui.alert.Severity; +import org.springframework.ui.binding.BindingResult; +import org.springframework.ui.message.MessageBuilder; +import org.springframework.ui.message.ResolvableArgument; + +class PropertyNotFoundResult implements BindingResult { + + private String property; + + private Object sourceValue; + + private MessageSource messageSource; + + public PropertyNotFoundResult(String property, Object sourceValue, MessageSource messageSource) { + this.property = property; + this.sourceValue = sourceValue; + this.messageSource = messageSource; + } + + public String getProperty() { + return property; + } + + public Object getSourceValue() { + return sourceValue; + } + + public boolean isFailure() { + return true; + } + + public Alert getAlert() { + return new Alert() { + public String getCode() { + return "propertyNotFound"; + } + + public Severity getSeverity() { + return Severity.WARNING; + } + + public String getMessage() { + MessageBuilder builder = new MessageBuilder(messageSource); + builder.code("bindSuccess"); + builder.arg("label", new ResolvableArgument(property)); + builder.arg("value", sourceValue); + // TODO lazily create default message + builder.defaultMessage("Successfully bound user value " + StylerUtils.style(sourceValue) + + " to property '" + property + "'"); + return builder.build(); + } + }; + } + + public String toString() { + return getAlert().toString(); + } + +} \ No newline at end of file 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 2968c2d3432..977e0cfb571 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 @@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue; import java.math.BigDecimal; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; @@ -16,23 +17,33 @@ import java.util.Map; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.context.i18n.LocaleContextHolder; +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.format.AnnotationFormatterFactory; import org.springframework.ui.format.Formatted; import org.springframework.ui.format.Formatter; import org.springframework.ui.format.date.DateFormatter; import org.springframework.ui.format.number.CurrencyFormat; import org.springframework.ui.format.number.CurrencyFormatter; +import org.springframework.ui.format.number.IntegerFormatter; +import org.springframework.ui.message.MockMessageSource; +import org.springframework.util.Assert; public class GenericBinderTests { + private GenericBinder binder; + private TestBean bean; @Before public void setUp() { bean = new TestBean(); + binder = new GenericBinder(bean); LocaleContextHolder.setLocale(Locale.US); } @@ -77,7 +88,6 @@ public class GenericBinderTests { @Test public void bindSingleValuesWithDefaultTypeConversionFailure() { - GenericBinder binder = new GenericBinder(bean); Map values = new LinkedHashMap(); values.put("string", "test"); // bad value @@ -91,7 +101,6 @@ public class GenericBinderTests { @Test public void bindSingleValuePropertyFormatter() throws ParseException { - GenericBinder binder = new GenericBinder(bean); binder.bindingRule("date").formatWith(new DateFormatter()); binder.bind(Collections.singletonMap("date", "2009-06-01")); assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate()); @@ -99,7 +108,6 @@ public class GenericBinderTests { @Test public void bindSingleValuePropertyFormatterParseException() { - GenericBinder binder = new GenericBinder(bean); binder.bindingRule("date").formatWith(new DateFormatter()); BindingResults results = binder.bind(Collections.singletonMap("date", "bogus")); assertEquals(1, results.size()); @@ -109,8 +117,6 @@ public class GenericBinderTests { @Test public void bindSingleValueWithFormatterRegistedByType() throws ParseException { - GenericBinder binder = new GenericBinder(bean); - GenericFormatterRegistry formatterRegistry = new GenericFormatterRegistry(); formatterRegistry.add(Date.class, new DateFormatter()); binder.setFormatterRegistry(formatterRegistry); @@ -119,40 +125,46 @@ public class GenericBinderTests { assertEquals(new DateFormatter().parse("2009-06-01", Locale.US), bean.getDate()); } - /* @Test public void bindSingleValueWithAnnotationFormatterFactoryRegistered() throws ParseException { - binder.addBinding("currency"); - binder.registerFormatterFactory(new CurrencyAnnotationFormatterFactory()); + GenericFormatterRegistry formatterRegistry = new GenericFormatterRegistry(); + formatterRegistry.add(new CurrencyAnnotationFormatterFactory()); + binder.setFormatterRegistry(formatterRegistry); + binder.bind(Collections.singletonMap("currency", "$23.56")); assertEquals(new BigDecimal("23.56"), bean.getCurrency()); } - @Test(expected = NoSuchBindingException.class) + @Test public void bindSingleValuePropertyNotFound() throws ParseException { - binder.bind(Collections.singletonMap("bogus", "2009-06-01")); + BindingResults results = binder.bind(Collections.singletonMap("bogus", "2009-06-01")); + assertEquals("bogus", results.get(0).getProperty()); + assertTrue(results.get(0).isFailure()); + assertEquals("propertyNotFound", results.get(0).getAlert().getCode()); } @Test(expected=MissingSourceValuesException.class) public void bindMissingRequiredSourceValue() { - binder.addBinding("string"); - binder.addBinding("integer").required(); - Map userMap = new LinkedHashMap(); - userMap.put("string", "test"); - // missing "integer" - binder.bind(userMap); + binder.setRequired(new String[] { + "integer" + }); + // missing "integer" - violated bind contract + binder.bind(Collections.singletonMap("string", "test")); } @Test public void getBindingCustomFormatter() { - binder.addBinding("currency").formatWith(new CurrencyFormatter()); Binding b = binder.getBinding("currency"); - assertFalse(b.isIndexable()); + assertFalse(b.isList()); + assertFalse(b.isMap()); assertEquals("", b.getValue()); - b.setValue("$23.56"); + b.applySourceValue("$23.56"); + assertEquals("$23.56", b.getValue()); + b.commit(); assertEquals("$23.56", b.getValue()); } + /* @Test public void getBindingCustomFormatterRequiringTypeCoersion() { // IntegerFormatter formats Longs, so conversion from Integer -> Long is performed