Sunday, February 17, 2013

JSR 303 Custom Constraint Validator

In this article, we do not detail about JSR 303, for working with JSR 303, please read SpringMVC 3 + JSR 303 validation first.

Why we need a custom constraint validator, of course there are too much reasons for this question. Here I will show you 2 reasons:
  • I need a new validator to validate a field matches a specified pattern or not
  • I need to make the comparison between two fields (cross field validation)
So, let walk through these steps to realize this purpose

1. Import BeanUtils


<dependency>

    <groupId>commons-beanutils</groupId>

    <artifactId>commons-beanutils</artifactId>

    <version>1.8.3</version>

</dependency>

I use this util to get the value of a field in an object. This util is not important, you can use another one

2. Create a new properties used in Custom Constraint Validator

Create new ValidationMessages.properties file in folder src/main/resources
When <form:errors> renders error messages on form, it will look in this file, for more information, please read Message interpolation.

Note: by default, the normal Hibernate Validator still reads messages from application.properties

3. Create a Custom Constraint Validator

There are many ways to create a custom constraint validator, here I will show you two ways.

3.1 Custom Constraint Validator to validate a field

According to Hibernate, to create a custom constraint, the following three steps are required:
  • Create a constraint annotation
  • Implement a validator
  • Define a default error message

3.1.1 Constraint annotation

I do not detail about how to create an annotation in this article, for more detail, read it in oracle
@Target({ElementType.METHOD, ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

@Constraint(validatedBy=CodeValidatorImpl.class)

public @interface CodeValidator {

    String message() default "{code.not.correct}";



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



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

}

  • Line 1, it means: this annotation is applied for a field
  • Line 2, it means: annotations of this type will be available at runtime by the means of reflection
  • Line 3 specifies the validator to be used to validate elements annotated with @CodeValidator
  • Line 5 defines the default message of this validator

3.1.2 Constraint validator

public class CodeValidatorImpl implements ConstraintValidator<CodeValidator, String> {



    @Override

    public void initialize(CodeValidator arg0) {

    }



    @Override

    public boolean isValid(String codeValue, ConstraintValidatorContext ctx) {

        return checkNull(codeValue, ctx) && checkMatched(codeValue, ctx);

    }



    private boolean checkNull(String codeValue, ConstraintValidatorContext ctx) {

        boolean isValid;



        if (codeValue == null || codeValue.equals("")) {

            isValid = false;

        } else {

            isValid = true;

        }

        

        if (!isValid) {

            ctx.disableDefaultConstraintViolation();

            ctx.buildConstraintViolationWithTemplate("{code.required}").addConstraintViolation();

        }

        

        return isValid;

    }

    

    private boolean checkMatched(String codeValue, ConstraintValidatorContext ctx) {

        Pattern pattern = Pattern.compile("[0-9a-zA-Z]*");

        

        Matcher matcher = pattern.matcher(codeValue);

        

        if (!matcher.matches()) {

            ctx.disableDefaultConstraintViolation();

            ctx.buildConstraintViolationWithTemplate("{code.contain.only}").addConstraintViolation();

            

            return false;

        } else {

            return true;

        }

    }

}

  • Line 1, ConstraintValidator<CodeValidator, String> type String means we use this validator to validate a String field.
  • Line 15, Make your validation in isValid function

3.1.3 Error message


In this example, I use ConstraintValidatorContext to create the custom error message
ctx.disableDefaultConstraintViolation();

ctx.buildConstraintViolationWithTemplate("{code.required}").addConstraintViolation();

These commands mean: Do not use the default message, let use the message with key = "code.required" in properties file (ValidationMessages.properties)

3.1.4 In Bean

Use the custom constraint as well as the normal constraint
public class User {



    @NotEmpty(message="user.firstName")

    private String firstName;

    

    @NotEmpty

    private String lastName;

    

    @CodeValidator

    private String code; 

....

}

3.2 Custom constraint for cross field validation

There are a few difference with the first way.

3.2.1 Constraint annotation

@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Constraint(validatedBy=PasswordMatchImpl.class)

public @interface PasswordMatch {



    String message() default "{notmatch.password}";



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



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

    

    /**

     * First field

     */

    String password();

    

    /**

     * Second field

     */

    String repassword();

}

  • Difference in Target
  • We require 2 fields in declaration of annotation

3.2.2 Constraint validator

public class PasswordMatchImpl implements ConstraintValidator<PasswordMatch, Object> {



    private String password;

    

    private String repassword;

    

    @Override

    public void initialize(PasswordMatch pm) {

        password = pm.password();

        repassword = pm.repassword();

    }



    @Override

    public boolean isValid(Object obj, ConstraintValidatorContext ctx) {

        

        try {

            // get field value

            Object pw = BeanUtils.getProperty(obj, password);

            Object rpw = BeanUtils.getProperty(obj, repassword);



            return pw != null && rpw != null && pw.equals(rpw);

        } catch (Exception ex) {

            return false;

        }

    }

}

  • ConstraintValidator<PasswordMatch, Object>, the argument type is Object
  • Initialize the attribute in initialize function (it's the name of field, not the value of field)
  • We get value of field through the obj parameter

3.2.3 In Bean

@PasswordMatch(password="password", repassword="repassword")
public class User {

    @NotEmpty(message="user.firstName")
    private String firstName;

...

}

  • We validate for an object, not for a field
  • The parameters are required

4. Error message in JSP

As normal validation, we use <form:errors> to show the error message
<tr>
    <td><form:label path="firstName">First Name</form:label></td>
    <td>
        <form:input path="firstName" />
        <form:errors path="firstName" class="errors" />
    </td>
</tr>


References:
Source code:

1 comment:

  1. Do you have any idea if cross field validation could be applied at field level?

    So for example above tutorial will display the "password not match" at top of the page, what if i need the message to get displayed at field level?

    ReplyDelete