Спящая проверка коллекций примитивов

Я хочу иметь возможность сделать что-то вроде:

@Email
public List<String> getEmailAddresses()
{
   return this.emailAddresses;
}

Другими словами, я хочу, чтобы каждый элемент в списке проверялся как адрес электронной почты. Конечно, такое аннотирование коллекции неприемлемо.

Есть ли способ сделать это?


person scrotty    schedule 29.11.2010    source источник


Ответы (6)


Ни JSR-303, ни Hibernate Validator не имеют готовых ограничений, которые могут проверять каждый элемент Collection.

Одним из возможных решений этой проблемы является создание пользовательского ограничения @ValidCollection и соответствующей реализации валидатора ValidCollectionValidator.

Для проверки каждого элемента коллекции нам нужен экземпляр Validator внутри ValidCollectionValidator; и чтобы получить такой экземпляр, нам нужна собственная реализация ConstraintValidatorFactory.

Посмотрите, нравится ли вам следующее решение...

Просто,

  • скопируйте и вставьте все эти классы Java (и импортируйте соответствующие классы);
  • добавить файлы validation-api, hibenate-validator, slf4j-log4j12 и testng в путь к классам;
  • запустить тест-кейс.

Действительная коллекция

    public @interface ValidCollection {

    Class<?> elementType();

    /* Specify constraints when collection element type is NOT constrained 
     * validator.getConstraintsForClass(elementType).isBeanConstrained(); */
    Class<?>[] constraints() default {};

    boolean allViolationMessages() default true;

    String message() default "{ValidCollection.message}";

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

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

}

Валидатор ValidCollectionValidator

    public class ValidCollectionValidator implements ConstraintValidator<ValidCollection, Collection>, ValidatorContextAwareConstraintValidator {

    private static final Logger logger = LoggerFactory.getLogger(ValidCollectionValidator.class);

    private ValidatorContext validatorContext;

    private Class<?> elementType;
    private Class<?>[] constraints;
    private boolean allViolationMessages;

    @Override
    public void setValidatorContext(ValidatorContext validatorContext) {
        this.validatorContext = validatorContext;
    }

    @Override
    public void initialize(ValidCollection constraintAnnotation) {
        elementType = constraintAnnotation.elementType();
        constraints = constraintAnnotation.constraints();
        allViolationMessages = constraintAnnotation.allViolationMessages();
    }

    @Override
    public boolean isValid(Collection collection, ConstraintValidatorContext context) {
        boolean valid = true;

        if(collection == null) {
            //null collection cannot be validated
            return false;
        }

        Validator validator = validatorContext.getValidator();

        boolean beanConstrained = validator.getConstraintsForClass(elementType).isBeanConstrained();

        for(Object element : collection) {
            Set<ConstraintViolation<?>> violations = new HashSet<ConstraintViolation<?>> ();

            if(beanConstrained) {
                boolean hasValidCollectionConstraint = hasValidCollectionConstraint(elementType);
                if(hasValidCollectionConstraint) {
                    // elementType has @ValidCollection constraint
                    violations.addAll(validator.validate(element));
                } else {
                    violations.addAll(validator.validate(element));
                }
            } else {
                for(Class<?> constraint : constraints) {
                    String propertyName = constraint.getSimpleName();
                    propertyName = Introspector.decapitalize(propertyName);
                    violations.addAll(validator.validateValue(CollectionElementBean.class, propertyName, element));
                }
            }

            if(!violations.isEmpty()) {
                valid = false;
            }

            if(allViolationMessages) { //TODO improve
                for(ConstraintViolation<?> violation : violations) {
                    logger.debug(violation.getMessage());
                    ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(violation.getMessage());
                    violationBuilder.addConstraintViolation();
                }
            }

        }

        return valid;
    }

    private boolean hasValidCollectionConstraint(Class<?> beanType) {
        BeanDescriptor beanDescriptor = validatorContext.getValidator().getConstraintsForClass(beanType);
        boolean isBeanConstrained = beanDescriptor.isBeanConstrained();
        if(!isBeanConstrained) {
            return false;
        }
        Set<ConstraintDescriptor<?>> constraintDescriptors = beanDescriptor.getConstraintDescriptors(); 
        for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {
            if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {
                return true;
            }
        }
        Set<PropertyDescriptor> propertyDescriptors = beanDescriptor.getConstrainedProperties();
        for(PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            constraintDescriptors = propertyDescriptor.getConstraintDescriptors();
            for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {
                if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {
                    return true;
                }
            }    
        }
        return false;
    }

}

ВалидаторContextAwareConstraintValidator

public interface ValidatorContextAwareConstraintValidator {

    void setValidatorContext(ValidatorContext validatorContext);

}

CollectionElementBean

    public class CollectionElementBean {

    /* add more properties on-demand */
    private Object notNull;
    private String notBlank;
    private String email;

    protected CollectionElementBean() {
    }

    @NotNull
    public Object getNotNull() { return notNull; }
    public void setNotNull(Object notNull) { this.notNull = notNull; }

    @NotBlank
    public String getNotBlank() { return notBlank; }
    public void setNotBlank(String notBlank) { this.notBlank = notBlank; }

    @Email
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

}

ConstraintValidatorFactoryImpl

public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {

    private ValidatorContext validatorContext;

    public ConstraintValidatorFactoryImpl(ValidatorContext nativeValidator) {
        this.validatorContext = nativeValidator;
    }

    @Override
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        T instance = null;

        try {
            instance = key.newInstance();
        } catch (Exception e) { 
            // could not instantiate class
            e.printStackTrace();
        }

        if(ValidatorContextAwareConstraintValidator.class.isAssignableFrom(key)) {
            ValidatorContextAwareConstraintValidator validator = (ValidatorContextAwareConstraintValidator) instance;
            validator.setValidatorContext(validatorContext);
        }

        return instance;
    }

}

Сотрудник

public class Employee {

    private String firstName;
    private String lastName;
    private List<String> emailAddresses;

    @NotNull
    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; }

    @ValidCollection(elementType=String.class, constraints={Email.class})
    public List<String> getEmailAddresses() { return emailAddresses; }
    public void setEmailAddresses(List<String> emailAddresses) { this.emailAddresses = emailAddresses; }

}

Команда

public class Team {

    private String name;
    private Set<Employee> members;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @ValidCollection(elementType=Employee.class)
    public Set<Employee> getMembers() { return members; }
    public void setMembers(Set<Employee> members) { this.members = members; }

}

Корзина

public class ShoppingCart {

    private List<String> items;

    @ValidCollection(elementType=String.class, constraints={NotBlank.class})
    public List<String> getItems() { return items; }
    public void setItems(List<String> items) { this.items = items; }

}

Валидколлектионтест

public class ValidCollectionTest {

    private static final Logger logger = LoggerFactory.getLogger(ValidCollectionTest.class);

    private ValidatorFactory validatorFactory;

    @BeforeClass
    public void createValidatorFactory() {
        validatorFactory = Validation.buildDefaultValidatorFactory();
    }

    private Validator getValidator() {
        ValidatorContext validatorContext = validatorFactory.usingContext();
        validatorContext.constraintValidatorFactory(new ConstraintValidatorFactoryImpl(validatorContext));
        Validator validator = validatorContext.getValidator();
        return validator;
    }

    @Test
    public void beanConstrained() {
        Employee se = new Employee();
        se.setFirstName("Santiago");
        se.setLastName("Ennis");
        se.setEmailAddresses(new ArrayList<String> ());
        se.getEmailAddresses().add("segmail.com");
        Employee me = new Employee();
        me.setEmailAddresses(new ArrayList<String> ());
        me.getEmailAddresses().add("[email protected]");

        Team team = new Team();
        team.setMembers(new HashSet<Employee>());
        team.getMembers().add(se);
        team.getMembers().add(me);

        Validator validator = getValidator();

        Set<ConstraintViolation<Team>> violations = validator.validate(team);
        for(ConstraintViolation<Team> violation : violations) {
            logger.info(violation.getMessage());
        }
    }

    @Test
    public void beanNotConstrained() {
        ShoppingCart cart = new ShoppingCart();
        cart.setItems(new ArrayList<String> ());
        cart.getItems().add("JSR-303 Book");
        cart.getItems().add("");

        Validator validator = getValidator();

        Set<ConstraintViolation<ShoppingCart>> violations = validator.validate(cart, Default.class);
        for(ConstraintViolation<ShoppingCart> violation : violations) {
            logger.info(violation.getMessage());
        }
    }

}

Вывод

02:16:37,581  INFO main validation.ValidCollectionTest:66 - {ValidCollection.message}
02:16:38,303  INFO main validation.ValidCollectionTest:66 - may not be null
02:16:39,092  INFO main validation.ValidCollectionTest:66 - not a well-formed email address

02:17:46,460  INFO main validation.ValidCollectionTest:81 - may not be empty
02:17:47,064  INFO main validation.ValidCollectionTest:81 - {ValidCollection.message}

Примечание. Если компонент имеет ограничения, НЕ указывайте атрибут constraints ограничения @ValidCollection. Атрибут constraints необходим, когда бин не имеет ограничений.

person dira    schedule 01.12.2010
comment
Какой отличный ответ! Я буду работать с этим как можно скорее. Спасибо, becomputer06! - person scrotty; 02.12.2010
comment
Очень подробный и развернутый ответ! - person Vishal Biyani; 02.10.2012

У меня недостаточно высокая репутация, чтобы прокомментировать исходный ответ, но, возможно, стоит отметить по этому вопросу, что JSR-308 находится на завершающей стадии выпуска, и эта проблема будет решена, когда он будет выпущен! Однако для этого потребуется как минимум Java 8.

Единственная разница будет заключаться в том, что аннотация проверки будет находиться внутри объявления типа.

//@Email
public List<@Email String> getEmailAddresses()
{
   return this.emailAddresses;
}

Пожалуйста, дайте мне знать, где, по вашему мнению, я мог бы лучше всего разместить эту информацию для других, которые ищут. Спасибо!

P.S. Для получения дополнительной информации ознакомьтесь с этим сообщением SO.

person daniel.caspers    schedule 03.08.2015
comment
Это правильный ответ, если вы также добавите @Valid в свой метод (вместо закомментированного //@Email) - person Sébastien Nussbaumer; 17.10.2016
comment
Ссылка на документацию? - person daniel.caspers; 17.10.2016
comment
Вот документация по элементу контейнера ограничения с помощью Hibernate Validator. - person Dario Seidl; 09.11.2018
comment
@Valid больше не требуется для метода или контейнера, начиная с Hibernate Validator 6. Еще одно замечание: в настоящее время это не работает в Kotlin, потому что Kotlin не сохраняет аннотации к элементам. youtrack.jetbrains.net/issue/KT-13228. - person Dario Seidl; 09.11.2018

Невозможно написать общую аннотацию-оболочку, такую ​​​​как @EachElement, чтобы обернуть любую аннотацию ограничения — из-за ограничений самих аннотаций Java. Однако вы можете написать общий класс валидатора ограничений, который делегирует фактическую проверку каждого элемента существующему валидатору ограничений. Вы должны написать аннотацию-оболочку для каждого ограничения, но только для одного валидатора.

Я реализовал этот подход в jirutka/validator-collection (доступен в Maven Central). Например:

@EachSize(min = 5, max = 255)
List<String> values;

Эта библиотека позволяет легко создать «псевдоограничение» для любого ограничения проверки, чтобы аннотировать набор простых типов без написания дополнительного средства проверки или ненужных классов-оболочек для каждого набора. Ограничение EachX поддерживается для всех стандартных ограничений Bean Validation и ограничений Hibernate.

Чтобы создать @EachAwesome для собственного ограничения @Awesome, просто скопируйте и вставьте класс аннотации, замените аннотацию @Constraint на @Constraint(validatedBy = CommonEachValidator.class) и добавьте аннотацию @EachConstraint(validateAs = Awesome.class). Это все!

// common boilerplate
@Documented
@Retention(RUNTIME)
@Target({METHOD, FIELD, ANNOTATION_TYPE})
// this is important!
@EachConstraint(validateAs = Awesome.class)
@Constraint(validatedBy = CommonEachValidator.class)
public @interface EachAwesome {

    // copy&paste all attributes from Awesome annotation here
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String someAttribute();
}

EDIT: обновлено для текущей версии библиотеки.

person Jakub Jirutka    schedule 15.04.2013
comment
это выглядит потрясающе, приятель, жаль, что это нельзя напечатать :(. Это сделало бы все намного элегантнее - person Stef; 01.05.2013
comment
@Stef Посмотрите текущую версию. ;) - person Jakub Jirutka; 09.05.2014
comment
@JakubJirutka Я делаю то же самое для своего пользовательского ограничения, но я получаю исключение из инициализации CommonEachValidator, говорящее, что Awesome.class, который я использую, не имеет собственного валидатора? Он использует только ограничение @Pattern и ничего больше - person buræquete; 11.07.2016
comment
@JakubJirutka Да, я получаю то же сообщение для пользовательского валидатора. Например, было бы очень неплохо иметь пример вашего замечательного фреймворка, который проверяет Set‹String›. Таким образом, люди явно могут копировать ... если я что-то не упустил в вашем документе. что я не думаю, что я. Заранее спасибо. - person Beezer; 23.07.2017
comment
@JakubJirutka хорошо, теперь это работает для меня (моя ошибка), но я не получаю сообщения об ошибках для ВСЕХ моих неудачных записей в моем наборе ... так что я как бы возвращаюсь к исходной точке ... другими словами, если у меня есть Set‹String› из 1 действительного и 2 недопустимых значений, ОСТАНОВИТ ли он проверку после первого недействительного или проверит ВСЕ из них и вернет ошибки всего Set‹String›, что я и ищу. Заранее спасибо ... и пример был бы потрясающим ... потому что я почти уверен, что это распространенный вариант использования ... чтобы проверить ВСЕ содержимое независимо от того, где проверка не удалась. - person Beezer; 23.07.2017
comment
извините за труд, но проверка проходит быстро. При 1-м сбое проверки он останавливается. Есть ли способ настроить его для продолжения? - person Beezer; 23.07.2017

Спасибо за отличный ответ от becomputer06. Но я думаю, что в определение ValidCollection следует добавить следующие аннотации:

@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidCollectionValidator.class)

И я до сих пор не понимаю, что делать с коллекциями обёрток примитивных типов и аннотаций ограничений типа @Size, @Min, @Max и т. д., потому что значение не может быть передано через becomputer06.

Конечно, я могу создавать пользовательские аннотации ограничений для всех случаев в моем приложении, но в любом случае я должен добавить свойства для этих аннотаций в CollectionElementBean. И это кажется достаточно плохим решением.

person Sergey Morozov    schedule 23.09.2012

JSR-303 имеет возможность расширять целевые типы встроенных ограничений: см. 7.1.2. . Переопределение определений ограничений в XML.

Вы можете реализовать ConstraintValidator<Email, List<String>>, который делает то же самое, что и данные ответы, делегируя примитивному валидатору. Затем вы можете сохранить определение модели и применить @Email к List<String>.

person Markus Malkusch    schedule 03.01.2014
comment
Интересный подход. Однако я не могу найти ссылку на пакет валидатора, который поддерживает новые аннотации TYPE_USE. Я нашел только этот пост, в котором упоминается, что Hibernate Validator 5.2 может его поддерживать: in.relation.to/2014/10/23/ - person bernie; 12.04.2016

Возможен очень простой обходной путь. Вместо этого вы можете проверить коллекцию ваших классов, которые обертывают свойство простого значения. Чтобы это работало, вам нужно использовать аннотацию @Valid в коллекции.

Пример:

public class EmailAddress {

  @Email
  String email;

  public EmailAddress(String email){
    this.email = email;
  }
}

public class Foo {

  /* Validation that works */
  @Valid
  List<EmailAddress> getEmailAddresses(){
    return this.emails.stream().map(EmailAddress::new).collect(toList());
  }

}
person aux    schedule 20.06.2017