From ffce230b012ea7265cb4a827ce0e4e7986b3113b Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Tue, 3 Mar 2009 21:38:02 +0000 Subject: [PATCH] validation testing --- ...lidationFailureMessageResolverFactory.java | 2 +- .../binding/validation/ValidationContext.java | 39 +++++++++- .../binding/validation/ValidationFailure.java | 75 ++++++++++++------- .../validation/ValidationFailureBuilder.java | 63 +++++++++++++--- .../validation/AccountRegistrationForm.java | 23 ++++++ .../validation/ConfirmationConstraint.java | 45 +++++++++++ .../binding/validation/LengthConstraint.java | 32 ++++++++ .../PasswordStrengthConstraint.java | 22 ++++++ .../validation/RequiredConstraint.java | 11 +++ .../validation/DefaultValidationContext.java | 16 ++++ 10 files changed, 287 insertions(+), 41 deletions(-) create mode 100644 spring-binding/src/test/java/org/springframework/binding/validation/AccountRegistrationForm.java create mode 100644 spring-binding/src/test/java/org/springframework/binding/validation/ConfirmationConstraint.java create mode 100644 spring-binding/src/test/java/org/springframework/binding/validation/LengthConstraint.java create mode 100644 spring-binding/src/test/java/org/springframework/binding/validation/PasswordStrengthConstraint.java create mode 100644 spring-binding/src/test/java/org/springframework/binding/validation/RequiredConstraint.java 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 ae740576..f03c19a4 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 @@ -124,7 +124,7 @@ public class DefaultValidationFailureMessageResolverFactory implements Validatio public Message resolveMessage(MessageSource messageSource, Locale locale) { DefaultMessageSourceResolvable resolvable = new DefaultMessageSourceResolvable(failureMessageCodesFactory - .createMessageCodes(failure, modelContext), failure.getDefaultMessage()); + .createMessageCodes(failure, modelContext), failure.getMessage()); String text = messageSource.getMessage(resolvable, locale); Expression expression = expressionParser.parseExpression(text, new FluentParserContext() .evaluate(Map.class).template()); 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 01153f61..b4dacb40 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 @@ -46,10 +46,41 @@ public interface ValidationContext { public Object getUserValue(String property); /** - * Add a validation failure to this context. Called by a validator to report failures against the current object - * being validated. Use to report a general failure against the object, or a specific failure against a property on - * the object. The failure code provided is typically mapped to one or message codes that result in a Message being - * added to the {@link MessageContext}. + * Set the property of the model that is about to be validated. This property becomes the "current property" + * associated with this context. + * @param property the name of the property that will be validated + */ + public void setProperty(String property); + + /** + * Validate the current property using the constraint provided. The value of the current property will be passed + * directly to the constraint for validation. Internally, the constraint may use {@link #addDefaultFailure()} to + * report a default failure, or {@link #addFailure(ValidationFailure)} to report a custom failure. + * @param constraint the validation constraint to invoek + */ + public void validate(Object constraint); + + /** + * Validate the current property context using the constraint provided. Call this method when additional context + * besides just the property value is required for constraint validation. Internally, the constraint may use + * {@link #addDefaultFailure()} to report a default failure, or {@link #addFailure(ValidationFailure)} to report a + * custom failure. + * @param constraint the validation constraint to invoke + * @param propertyContext the property context object to validate + */ + public void validate(Object constraint, Object propertyContext); + + /** + * Add the default validation failure for the current context. Called by a validation constraint to report failures + * against the current object being validated. + */ + public void addDefaultFailure(); + + /** + * Add a validation failure to this context. Called by a validation constraint to report failures against the + * current object being validated. Use to report a general failure against the object, or a specific failure against + * a property on the object. The failure code provided is typically mapped to one or message codes that result in a + * Message being added to the {@link MessageContext}. * @param failure the validation failure * @see #getMessageContext() * @see ValidationFailureMessageResolverFactory 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 019ee28d..edd14594 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 @@ -15,6 +15,7 @@ */ package org.springframework.binding.validation; +import java.util.Collections; import java.util.Map; import org.springframework.binding.message.Severity; @@ -22,9 +23,10 @@ import org.springframework.util.Assert; /** * 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. + * "required", "length", "invalidFormat". A failure has a severity describing the intensity of the violation. A failure + * may have an explicit message summarizing what went wrong, and may also provide additional details, such as a cause or + * suggested recovery action. A failure may also have additional arguments that can be used as named parameters in any + * UI display messages. */ public class ValidationFailure { @@ -34,38 +36,43 @@ public class ValidationFailure { private Severity severity; - private Map arguments; + private String message; - private String defaultMessage; + private Map details; + + private Map arguments; /** * Creates a new validation failure - * @param property 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 property the property that failed to validate (may be null) + * @param constraint the name of the validation constraint that failed (may be null) + * @param message an explicit failure message (may be null) + * @param details additional failure details (may be null) * @param arguments named failure arguments (may be null) - * @param defaultMessage the default message text (may be null) */ - public ValidationFailure(String property, String constraint, Severity severity, Map arguments, String defaultMessage) { - Assert.hasText(constraint, "The constraint is required"); + public ValidationFailure(Severity severity, String property, String constraint, String message, Map details, + Map arguments) { Assert.notNull(severity, "The severity is required"); this.property = property; this.constraint = constraint; - this.severity = severity; - this.arguments = arguments; - this.defaultMessage = defaultMessage; + this.message = message; + this.details = details != null ? Collections.unmodifiableMap(details) : Collections.EMPTY_MAP; + this.arguments = arguments != null ? Collections.unmodifiableMap(arguments) : Collections.EMPTY_MAP; } /** - * The name of the property that failed to validate. May be null to indicate a general failure against the validated - * object. + * The name of the property that failed to validate. May be null to indicate a failure against the currently + * validating object. */ public String getProperty() { return property; } /** - * The validation constraint that caused this failure to be reported. + * The name of the validation constraint that caused this failure to be reported. This constraint name can be used + * to resolve the failure message if no explicit {@link #getMessage()} is configured. May be null to indicate a + * failure against the currently validating constraint. */ public String getConstraint() { return constraint; @@ -79,20 +86,34 @@ public class ValidationFailure { } /** - * An map of arguments that can be used as named parameters in the message associated with this validation failure. - * Each constraint that can fail defines a set of arguments that are specific to it. For example, a range constriant - * might define arguments of "min" and "max" of Integer values. In the message bundle, you then might see - * "range=The ${label} field value must be between ${min} and ${max}". + * The message summarizing this failure. May be a literal string or a resolvable message code. Can be null. If null, + * the failure message will be resolved using the constraint that was being validated when this failure was + * reported. + */ + public String getMessage() { + return message; + } + + /** + * A map of details providing additional information about this failure. Each entry in this map is a failure detail + * item that has a name and value. The name uniquely identifies the failure detail and describes its purpose; for + * example, a "cause" or "recommendedAction". The value is the failure detail message, either a literal string or + * resolvable code. If resolvable, the detail code is relative to the constraint associated with this failure. + * Returns an empty map if no details are present. + */ + public Map getDetails() { + return details; + } + + /** + * An map of arguments that can be used as named parameters in resolvable messages associated with this validation + * failure. Each constraint that can fail defines a set of arguments that are specific to it. For example, a length + * constraint might define arguments of "min" and "max" of Integer values. In the message bundle, you then might see + * "length=The ${label} field value must be between ${min} and ${max}". Returns an empty map if no arguments are + * present. */ public Map getArguments() { return arguments; } - /** - * The default message to display if no suitable display message can be resolved for this failure. - */ - public String getDefaultMessage() { - return defaultMessage; - } - } 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 1b4c364f..21d3418e 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 @@ -15,6 +15,7 @@ */ package org.springframework.binding.validation; +import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -41,7 +42,27 @@ public class ValidationFailureBuilder { private Map args; - private String defaultText; + private String message; + + private Map details; + + /** + * 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. This is the default Severity. + * @return this, for fluent call chaining + */ + public ValidationFailureBuilder error() { + severity = Severity.ERROR; + return this; + } /** * Sets the property the failure occurred against. @@ -64,20 +85,38 @@ public class ValidationFailureBuilder { } /** - * Sets the failure to be of warning severity. + * Sets an explicit failure message. The value may be a literal string or a resolvable message code. + * @param message the failure message * @return this, for fluent call chaining */ - public ValidationFailureBuilder warning() { - severity = Severity.WARNING; + public ValidationFailureBuilder message(String message) { + this.message = message; return this; } /** - * Sets the failure to be of error severity. + * Add a detail to associate with the failure. The value provided serves as both the logical name of the detail and + * the code used to resolve the detail message text. + * @param nameAndValue the value to use as both the logical name of the message and the constraint-relative message + * code; for example, "cause" or "recommendedAction". * @return this, for fluent call chaining */ - public ValidationFailureBuilder error() { - severity = Severity.ERROR; + public ValidationFailureBuilder detail(String nameAndValue) { + return detail(nameAndValue, nameAndValue); + } + + /** + * Add a detail to associate with the failure. + * @param name the logical name of the message; for example, "cause" or "recommendedAction" + * @param value the detail value, either a hard coded message string or a constraint-relative message code used to + * resolve the detail message text + * @return this, for fluent call chaining + */ + public ValidationFailureBuilder detail(String name, String value) { + if (details == null) { + details = new HashMap(); + } + details.put(name, value); return this; } @@ -108,14 +147,20 @@ public class ValidationFailureBuilder { } /** - * Build the ValidationFailure. Called after setting builder properties. + * Build the ValidationFailure. Call after setting builder properties. + * @see #forProperty(String) + * @see #constraint(String) + * @see #message(String) + * @see #detail(String) + * @see #arg(String, Object) + * @see #resolvableArg(String, String) * @return this, for fluent call chaining */ public ValidationFailure build() { if (severity == null) { severity = Severity.ERROR; } - return new ValidationFailure(property, constraint, severity, args, defaultText); + return new ValidationFailure(severity, property, constraint, message, details, args); } } \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/validation/AccountRegistrationForm.java b/spring-binding/src/test/java/org/springframework/binding/validation/AccountRegistrationForm.java new file mode 100644 index 00000000..4c3cec51 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/validation/AccountRegistrationForm.java @@ -0,0 +1,23 @@ +package org.springframework.binding.validation; + +import org.springframework.binding.validation.ConfirmationConstraint.ConfirmationForm; + +public class AccountRegistrationForm { + private String username; + private String password; + private String confirmedPassword; + + public void validate(ValidationContext context) { + context.setProperty("username"); + context.validate(new RequiredConstraint()); + context.validate(new LengthConstraint(3, 10)); + + context.setProperty("password"); + context.validate(new RequiredConstraint()); + context.validate(new LengthConstraint(6, 10)); + context.validate(new PasswordStrengthConstraint()); + + context.setProperty("confirmedPassword"); + context.validate(new ConfirmationConstraint(), new ConfirmationForm(password, confirmedPassword)); + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/validation/ConfirmationConstraint.java b/spring-binding/src/test/java/org/springframework/binding/validation/ConfirmationConstraint.java new file mode 100644 index 00000000..4a3a911b --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/validation/ConfirmationConstraint.java @@ -0,0 +1,45 @@ +package org.springframework.binding.validation; + +import org.springframework.util.ObjectUtils; + +public class ConfirmationConstraint { + + public void validate(ConfirmationForm form, ValidationContext context) { + if (form.getValue() == null) { + return; + } + if (!ObjectUtils.nullSafeEquals(form.getValue(), form.getConfirmedValue())) { + context.addDefaultFailure(); + } + } + + public static class ConfirmationForm { + + private Object value; + + private Object confirmedValue; + + public ConfirmationForm(String value, String confirmedValue) { + this.value = value; + this.confirmedValue = confirmedValue; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public Object getConfirmedValue() { + return confirmedValue; + } + + public void setConfirmedValue(Object confirmedValue) { + this.confirmedValue = confirmedValue; + } + + } + +} \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/validation/LengthConstraint.java b/spring-binding/src/test/java/org/springframework/binding/validation/LengthConstraint.java new file mode 100644 index 00000000..f83be355 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/validation/LengthConstraint.java @@ -0,0 +1,32 @@ +package org.springframework.binding.validation; + +public class LengthConstraint { + + private Integer min; + + private Integer max; + + public LengthConstraint(int min, int max) { + this.min = new Integer(min); + this.max = new Integer(max); + } + + public Integer getMin() { + return min; + } + + public Integer getMax() { + return max; + } + + public void validate(String value, ValidationContext context) { + if (value == null || value.length() < min.intValue() || value.length() > max.intValue()) { + context.addFailure(createFailure()); + } + } + + protected ValidationFailure createFailure() { + return new ValidationFailureBuilder().arg("min", getMin()).arg("max", getMax()).build(); + } + +} diff --git a/spring-binding/src/test/java/org/springframework/binding/validation/PasswordStrengthConstraint.java b/spring-binding/src/test/java/org/springframework/binding/validation/PasswordStrengthConstraint.java new file mode 100644 index 00000000..ced40700 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/validation/PasswordStrengthConstraint.java @@ -0,0 +1,22 @@ +package org.springframework.binding.validation; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PasswordStrengthConstraint { + + private Pattern pattern = Pattern.compile("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])[0-9a-zA-Z]$"); + + public void validate(String password, ValidationContext context) { + Matcher matcher = pattern.matcher(password); + if (!matcher.find()) { + context.addFailure(createFailure()); + } + } + + protected ValidationFailure createFailure() { + return new ValidationFailureBuilder().warning().detail("cause").detail("recommendedAction") + .build(); + } + +} \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/validation/RequiredConstraint.java b/spring-binding/src/test/java/org/springframework/binding/validation/RequiredConstraint.java new file mode 100644 index 00000000..1f80538b --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/validation/RequiredConstraint.java @@ -0,0 +1,11 @@ +package org.springframework.binding.validation; + +public class RequiredConstraint { + + public void validate(Object value, ValidationContext context) { + if (value == null) { + context.addDefaultFailure(); + } + } + +} \ No newline at end of file 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 168d223b..aa6da78e 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 @@ -48,6 +48,22 @@ public class DefaultValidationContext implements ValidationContext { return result != null ? result.getOriginalValue() : null; } + public void setProperty(String property) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + public void validate(Object constraint, Object propertyContext) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + public void validate(Object constraint) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + public void addDefaultFailure() { + throw new UnsupportedOperationException("Not yet implemented"); + } + public void addFailure(final ValidationFailure failure) { ValidationFailureModelContext modelContext = new ValidationFailureModelContext() { public String getModel() {