Validation context polishing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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("foo").constraint("required").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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user