Cross field validation with Hibernate Validator (JSR 303)
Is there an implementation of (or third-party implementation for) cross field validation in Hibernate Validator 4.x? If not, what is the cleanest way to implement a cross field validator?
As an example, how can you use the API to validate two bean properties are equal (such as validating a password field matches the password verify field).
In annotations, I'd expect something like:
public class MyBean {
  @Size(min=6, max=50)
  private String pass;
  @Equals(property="pass")
  private String passVerify;
}
Each field constraint should be handled by a distinct validator annotation, or in other words it's not suggested practice to have one field's validation annotation checking against other fields; cross-field validation should be done at the class level. Additionally, the JSR-303 Section 2.2 preferred way to express multiple validations of the same type is via a list of annotations. This allows the error message to be specified per match.
For example, validating a common form:
@FieldMatch.List({
        @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
        @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationForm  {
    @NotNull
    @Size(min=8, max=25)
    private String password;
    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;
    @NotNull
    @Email
    private String email;
    @NotNull
    @Email
    private String confirmEmail;
}
The Annotation:
package constraints;
import constraints.impl.FieldMatchValidator;
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 that 2 fields have the same value.
 * An array of fields and their matching confirmation fields can be supplied.
 *
 * Example, compare 1 pair of fields:
 * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
 * 
 * Example, compare more than 1 pair of fields:
 * @FieldMatch.List({
 *   @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
 *   @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
 */
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
    String message() default "{constraints.fieldmatch}";
    Class<?>[] groups() default {};
    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 FieldMatch
     */
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
            @interface List
    {
        FieldMatch[] value();
    }
}
The Validator:
package constraints.impl;
import constraints.FieldMatch;
import org.apache.commons.beanutils.BeanUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
    private String firstFieldName;
    private String secondFieldName;
    @Override
    public void initialize(final FieldMatch constraintAnnotation)
    {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }
    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context)
    {
        try
        {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }
        catch (final Exception ignore)
        {
            // ignore
        }
        return true;
    }
}
I suggest you another possible solution. Perhaps less elegant, but easier!
public class MyBean {
  @Size(min=6, max=50)
  private String pass;
  private String passVerify;
  @AssertTrue(message="passVerify field should be equal than pass field")
  private boolean isValid() {
    return this.pass.equals(this.passVerify);
  }
}
isValid() method is invoked by the validator automatically.
I'm surprised this isn't available out of the box. Anyway, here is a possible solution.
I've created a class level validator, not the field level as described in the original question.
Here is the annotation code:
package com.moa.podium.util.constraints;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MatchesValidator.class)
@Documented
public @interface Matches {
  String message() default "{com.moa.podium.util.constraints.matches}";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
  String field();
  String verifyField();
}
And the validator itself:
package com.moa.podium.util.constraints;
import org.mvel2.MVEL;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MatchesValidator implements ConstraintValidator<Matches, Object> {
  private String field;
  private String verifyField;
  public void initialize(Matches constraintAnnotation) {
    this.field = constraintAnnotation.field();
    this.verifyField = constraintAnnotation.verifyField();
  }
  public boolean isValid(Object value, ConstraintValidatorContext context) {
    Object fieldObj = MVEL.getProperty(field, value);
    Object verifyFieldObj = MVEL.getProperty(verifyField, value);
    boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
    if (neitherSet) {
      return true;
    }
    boolean matches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);
    if (!matches) {
      context.disableDefaultConstraintViolation();
      context.buildConstraintViolationWithTemplate("message")
          .addNode(verifyField)
          .addConstraintViolation();
    }
    return matches;
  }
}
Note that I've used MVEL to inspect the properties of the object being validated. This could be replaced with the standard reflection APIs or if it is a specific class you are validating, the accessor methods themselves.
The @Matches annotation can then be used used on a bean as follows:
@Matches(field="pass", verifyField="passRepeat")
public class AccountCreateForm {
  @Size(min=6, max=50)
  private String pass;
  private String passRepeat;
  ...
}
As a disclaimer, I wrote this in the last 5 minutes, so I probably haven't ironed out all the bugs yet. I'll update the answer if anything goes wrong.
链接地址: http://www.djcxy.com/p/61088.html