Custom JSR 303 Constraints
In my previous blog post I talked about validating Spring Web applications using JSR 303 annotations. In that post, we used the out of the box JSR 303 constraints such as @NotEmpty and @Size. These out of the box constraints should be sufficient for most cases. However, there will be situations where you want to develop custom constraints. In this post, we will look at creating a custom constraint
that validates the ISBN number of a book in our Online Bookstore Admin application.
Each JSR 303 validation constraint consists of two parts. The first part is the constraint annotation itself.
package com.inflinx.blog.bookstore.constraint; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy=IsbnValidator.class) public @interface Isbn { String message() default "{Isbn.message}"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; }
The JSR 303 specification requires each constraint annotation to define the following three attributes:
- message – The error message that gets returned upon validation failure. Here we have defined the default value Isbn.message that acts as a resource bundle key. It is possible to just hard code a message by simply omitting the braces.
- groups – This defines the constraint groups that this annotation needs to be associated with. Here we will use the default group.
- Payload – This holds the additional metadata information that can be supplied by validation clients. Here we are using the default empty array.
The @Target, @Retention and @Documented annotations are needed for the annotation declaration. The @Constraint annotation declares the validator that we will be using to validate elements annotated with @Isbn constraint.
The next step in creating the custom constraint is defining the validator.
package com.inflinx.blog.bookstore.constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import org.apache.commons.validator.routines.ISBNValidator; public class IsbnValidator implements ConstraintValidator{ @Override public void initialize(Isbn isbn) { } @Override public boolean isValid(String value, ConstraintValidatorContext validatorContext) { if(value == null || "".equals(value)) { return true; } else { return ISBNValidator.getInstance().isValid(value); } } }
Every constraint validator needs to implement the ConstraintValidator interface. The actual implementation of the IsbnValidator is straightforward. According to the specification, if the annotated element’s value is null or empty the validation should succeed. If you don’t want a null value, the element should be annotated with @NotNull annotation. The actual validation of the ISBN value is delegated to the Apache Validation API.
Now that we have the constraint defined, the next step is to use it in our Book Store application. To do that, we start out by creating a new property in the Book domain class and annotated it with @Isbn annotation. Here is the modified book class:
package com.inflinx.blog.bookstore.domain; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.NotEmpty; import com.inflinx.blog.bookstore.constraint.Isbn; public class Book { @NotEmpty private String name; @Size(min=1, max=50) private String description; @NotEmpty @Isbn private String isbn; public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Name: " + name + ", Description: " + description + ", ISBN: " + isbn; } }
Then we need to modify the form.jsp file to add the ISBN form field.
<form:form method="post" action="book.html" commandName="book"> <table> <tr> <td>Name:</td> <td><form:input path="name" /></td> <td><form:errors path="name" /></td> </tr> <tr> <td>Description:</td> <td><form:textarea path="description"/></td> <td><form:errors path="description" /></td> </tr> <tr> <td>ISBN:</td> <td><form:input path="isbn" /></td> <td><form:errors path="isbn" /></td> </tr> </table> <input type="submit" value="Create" /> </form:form>
The final step in this process is to add a new validation key/value to the messages.properties file. Here is the new messages.properties file:
NotEmpty.book.name=Name is a required field Size.book.description=Description must be between 1 and 50 characters Isbn.book.isbn=Please enter a valid ISBN
Now, when you submit the form with an invalid ISBN value, you will see a validation error.