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 fi
I like the idea from Jakub Jirutka to use Spring Expression Language. If you don't want to add another library/dependency (assuming that you already use Spring), here is a simplified implementation of his idea.
The constraint:
@Constraint(validatedBy=ExpressionAssertValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExpressionAssert {
String message() default "expression must evaluate to true";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value();
}
The validator:
public class ExpressionAssertValidator implements ConstraintValidator<ExpressionAssert, Object> {
private Expression exp;
public void initialize(ExpressionAssert annotation) {
ExpressionParser parser = new SpelExpressionParser();
exp = parser.parseExpression(annotation.value());
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
return exp.getValue(value, Boolean.class);
}
}
Apply like this:
@ExpressionAssert(value="pass == passVerify", message="passwords must be same")
public class MyBean {
@Size(min=6, max=50)
private String pass;
private String passVerify;
}
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<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);
in the above case it would be
Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);
I made a small adaptation in Nicko's solution so that it is not necessary to use the Apache Commons BeanUtils library and replace it with the solution already available in spring, for those using it as I can be simpler:
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
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 object, final ConstraintValidatorContext context) {
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);
boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode(firstFieldName)
.addConstraintViolation();
}
return isValid;
}
}
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();
}
}
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;
}
}
Solution realated with question: How to access a field which is described in annotation property
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Match {
String field();
String message() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MatchValidator.class)
@Documented
public @interface EnableMatchConstraint {
String message() default "Fields must match!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MatchValidator implements ConstraintValidator<EnableMatchConstraint, Object> {
@Override
public void initialize(final EnableMatchConstraint constraint) {}
@Override
public boolean isValid(final Object o, final ConstraintValidatorContext context) {
boolean result = true;
try {
String mainField, secondField, message;
Object firstObj, secondObj;
final Class<?> clazz = o.getClass();
final Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Match.class)) {
mainField = field.getName();
secondField = field.getAnnotation(Match.class).field();
message = field.getAnnotation(Match.class).message();
if (message == null || "".equals(message))
message = "Fields " + mainField + " and " + secondField + " must match!";
firstObj = BeanUtils.getProperty(o, mainField);
secondObj = BeanUtils.getProperty(o, secondField);
result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
if (!result) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
break;
}
}
}
} catch (final Exception e) {
// ignore
//e.printStackTrace();
}
return result;
}
}
And how to use it...? Like this:
@Entity
@EnableMatchConstraint
public class User {
@NotBlank
private String password;
@Match(field = "password")
private String passwordConfirmation;
}