views:

4231

answers:

8

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;
}
+3  A: 

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.

bradhouse
A: 

hi,

i tried this example but the validator is never called. Im using JSF2 (RI, 2.0.1) + HibernateValidator (4.0.2GA). Have you any idea ?

Hirat
A: 

You need to call it explicitly. In the example above, bradhouse has given you all the steps to write a custom constraint.

Add this code in your caller class.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator();

Set> constraintViolations = validator.validate(yourObject);

in the above case it would be Set> constraintViolations = validator.validate(objAccountCreateForm);

Vishal
A: 

thank you for your answer but how can i add this while the caller is JSF?

Is it possible to implement an "Equals" annotation like in ExtVal with JSR-303?

Hirat
btw. you should make these comments, not answers. As for interacting with JSF, I'm not sure. I'm not at all familiar with JSF. Sorry.
bradhouse
ok.i tried AlbertHoven's example but the validation impl throws a exception:Annotated methods must follow the JavaBeans naming convention. match() does not.This occurs if the member is a Method.It seems to be that a simple cross-validation is not possible with jsr 303.There should be an PropertyValidator, which also gives u the ClassObject in your validator.
Hirat
digitaljoel
+1  A: 

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 match() {
    return this.pass == this.passVerify;
  }
}

match() method is invoked by the validator automatically.

Alberthoven
Interesting. It certainly gets around the complexity of associating field level errors correctly when you're using a class level validator. My only concern would be around i18n. For a given 'type' of validation failure I have generic messages for those types. If you only had one @AssertTrue in your app this would be fine, but otherwise I'm not sure it would work (in a generic sense with i18n).
bradhouse
Can't you put a message code into the message attribute, so it goes through the normal validator MessageInterpolator?
GaryF
I think this is a mixing of concerns again. The whole point of Bean Validation is to externalize the validation into ConstraintValidators. In this case you have part of the validation logic in the bean itself and part in the Validator framework. The way to go is a class level constraint. Hibernate Validator also offers now a @ScriptAssert which makes the implementation of bean internal dependencies easier.
Hardy
A: 

Very nice solution bradhouse. Is there any way to apply the @Matches annotation to more than one field?

EDIT: Here's the solution I came up with to answer this question, I modified the Constraint to accept an array instead of a single value:

@Matches(fields={"password", "email"}, verifyFields={"confirmPassword", "confirmEmail"})
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 code for the annotation:

package springapp.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 "{springapp.util.constraints.matches}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  String[] fields();

  String[] verifyFields();
}

And the implementation:

package springapp.util.constraints;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.beanutils.BeanUtils;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

    private String[] fields;
    private String[] verifyFields;

    public void initialize(Matches constraintAnnotation) {
        fields = constraintAnnotation.fields();
        verifyFields = constraintAnnotation.verifyFields();
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {

        boolean matches = true;

        for (int i=0; i<fields.length; i++) {
            Object fieldObj, verifyFieldObj;
            try {
                fieldObj = BeanUtils.getProperty(value, fields[i]);
                verifyFieldObj = BeanUtils.getProperty(value, verifyFields[i]);
            } catch (Exception e) {
                //ignore
                continue;
            }
            boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
            if (neitherSet) {
                continue;
            }

            boolean tempMatches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

            if (!tempMatches) {
                addConstraintViolation(context, fields[i]+ " fields do not match", verifyFields[i]);
            }

            matches = matches?tempMatches:matches;
        }
        return matches;
    }

    private void addConstraintViolation(ConstraintValidatorContext context, String message, String field) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addNode(field).addConstraintViolation();
    }
}
McGin
Hmm. Not sure. You could try either creating specific validators for each confirmation field (so they have different annotations), or updating the @Matches annotation to accept multiple pairs of fields.
bradhouse
Thanks bradhouse, came up with a solution and have posted it above. It needs a little work to cater for when differing number of arguements are passed so you don't get IndexOutOfBoundsExceptions, but the basics are there.
McGin
+9  A: 

Each field match constraint should be handled by a distinct validator annotation. The JSR-303 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 implementation:

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;
    }
}
Nicko
Nice implementation with no external dependencies.
AndyT
@AndyT: There is an external dependency on Apache Commons BeanUtils.
GaryF
A: 

I have tried Alberthoven's example (hibernate-validator 4.0.2.GA) and i get an ValidationException: „Annotated methods must follow the JavaBeans naming convention. match() does not.“ too. After I renamed the method from „match“ to "isValid" it works.

public class Password {

    private String password;

    private String retypedPassword;

    public Password(String password, String retypedPassword) {
        super();
        this.password = password;
        this.retypedPassword = retypedPassword;
    }

    @AssertTrue(message="password should match retyped password")
    private boolean isValid(){
        if (password == null) {
            return retypedPassword == null;
        } else {
            return password.equals(retypedPassword);
        }
    }

    public String getPassword() {
        return password;
    }

    public String getRetypedPassword() {
        return retypedPassword;
    }

}
Ralph