Validation context polishing

This commit is contained in:
Keith Donald
2009-02-27 20:56:15 +00:00
parent 55b3dc58a1
commit 875e0aa64a
9 changed files with 358 additions and 70 deletions

View File

@@ -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

View File

@@ -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:
* <ol>
* <li>Try the ${failureMessageCodePrefix}.${objectName}.${propertyName}.${constraint} code; if match, resolve message
* and return.
* <li>Try the ${failureMessageCodePrefix}.${propertyType}.${constraint} code; if matched, resolve message and return.
* <li>Try the ${failureMessageCodePrefix}.${constrant} code; if matched, resolve message and return.
* </ol>
*
* 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:
*
* <pre>
* 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
* </pre>
* @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");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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;
}
/**

View File

@@ -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:
*
* <pre>
* new ValidationFailureBuilder().forProperty(&quot;foo&quot;).constraint(&quot;required&quot;).build();
* </pre>
* @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);
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}