Friday, 29 July 2011

Adding Optional Arguments to a JSR 303 Custom Constraint

When looking at JSR 303 you may have noticed that you can add argument values to some of the built-in constraints. For example:

  @Size(min = 2, max = 25)
 
private String street;

or

  @Min(value=10)
 
private int greenBottlesHangingOnTheWar;

This blog demonstrates how to add optional arguments similar to those described above to your own JSR 303 custom constraints. I’m going to use the Address object that I’ve used in other blogs, only this time I’ll be validating the country attribute against a list of acceptable country codes. The annotation class is called Country and will be applied to the Address bean in the following manner:

public class Address {

 
@Country(values = { "UK", "US", "FR", "DE", "ES", "PL", "AU" }, message = "Invalid Country Code")
 
private String country;

 
// Other attributes and methods go here

There are two classes to write for a JSR 303 custom validator: the annotation interface and the validator. The Country annotation code is:

@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CountryValidator.class)
public @interface Country {

 
String message() default "{country.constraint}";

  String
[] values() default "UK";

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

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

In the code above, the crucial line is the values string array. This has a default of “UK”, which means that if the values argument is left out of the country attribute annotation shown above, then country must contain “UK”.

The second JSR 303 class is the validator class:

public class CountryValidator implements ConstraintValidator<Country, String> {

 
private List<String> countries;

 
public void initialize(Country constraintAnnotation) {

   
countries = Arrays.asList(constraintAnnotation.values());
    System.out.println
("Validating the following countries: " + countries);
 
}

 
public boolean isValid(String value, ConstraintValidatorContext context) {

   
if (countries.contains(value)) {
     
return true;
   
}

   
return false;
 
}
}


The idea here is that in the initialize() method you get hold of the @Country annotation constraint and call its values() method to get an array of acceptable country codes. These are then stored as a list for later use in the isValid() method.

Finally, to prove that this works there is the following JUnit code:

public class CountryTest {

 
/**
   * The validator to be used for object validation. Will be retrieved once for all test
   * methods.
   */
 
private static Validator validator;

 
/** The instance to test */
 
private Address address;

 
/** Retrieves the validator instance. */
 
@BeforeClass
 
public static void setUpClass() {

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

 
@Before
 
public void setUp() {

   
address = new Address();
 
}

 
/**
   * One constraint violation owing to country code being null
   */
 
@Test
 
public void countryIsNull() {

   
Set<ConstraintViolation<Address>> constraintViolations = validator.validate(address);
    assertEquals
(1, constraintViolations.size());
    printViolations
(constraintViolations);
 
}

 
private void printViolations(Set<ConstraintViolation<Address>> constraintViolations) {

   
for (ConstraintViolation<Address> violation : constraintViolations) {

     
String invalidValue = (String) violation.getInvalidValue();
      String message = violation.getMessage
();
      System.out.println
("Found constraint violation. Value: " + invalidValue
          +
" Message: " + message);
   
}
  }

 
/**
   * One constraint violation owing to country being invalid
   */
 
@Test
 
public void countryCodeIsInvalid() {

   
address.setCountry("ZR");

    Set<ConstraintViolation<Address>> constraintViolations = validator.validate
(address);
    assertEquals
(1, constraintViolations.size());
    printViolations
(constraintViolations);
 
}

 
/**
   * Valid country code, so no violations.
   */
 
@Test
 
public void countryCodeIsValid() {

   
address.setCountry("US");

    Set<ConstraintViolation<Address>> constraintViolations = validator.validate
(address);
    assertEquals
(0, constraintViolations.size());
 
}
}

No comments: