Hello,
In large code bases, there may be times where developers use a Facade pattern to get a value from a different object. For example in the UserBean object below, getting the email actually comes from the User class. I'd like to add a @ValidateReference annotation (or something like it) to the Spec that references to the object and field for validation. In the current implementation, I have to validate twice using validator.validateValue(User.class, "email", email, EmailGroup.class) to validate the email is formatted correctly and then validator.validate(userBean) to validate that both fields match. I would prefer to validate once on the email property using validator.validate(userBean, EmailGroup.class) -- with the @ValidateReference addition. I could simply copy and paste the email constraints from User to UserBean but then I'd get complaints of not being DRY (Don't Repeat Yourself).
Please correct me if my understanding above is incorrect.
Code:
public class User
{
private String name;
private String email;
@Size(min = 3, message = "Name should at least be 3 characters long")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Pattern(regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", message = "This is not a valid email", groups = EmailGroup.class)
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Code:
@FieldsValueMatch(first="user.email", second = "confirmEmail", groups= EmailGroup.class)
public class UserBean {
private User user = new User(); // more likely injected in a web form.
private String confirmEmail;
public String getName() {
return user.getName();
}
public void setName(String name) {
this.user.setName( name);
}
@ValidateReference(refClass=User.class method="email" groups=EmailGroup.class)
public String getEmail() {
return user.getEmail();
}
public void setEmail(String email) {
this.user.setEmail(email);
}
public String getConfirmEmail()
{
return confirmEmail;
}
public void setConfirmEmail(String email)
{
this.confirmEmail = email;
}
}
Code:
package com.networkfleet.constraints;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.networkfleet.constraints.FieldsValueMatchValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
/**
* Validation annotation to validate 2 fields that have the same value.
* An array of fields and their matching confirmation fields can be supplied.
*
* Example, compare email fields in an HTML form:
* @ValueMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
*
* Example, compare more than 1 pair of fields:
* @ValueMatch.List({
* @ValueMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
* @ValueMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
*/
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Documented
public @interface FieldsValueMatch
{
String message() default "Fields don't match";
Class<?>[] groups() default {};
boolean ignoreCase() default false;
Class<? extends Payload>[] payload() default {};
/**
* @return The first field
*/
String first();
/**
* @return The second field
*/
String second();
/**
* Defines several <code>@FieldMatch</code> annotations on the same element
*
* @see FieldsValueMatch
*/
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List
{
FieldsValueMatch[] value();
}
}
Code:
package com.networkfleet.constraints;
import org.apache.commons.beanutils.BeanUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FieldsValueMatchValidator implements ConstraintValidator<FieldsValueMatch, Object>
{
private String firstFieldName;
private String secondFieldName;
private boolean ignoreCase;
@Override
public void initialize(final FieldsValueMatch constraintAnnotation)
{
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
ignoreCase = constraintAnnotation.ignoreCase();
}
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context)
{
try
{
final String firstObj = BeanUtils.getProperty(value, firstFieldName);
final String secondObj = BeanUtils.getProperty(value, secondFieldName);
if (firstObj == null)
{
return secondObj == null;
}
return (ignoreCase)?firstObj.equalsIgnoreCase(secondObj):firstObj.equals(secondObj);
}
catch (final Exception exception)
{
throw new IllegalArgumentException("Could not compare field("+firstFieldName+") with other field ("+secondFieldName+") because one (or more) of the field name(s) is invalid.", exception);
}
}
}