From 875e0aa64a023a671e6401d5e0d716b5f37c41dc Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Fri, 27 Feb 2009 20:56:15 +0000 Subject: [PATCH] Validation context polishing --- .../message/DefaultMessageResolver.java | 14 +- ...lidationFailureMessageResolverFactory.java | 204 ++++++++++++++++++ .../validation/PropertyValidationFailure.java | 37 ---- .../binding/validation/ValidationContext.java | 14 +- .../binding/validation/ValidationFailure.java | 40 ++-- .../validation/ValidationFailureBuilder.java | 49 ++++- ...lidationFailureMessageResolverFactory.java | 13 +- .../ValidationFailureModelContext.java | 48 +++++ .../validation/DefaultValidationContext.java | 9 +- 9 files changed, 358 insertions(+), 70 deletions(-) create mode 100644 spring-binding/src/main/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactory.java delete mode 100644 spring-binding/src/main/java/org/springframework/binding/validation/PropertyValidationFailure.java create mode 100644 spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureModelContext.java diff --git a/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageResolver.java b/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageResolver.java index 3e2b0efc..4772bc16 100644 --- a/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageResolver.java +++ b/spring-binding/src/main/java/org/springframework/binding/message/DefaultMessageResolver.java @@ -21,7 +21,7 @@ import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.core.style.ToStringCreator; -class DefaultMessageResolver implements MessageResolver, MessageSourceResolvable { +public class DefaultMessageResolver implements MessageResolver, MessageSourceResolvable { private Object source; @@ -42,7 +42,17 @@ class DefaultMessageResolver implements MessageResolver, MessageSourceResolvable } public Message resolveMessage(MessageSource messageSource, Locale locale) { - return new Message(source, messageSource.getMessage(this, locale), severity); + return new Message(source, postProcessMessageText(messageSource.getMessage(this, locale)), severity); + } + + /** + * Subclasses may override to perform special post-processing of the returned message text; for example, running it + * through an Expression evaluator. + * @param text the resolved message text + * @return the post processeed message text + */ + protected String postProcessMessageText(String text) { + return text; } // implementing MessageSourceResolver 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 new file mode 100644 index 00000000..6114358e --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/validation/DefaultValidationFailureMessageResolverFactory.java @@ -0,0 +1,204 @@ +package org.springframework.binding.validation; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.support.FluentParserContext; +import org.springframework.binding.message.Message; +import org.springframework.binding.message.MessageResolver; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.util.Assert; + +/** + * Maps validation failures to messages resolvable in a {@link MessageSource}. Configurable with an + * {@link ExpressionParser} to provide support messages that contain #{expressions} for inserting failure arguments. + * Configurable with a {@link ConversionService} to support String-encoding failure arguments. + * + * Employs the following algorithm to map property validation failure to a message: + *
    + *
  1. Try the ${failureMessageCodePrefix}.${objectName}.${propertyName}.${constraint} code; if match, resolve message + * and return. + *
  2. Try the ${failureMessageCodePrefix}.${propertyType}.${constraint} code; if matched, resolve message and return. + *
  3. Try the ${failureMessageCodePrefix}.${constrant} code; if matched, resolve message and return. + *
+ * + * Named message arguments may be denoted by using ${} or #{} expressions. For property validation failures, the value + * of the "label" argument is automatically mapped to the message with code + * ${labelMessageCodePrefix}.${objectName}.${propertyName}. + * + * messages.properties example: + * + *
+ * validation.booking.checkinDate.required=The Checkin Date is required
+ * validation.java.util.Date.required=Dates are required
+ * validation.required=#{label} is required
+ * 
+ * label.booking.checkoutDate=Check Out Date
+ * 
+ * @author Keith Donald + */ +public class DefaultValidationFailureMessageResolverFactory implements ValidationFailureMessageResolverFactory { + + private static final char CODE_SEPARATOR = '.'; + + private ExpressionParser expressionParser; + + private ConversionService conversionService; + + private String failureMessageCodePrefix = "validation"; + + private String labelMessageCodePrefix = "label"; + + /** + * Creates a new message resolver factory. + * @param expressionParser the expression parser + * @param conversionService the conversion service + */ + public DefaultValidationFailureMessageResolverFactory(ExpressionParser expressionParser, + ConversionService conversionService) { + Assert.notNull(expressionParser, "The expressionParser is required"); + this.expressionParser = expressionParser; + } + + /** + * A prefix to prepend to all validation failure message codes; default is "validation". + * @param failureMessageCodePrefix the failure message code prefix + */ + public void setFailureMessageCodePrefix(String failureMessageCodePrefix) { + this.failureMessageCodePrefix = failureMessageCodePrefix; + } + + /** + * The prefix to prepend to all property label message codes; default is "label". + * @param labelMessageCodePrefix the label message code prefix + */ + public void setLabelMessageCodePrefix(String labelMessageCodePrefix) { + this.labelMessageCodePrefix = labelMessageCodePrefix; + } + + public MessageResolver createMessageResolver(ValidationFailure failure, ValidationFailureModelContext modelContext) { + return new DefaultValidationMessageResolver(failure, modelContext); + } + + private class DefaultValidationMessageResolver implements MessageResolver { + + private static final String LABEL_ARGUMENT = "label"; + + private ValidationFailure failure; + + private ValidationFailureModelContext modelContext; + + public DefaultValidationMessageResolver(ValidationFailure failure, ValidationFailureModelContext modelContext) { + this.modelContext = modelContext; + this.failure = failure; + } + + public Message resolveMessage(MessageSource messageSource, Locale locale) { + DefaultMessageSourceResolvable resolvable = new DefaultMessageSourceResolvable(buildCodes(), failure + .getDefaultMessage()); + String text = messageSource.getMessage(resolvable, locale); + Expression expression = expressionParser.parseExpression(text, new FluentParserContext() + .evaluate(Map.class).template()); + Map stringArgs = new HashMap(); + if (!stringArgs.containsKey(LABEL_ARGUMENT)) { + String label; + if (failure.getPropertyName() != null) { + label = appendLabelPrefix().append(CODE_SEPARATOR).append(modelContext.getObjectName()).append( + CODE_SEPARATOR).append(failure.getPropertyName()).toString(); + } else { + label = appendLabelPrefix().append(CODE_SEPARATOR).append(modelContext.getObjectName()).toString(); + } + stringArgs.put(LABEL_ARGUMENT, new DefaultMessageSourceResolvable(label)); + } + if (failure.getArguments() != null) { + Iterator it = failure.getArguments().entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + Object arg = entry.getValue(); + if (!(arg instanceof String) && !(arg instanceof MessageSourceResolvable)) { + if (modelContext.getPropertyTypeConverter() != null + && arg.getClass().equals(modelContext.getPropertyType())) { + arg = conversionService.executeConversion(modelContext.getPropertyTypeConverter(), arg, + String.class); + } else { + arg = conversionService.executeConversion(arg, String.class); + } + } + stringArgs.put(entry.getKey(), arg); + } + } + text = (String) expression.getValue(new LazyMessageResolvingMap(messageSource, locale, stringArgs)); + return new Message(failure.getPropertyName(), text, failure.getSeverity()); + } + + private String[] buildCodes() { + String constraintMessageCode = appendMessageCodePrefix().append(failure.getConstraint()).toString(); + if (failure.getPropertyName() != null) { + String propertyConstraintMessageCode = appendMessageCodePrefix().append(CODE_SEPARATOR).append( + modelContext.getObjectName()).append(CODE_SEPARATOR).append(failure.getPropertyName()).append( + CODE_SEPARATOR).append(failure.getConstraint()).toString(); + String typeConstraintMessageCode = appendMessageCodePrefix().append(CODE_SEPARATOR).append( + modelContext.getPropertyType().getName()).append(CODE_SEPARATOR) + .append(failure.getConstraint()).toString(); + return new String[] { propertyConstraintMessageCode, typeConstraintMessageCode, constraintMessageCode }; + } else { + String objectConstraintMessageCode = appendMessageCodePrefix().append(CODE_SEPARATOR).append( + modelContext.getObjectName()).append(CODE_SEPARATOR).append(failure.getConstraint()).toString(); + return new String[] { objectConstraintMessageCode, failure.getConstraint() }; + } + } + + private StringBuilder appendMessageCodePrefix() { + return new StringBuilder().append(failureMessageCodePrefix); + } + + private StringBuilder appendLabelPrefix() { + return new StringBuilder().append(labelMessageCodePrefix); + } + } + + private static class LazyMessageResolvingMap extends StringKeyedMapAdapter { + + private MessageSource messageSource; + + private Locale locale; + + private Map args; + + public LazyMessageResolvingMap(MessageSource messageSource, Locale locale, Map args) { + this.messageSource = messageSource; + this.locale = locale; + this.args = args; + } + + protected Object getAttribute(String key) { + Object arg = args.get(key); + if (arg instanceof MessageSourceResolvable) { + arg = messageSource.getMessage(((MessageSourceResolvable) arg), locale); + args.put(key, arg); + } + return arg; + } + + protected Iterator getAttributeNames() { + return args.keySet().iterator(); + } + + protected void removeAttribute(String key) { + throw new UnsupportedOperationException("Should not be called"); + } + + protected void setAttribute(String key, Object value) { + throw new UnsupportedOperationException("Should not be called"); + } + + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/validation/PropertyValidationFailure.java b/spring-binding/src/main/java/org/springframework/binding/validation/PropertyValidationFailure.java deleted file mode 100644 index 9eeff008..00000000 --- a/spring-binding/src/main/java/org/springframework/binding/validation/PropertyValidationFailure.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.springframework.binding.validation; - -import java.util.Map; - -import org.springframework.binding.message.Severity; - -/** - * A failure that occurred against a specific property on the object being violated. - * - * @author Keith Donald - */ -public final class PropertyValidationFailure extends ValidationFailure { - - private String propertyName; - - /** - * Creates a new property validation failure. - * @param propertyName the property name - * @param code the failure code - * @param severity the severity - * @param arguments named failure arguments - * @param defaultMessage default message text - */ - public PropertyValidationFailure(String propertyName, String code, Severity severity, Map arguments, - String defaultMessage) { - super(code, severity, arguments, defaultMessage); - this.propertyName = propertyName; - } - - /** - * The name of the property. - */ - public String getPropertyName() { - return propertyName; - } - -} diff --git a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationContext.java b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationContext.java index 49cf5aba..51c5eb93 100644 --- a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationContext.java +++ b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationContext.java @@ -29,11 +29,9 @@ import org.springframework.binding.message.MessageContext; public interface ValidationContext { /** - * A context for adding messages to display to the user directly. The {@link #addFailure(ValidationFailure)} - * operation provides a higher level of abstraction for reporting validation failures. This method provides more - * control over the actual message added. + * The the name of the object being validated. */ - public MessageContext getMessageContext(); + public String getObjectName(); /** * The current user. @@ -60,8 +58,14 @@ public interface ValidationContext { * @param failure the validation failure * @see #getMessageContext() * @see ValidationFailureMessageResolverFactory - * @see PropertyValidationFailure */ public void addFailure(ValidationFailure failure); + /** + * A context for adding messages to display to the user directly. The {@link #addFailure(ValidationFailure)} + * operation provides a higher level of abstraction for reporting validation failures. This method provides more + * control over the actual message added. + */ + public MessageContext getMessageContext(); + } diff --git a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailure.java b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailure.java index edd1079c..c41d5154 100644 --- a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailure.java +++ b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailure.java @@ -3,16 +3,19 @@ package org.springframework.binding.validation; import java.util.Map; import org.springframework.binding.message.Severity; +import org.springframework.util.Assert; /** - * A failure that occurred during validation. Each failure has a code that uniquely identifies the validation constraint - * that was violated; for example "required", or "maxLength". A failure has a severity describing the intensity of the + * An indication of a validation failure. A failure is generated when a validation constraint is violated; for example + * "required", "maxLength", "invalidFormat", or "range". A failure has a severity describing the intensity of the * violation. A failure may have additional arguments that can be used as named parameters in a UI display message. A * failure can also have a default message to display if no suitable message can be resolved. */ public class ValidationFailure { - private String code; + private String propertyName; + + private String constraint; private Severity severity; @@ -22,23 +25,36 @@ public class ValidationFailure { /** * Creates a new validation failure - * @param code the failure code - * @param severity the severity - * @param arguments named failure arguments - * @param defaultMessage the default message text + * @param propertyName the property that failed to validate (may be null to indicate a general failure) + * @param constraint the name of the validation constraint that failed (required) + * @param severity the severity of the failure (required) + * @param arguments named failure arguments (may be null) + * @param defaultMessage the default message text (may be null) */ - public ValidationFailure(String code, Severity severity, Map arguments, String defaultMessage) { - this.code = code; + public ValidationFailure(String propertyName, String constraint, Severity severity, Map arguments, + String defaultMessage) { + Assert.hasText(constraint, "The constraint is required"); + Assert.notNull(severity, "The severity is required"); + this.propertyName = propertyName; + this.constraint = constraint; this.severity = severity; this.arguments = arguments; this.defaultMessage = defaultMessage; } /** - * The failure code, uniquely identifying the validation constraint that failed. + * The name of the property that failed to validate. May be null to indicate a general failure against the validated + * object. */ - public String getCode() { - return code; + public String getPropertyName() { + return propertyName; + } + + /** + * The validation constraint that caused this failure to be reported. + */ + public String getConstraint() { + return constraint; } /** 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 38b5ee86..e0809f30 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,8 +19,16 @@ import java.util.Map; import java.util.TreeMap; import org.springframework.binding.message.Severity; -import org.springframework.util.StringUtils; +/** + * A builder that provides a fluent interface for configuring a Validation failure. Example: + * + *
+ * new ValidationFailureBuilder().forProperty("foo").constraint("required").build();
+ * 
+ * @author Keith Donald + * + */ public class ValidationFailureBuilder { private String propertyName; @@ -29,44 +37,69 @@ public class ValidationFailureBuilder { private Severity severity; - private Map args = new TreeMap(); + private Map args; private String defaultText; + /** + * Sets the property the failure occurred against. + * @param propertyName the property name + * @return this, for fluent call chaining + */ public ValidationFailureBuilder forProperty(String propertyName) { this.propertyName = propertyName; return this; } + /** + * Sets the name of the constraint that failed. + * @param constraint the name of the validation constraint + * @return this, for fluent call chaining + */ public ValidationFailureBuilder constraint(String constraint) { this.constraint = constraint; return this; } + /** + * Sets the failure to be of warning severity. + * @return this, for fluent call chaining + */ public ValidationFailureBuilder warning() { severity = Severity.WARNING; return this; } + /** + * Sets the failure to be of error severity. + * @return this, for fluent call chaining + */ public ValidationFailureBuilder error() { severity = Severity.ERROR; return this; } + /** + * Sets the failure to be of warning severity. + * @return this, for fluent call chaining + */ public ValidationFailureBuilder arg(String name, Object value) { + if (args == null) { + args = new TreeMap(); + } args.put(name, value); return this; } + /** + * Build the ValidationFailure. Called after setting builder properties. + * @return this, for fluent call chaining + */ public ValidationFailure build() { if (severity == null) { severity = Severity.ERROR; } - if (StringUtils.hasText(propertyName)) { - return new PropertyValidationFailure(propertyName, constraint, severity, args, defaultText); - } else { - return new ValidationFailure(constraint, severity, args, defaultText); - } + return new ValidationFailure(propertyName, constraint, severity, args, defaultText); } -} +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureMessageResolverFactory.java b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureMessageResolverFactory.java index a7b392f9..b7f11b99 100644 --- a/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureMessageResolverFactory.java +++ b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureMessageResolverFactory.java @@ -1,9 +1,12 @@ package org.springframework.binding.validation; +import org.springframework.binding.message.Message; +import org.springframework.binding.message.MessageContext; import org.springframework.binding.message.MessageResolver; /** - * Translates a validation failure into a resolvable message. + * Translates a validation failure into a message resolver that can be used to add a {@link Message} to a + * {@link MessageContext} . * * @author Keith Donald */ @@ -11,8 +14,10 @@ public interface ValidationFailureMessageResolverFactory { /** * Creates a new message resolver for the validation failure. - * @param failure the validation failure - * @return the message resolver for the failure + * @param failure a validation failure reported by the validator + * @param modelContext additional information about the model object that failed to validate + * @return the resolver of the failure message */ - MessageResolver createMessageResolver(ValidationFailure failure); + public MessageResolver createMessageResolver(ValidationFailure failure, ValidationFailureModelContext modelContext); + } 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 new file mode 100644 index 00000000..2b55aad5 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/validation/ValidationFailureModelContext.java @@ -0,0 +1,48 @@ +package org.springframework.binding.validation; + +/** + * Provides additional model context regarding a validation failure. Used by a + * {@link ValidationFailureMessageResolverFactory} to resolve failure messages. + */ +public class ValidationFailureModelContext { + + private String objectName; + + private Class propertyType; + + private String propertyTypeConverter; + + /** + * Creates a new validation model context. + * @param objectName the object name + * @param propertyType the property type + * @param propertyTypeConverter the id of the property type converter + */ + public ValidationFailureModelContext(String objectName, Class propertyType, String propertyTypeConverter) { + this.objectName = objectName; + this.propertyType = propertyType; + this.propertyTypeConverter = propertyTypeConverter; + } + + /** + * The name of the object being validated. + */ + public String getObjectName() { + return objectName; + } + + /** + * When reporting a property validation failure, the type of the property that failed to validate. + */ + public Class getPropertyType() { + return propertyType; + } + + /** + * When reporting a property validation failure, the id of the custom converter used to format the UI display value. + */ + public String getPropertyTypeConverter() { + return propertyTypeConverter; + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/validation/DefaultValidationContext.java b/spring-webflow/src/main/java/org/springframework/webflow/validation/DefaultValidationContext.java index 58675d52..135b22fc 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/validation/DefaultValidationContext.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/validation/DefaultValidationContext.java @@ -25,8 +25,9 @@ public class DefaultValidationContext implements ValidationContext { this.mappingResults = mappingResults; } - public MessageContext getMessageContext() { - return requestContext.getMessageContext(); + public String getObjectName() { + // TODO + return null; } public String getUserEvent() { @@ -52,6 +53,10 @@ public class DefaultValidationContext implements ValidationContext { // TODO } + public MessageContext getMessageContext() { + return requestContext.getMessageContext(); + } + private static class PropertyMappingResult implements MappingResultsCriteria { private String property;