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: + *
+ * 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;