In my previous post I showed how one can make use of the default constraints provided by the Bean Validation API in validatng the model data. Bean Validation API allows the developers to define their own constraints by creating a new annotation and writing the validator which is used to validate the value. In this post I will show you how to create custom constraints and then use it to validate your data. I am writing series of articles on Java EE 7 features.
To create a custom constraints you have to identify the following:
- Where the constraint can be used i.e on method, field, another annotation, constructor, method parameter.
- The validator which is used by the constraint to validate the value.
Before I go into the details of creating custom constraint, I will explain the model classes involved in this sample and also the validation constraints I want to impose on the data in my model classes. There are two model classes involved: MyPerson
which holds the data for a person and MyAddress
which holds the address of a person.
public class MyAddress { private String streetAddress; private String locality; private String city; private String state; private String country; private String pinCode; public MyAddress(String streetAddress, String locality, String city, String state, String country, String pinCode) { this.streetAddress = streetAddress; this.locality = locality; this.city = city; this.state = state; this.country = country; this.pinCode = pinCode; } public String getStreetAddress() { return streetAddress; } public void setStreetAddress(String streetAddress) { this.streetAddress = streetAddress; } public String getLocality() { return locality; } public void setLocality(String locality) { this.locality = locality; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getPinCode() { return pinCode; } public void setPinCode(String pinCode) { this.pinCode = pinCode; } }
Now the MyPerson
class uses default constraints for few of its fields and we will build a custom constraint for validating the address
field which we call it as @Address
. Lets look at the code for MyPerson
class:
import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import validators.Address; public class MyPerson { @NotNull @Size(min=4) private String firstName; @NotNull @Size(min=4) private String lastName; @Address private MyAddress address; public MyPerson(String firstName, String lastName, MyAddress address) { this.firstName = firstName; this.lastName = lastName; this.address = address; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public MyAddress getAddress() { return address; } public void setAddress(MyAddress address) { this.address = address; } }
Now that we have seen our model classes, let me dive into creating the custom constraint. One would have to do the following:
- Create annotation for using it in the fields or methods of the model class.
- Create a validator which is used for validating the value.
Creating a Validator
Before I show you how to create the validator, let me state the constraints which must be satisfied by every instance of MyAddress class:
- The address field in the
MyPerson
class should not be null - The instance of MyAdress should have all the fields defined i.e not even a single field should be null.
- The pin code fields in MyAdress should be of atleast 6 characters.
- The country field in MyAddress should be of atleast 4 characters.
The validator has to implement the interface javax.validation.ConstraintValidator<A extends java.lang.annotation.Annotation,T>
and override the initialize
and isValid
methods.
From the Java doc for javax.validation.ConstraintValidator<A extends java.lang.annotation.Annotation,T>
: Defines the logic to validate a given constraint A for a given object type T.
When we override the isValid
method we get a reference to the value which is assigned to the field for which we are writing the constraint which in this example is an instance of MyAddress class. The isValid
method implementation will check for the constraints mentioned above and return a boolean value to indicate if the value satifies our constraints or not. Lets look at the imeplementation of the validator:
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import model.MyAddress; public class AddressValidator implements ConstraintValidator<Address, MyAddress> { @Override public void initialize(Address constraintAnnotation) { } /** * 1. The address should not be null. * 2. The address should have all the data values specified. * 3. Pin code in the address should be of atleast 6 characters. * 4. The country in the address should be of atleast 4 characters. */ @Override public boolean isValid(MyAddress value, ConstraintValidatorContext context) { if (value == null) { return false; } if (value.getCity() == null || value.getCountry() == null || value.getLocality() == null || value.getPinCode() == null || value.getState() == null || value.getStreetAddress() == null) { return false; } if (value.getPinCode().length() < 6) { return false; } if (value.getCountry().length() < 4) { return false; } return true; } }
Creating Custom Constraint Annotation
Now that we have the validator ready the below code shows how to create an annotation @Address
for our address
field in MyPerson
class.
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; //Linking the validator I had shown above. @Constraint(validatedBy = {AddressValidator.class}) //This constraint annotation can be used only on fields and method parameters. @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface Address { //The message to return when the instance of MyAddress fails the validation. String message() default "Invalid Address"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Test the New Custom Nonstraint using JUnit
Let’s try out the custom constraint you have created by now. For this I will create a JUnit test and then make use of javax.validation.Validator
as I did in my previous post to simulate the validation of the data.
import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import org.junit.Test; import static org.junit.Assert.*; import org.junit.BeforeClass; public class MyPersonTest { public MyPersonTest() { } private static ValidatorFactory vf; private static Validator validator; private static MyAddress address; private static MyPerson person; /** * Setting up the validator and model data. */ @BeforeClass public static void setup(){ vf = Validation.buildDefaultValidatorFactory(); validator = vf.getValidator(); address = new MyAddress("#12, 4th Main", "XYZ Layout", "Bangalore", "Karnataka", "India", "560045"); person = new MyPerson("First Name", "Last Name", address); } /** * Validating the model data which has correct values. */ @Test public void testCorrectAddress(){ System.out.println("********** Running validator with corret address **********"); Set<ConstraintViolation<MyPerson>> violations = validator.validate(person); assertEquals(violations.size(), 0); } /** * Validating the model data which has incorrect values. */ @Test public void testInvalidAddress(){ System.out.println("********** Running validator against wrong address **********"); //setting address itself as null person.setAddress(null); validateAndPrintErrors(); //One of the address fields as null. address.setCity(null); person.setAddress(address); validateAndPrintErrors(); //Setting pin code less than 6 characters. address.setPinCode("123"); address.setCity("Bangalore"); validateAndPrintErrors(); //Setting country name with less than 4 characters address.setPinCode("123456"); address.setCountry("ABC"); validateAndPrintErrors(); } private void validateAndPrintErrors(){ Set<ConstraintViolation<MyPerson>> violations = validator.validate(person); for ( ConstraintViolation<MyPerson> viol : violations){ System.out.println(viol.getMessage()); } assertEquals(1, violations.size()); } }
And the output will be:
********** Running validator with corret address ********** ********** Running validator against wrong address ********** Invalid Address Invalid Address Invalid Address Invalid Address