diff --git a/spring-binding/src/main/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactory.java b/spring-binding/src/main/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactory.java index 6114358e..16498176 100644 --- a/spring-binding/src/main/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactory.java +++ b/spring-binding/src/main/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactory.java @@ -66,6 +66,7 @@ public class DefaultValidationFailureMessageResolverFactory implements Validatio ConversionService conversionService) { Assert.notNull(expressionParser, "The expressionParser is required"); this.expressionParser = expressionParser; + this.conversionService = conversionService; } /** @@ -92,6 +93,8 @@ public class DefaultValidationFailureMessageResolverFactory implements Validatio private static final String LABEL_ARGUMENT = "label"; + private static final String VALUE_ARGUMENT = "value"; + private ValidationFailure failure; private ValidationFailureModelContext modelContext; @@ -116,7 +119,11 @@ public class DefaultValidationFailureMessageResolverFactory implements Validatio } else { label = appendLabelPrefix().append(CODE_SEPARATOR).append(modelContext.getObjectName()).toString(); } - stringArgs.put(LABEL_ARGUMENT, new DefaultMessageSourceResolvable(label)); + stringArgs.put(LABEL_ARGUMENT, new DefaultMessageSourceResolvable(new String[] { label }, failure + .getPropertyName())); + } + if (!stringArgs.containsKey(VALUE_ARGUMENT) && failure.getPropertyName() != null) { + stringArgs.put(VALUE_ARGUMENT, modelContext.getInvalidUserValue()); } if (failure.getArguments() != null) { Iterator it = failure.getArguments().entrySet().iterator(); @@ -140,7 +147,8 @@ public class DefaultValidationFailureMessageResolverFactory implements Validatio } private String[] buildCodes() { - String constraintMessageCode = appendMessageCodePrefix().append(failure.getConstraint()).toString(); + String constraintMessageCode = appendMessageCodePrefix().append(CODE_SEPARATOR).append( + failure.getConstraint()).toString(); if (failure.getPropertyName() != null) { String propertyConstraintMessageCode = appendMessageCodePrefix().append(CODE_SEPARATOR).append( modelContext.getObjectName()).append(CODE_SEPARATOR).append(failure.getPropertyName()).append( diff --git a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureBuilder.java b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureBuilder.java index e0809f30..10c86328 100644 --- a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureBuilder.java +++ b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureBuilder.java @@ -19,6 +19,8 @@ import java.util.Map; import java.util.TreeMap; import org.springframework.binding.message.Severity; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; /** * A builder that provides a fluent interface for configuring a Validation failure. Example: @@ -80,7 +82,9 @@ public class ValidationFailureBuilder { } /** - * Sets the failure to be of warning severity. + * Adds a failure message argument. + * @param name the arg name + * @param value the arg value * @return this, for fluent call chaining */ public ValidationFailureBuilder arg(String name, Object value) { @@ -91,6 +95,18 @@ public class ValidationFailureBuilder { return this; } + /** + * Adds a failure message argument whose value is also message source resolvable. Use this when the argument value + * itself needs to be localized. + * @param name the arg name + * @param code the code that will be used to resolve the arg + * @see MessageSourceResolvable + * @return this, for fluent call chaining + */ + public ValidationFailureBuilder resolvableArg(String name, String code) { + return arg(name, new DefaultMessageSourceResolvable(code)); + } + /** * Build the ValidationFailure. Called after setting builder properties. * @return this, for fluent call chaining diff --git a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureModelContext.java b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureModelContext.java index 08a40d47..d001760e 100644 --- a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureModelContext.java +++ b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureModelContext.java @@ -12,14 +12,19 @@ public class ValidationFailureModelContext { private String propertyTypeConverter; + private Object invalidValue; + /** * Creates a new validation model context. * @param objectName the object name + * @param invalidValue the invalid value the user entered * @param propertyType the property type (may be null) * @param propertyTypeConverter the id of the property type converter (may be null) */ - public ValidationFailureModelContext(String objectName, Class propertyType, String propertyTypeConverter) { + public ValidationFailureModelContext(String objectName, Object invalidValue, Class propertyType, + String propertyTypeConverter) { this.objectName = objectName; + this.invalidValue = invalidValue; this.propertyType = propertyType; this.propertyTypeConverter = propertyTypeConverter; } @@ -31,6 +36,13 @@ public class ValidationFailureModelContext { return objectName; } + /** + * The user entered value. + */ + public Object getInvalidUserValue() { + return invalidValue; + } + /** * When reporting a property validation failure, the type of the property that failed to validate. */ diff --git a/spring-binding/src/test/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactoryTests.java b/spring-binding/src/test/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactoryTests.java new file mode 100644 index 00000000..3d7bab95 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactoryTests.java @@ -0,0 +1,127 @@ +package org.springframework.binding.validation; + +import java.util.Date; +import java.util.Locale; + +import junit.framework.TestCase; + +import org.jboss.el.ExpressionFactoryImpl; +import org.springframework.binding.convert.converters.StringToObject; +import org.springframework.binding.convert.service.DefaultConversionService; +import org.springframework.binding.expression.el.ELExpressionParser; +import org.springframework.binding.message.Message; +import org.springframework.binding.message.MessageResolver; +import org.springframework.context.support.ResourceBundleMessageSource; + +public class DefaultValidationFailureMessageResolverFactoryTests extends TestCase { + + private ELExpressionParser parser = new ELExpressionParser(new ExpressionFactoryImpl()); + + private DefaultConversionService conversionService = new DefaultConversionService(); + + private ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + + private DefaultValidationFailureMessageResolverFactory factory; + + public void setUp() { + factory = new DefaultValidationFailureMessageResolverFactory(parser, conversionService); + messageSource.setBasename("org.springframework.binding.validation.messages"); + } + + public void testResolveMessage() { + ValidationFailureBuilder builder = new ValidationFailureBuilder(); + ValidationFailure failure = builder.forProperty("foo").constraint("required").build(); + MessageResolver resolver = factory.createMessageResolver(failure, new ValidationFailureModelContext("testBean", + "", String.class, null)); + Message message = resolver.resolveMessage(messageSource, Locale.getDefault()); + assertEquals("Foo is required", message.getText()); + } + + public void testResolveMessageNoPropertyLabel() { + ValidationFailureBuilder builder = new ValidationFailureBuilder(); + ValidationFailure failure = builder.forProperty("bogus").constraint("required").build(); + MessageResolver resolver = factory.createMessageResolver(failure, new ValidationFailureModelContext("testBean", + "", String.class, null)); + Message message = resolver.resolveMessage(messageSource, Locale.getDefault()); + assertEquals("bogus is required", message.getText()); + } + + public void testResolveMessageWithCustomArg() { + ValidationFailureBuilder builder = new ValidationFailureBuilder(); + ValidationFailure failure = builder.forProperty("checkinDate").constraint("invalidFormat").arg("format", + "yyyy-MM-dd").build(); + MessageResolver resolver = factory.createMessageResolver(failure, new ValidationFailureModelContext("testBean", + "bogus", Date.class, null)); + Message message = resolver.resolveMessage(messageSource, Locale.getDefault()); + assertEquals("Check In Date must be in format yyyy-MM-dd", message.getText()); + } + + public void testResolveMessageWithCustomResolvableArg() { + ValidationFailureBuilder builder = new ValidationFailureBuilder(); + ValidationFailure failure = builder.forProperty("checkinDate").constraint("invalidFormat").resolvableArg( + "format", "formats.dateFormat").build(); + MessageResolver resolver = factory.createMessageResolver(failure, new ValidationFailureModelContext("testBean", + "bogus", Date.class, null)); + Message message = resolver.resolveMessage(messageSource, Locale.getDefault()); + assertEquals("Check In Date must be in format yyyy-MM-dd", message.getText()); + } + + public void testResolveMessageWithValue() { + ValidationFailureBuilder builder = new ValidationFailureBuilder(); + ValidationFailure failure = builder.forProperty("checkinDate").constraint("invalidFormat2").resolvableArg( + "format", "formats.dateFormat").build(); + MessageResolver resolver = factory.createMessageResolver(failure, new ValidationFailureModelContext("testBean", + "bogus", Date.class, null)); + Message message = resolver.resolveMessage(messageSource, Locale.getDefault()); + assertEquals("Check In Date must be in format yyyy-MM-dd but it was 'bogus'", message.getText()); + } + + public void testResolveMessageWithArgDefaultConversion() { + ValidationFailureBuilder builder = new ValidationFailureBuilder(); + ValidationFailure failure = builder.forProperty("amount").constraint("range").arg("min", new Integer(1)).arg( + "max", new Integer(100)).build(); + MessageResolver resolver = factory.createMessageResolver(failure, new ValidationFailureModelContext("testBean", + "bogus", Integer.class, null)); + Message message = resolver.resolveMessage(messageSource, Locale.getDefault()); + assertEquals("Amount must be between 1 and 100", message.getText()); + } + + public void testResolveMessageWithArgCustomConversion() { + conversionService.addConverter("stringToMoney", new StringToMoney()); + ValidationFailureBuilder builder = new ValidationFailureBuilder(); + ValidationFailure failure = builder.forProperty("amount").constraint("range").arg("min", new Money(1)).arg( + "max", new Money(100)).build(); + MessageResolver resolver = factory.createMessageResolver(failure, new ValidationFailureModelContext("testBean", + "bogus", Money.class, "stringToMoney")); + Message message = resolver.resolveMessage(messageSource, Locale.getDefault()); + assertEquals("Amount must be between $1 and $100", message.getText()); + } + + public static class Money { + private int amount; + + public Money(int amount) { + this.amount = amount; + } + + public int getAmount() { + return amount; + } + } + + public static class StringToMoney extends StringToObject { + + public StringToMoney() { + super(Money.class); + } + + protected Object toObject(String string, Class targetClass) throws Exception { + throw new UnsupportedOperationException("Not supported"); + } + + protected String toString(Object object) throws Exception { + return "$" + String.valueOf(((Money) object).amount); + } + + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/validation/messages.properties b/spring-binding/src/test/java/org/springframework/binding/validation/messages.properties new file mode 100644 index 00000000..7c27dc16 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/validation/messages.properties @@ -0,0 +1,10 @@ +validation.required=#{label} is required +validation.invalidFormat=#{label} must be in format #{format} +validation.invalidFormat2=#{label} must be in format #{format} but it was '#{value}' +validation.range=#{label} must be between #{min} and #{max} + +label.testBean.foo=Foo +label.testBean.checkinDate=Check In Date +label.testBean.amount=Amount + +formats.dateFormat=yyyy-MM-dd \ No newline at end of file