@Constraint(validatedBy = ZipCodeValidator.class)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface ZipCode {
··String message() default "{org.agoncal.book.javaee7.chapter03.ZipCode.message}";
··Class<?>[] groups() default {};
··Class<? extends Payload>[] payload() default {};
··@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
··@Retention(RUNTIME)
··@interface List {
····ZipCode[] value();
··}
}
В листинге 3.26 показан класс реализации ограничивающей аннотации: ZipCodeValidator реализует интерфейс javax.validation.ConstraintValidator с обобщенным типом String. Метод isValid реализует алгоритм валидации, в рамках которого выполняется сопоставление с шаблоном регулярного выражения и происходит вызов внешнего сервиса: ZipCodeChecker. Код ZipCodeChecker здесь не показан, так как в данном случае он неважен. Но необходимо отметить, что он внедряется (@Inject) с квалификатором CDI (@USA, показан в листинге 3.27). Итак, здесь мы наблюдаем взаимодействие спецификаций CDI и Bean Validation.
public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> {
··@Inject @USA
··private ZipCodeChecker checker;
··private Pattern zipPattern = Pattern.compile("\\d{5}(-\\d{5})?");
··public void initialize(ZipCode zipCode) {
··}
··public boolean isValid(String value, ConstraintValidatorContext context) {
····if (value == null)
······return true;
····Matcher m = zipPattern.matcher(value);
····if (!m.matches())
····return false;
····return checker.isZipCodeValid(value);
··}
}
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD})
public @interface USA {
}
В следующих главах будет показано, как интегрировать валидацию компонентов с другими спецификациями, в частности JPA (можно добавлять ограничения к вашим сущностям) или JSF (можно ограничивать базовые компоненты).
Написание интеграционных тестов CustomerIT и AddressIT
Как мы теперь можем протестировать ограничения, налагаемые на наши компоненты? Мы ведь не можем писать модульные тесты для @Email, так как это аннотация, агрегирующая ограничения, равно как и для @ZipCode, которое может работать только для внедрения (а это контейнерная служба). Проще всего будет написать интеграционный тест, то есть использовать фабрику ValidatorFactory для получения Validator, а потом валидировать наши компоненты.
В листинге 3.28 показан класс CustomerIT, выполняющий интеграционное тестирование компонента Customer. Метод инициализирует Validator (с помощью ValidatorFactory), а метод close() высвобождает фабрику. Класс далее содержит два теста: в одном создается валидный объект Customer, а в другом создается объект с недопустимым адресом электронной почты и проверяется, окончится ли валидация ошибкой.
public class CustomerIT {
··private static ValidatorFactory vf;
··private static Validator validator;
··@BeforeClass
··public static void init() {
····vf = Validation.buildDefaultValidatorFactory();
····validator = vf.getValidator();
··}
··@AfterClass
··public static void close() {
····vf.close();
··}
··@Test
··public void shouldRaiseNoConstraintViolation() {
····Customer customer = new Customer("John", "Smith", "jsmith@gmail.com");
····Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
····assertEquals(0, violations.size());
··}
··@Test
··public void shouldRaiseConstraintViolationCauseInvalidEmail() {
····Customer customer = new Customer("Джон", "Смит", "DummyEmail");
····Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
····assertEquals(1, violations.size());
····assertEquals("invalid email address", violations.iterator(). next(). getMessage());
····assertEquals("dummy", violations.iterator(). next(). getInvalidValue());
····assertEquals("{org.agoncal.book.javaee7.chapter03.Email.message}",
·················violations.iterator(). next(). getMessageTemplate());
··}
}
Листинг 3.29 построен по такому же принципу (Validator создается с помощью фабрики, происходит валидация компонента, фабрика закрывается), но проверяет компонент Address.