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

Я хочу быть в состоянии сделать что-то вроде:

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

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

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

6 ответов

Решение

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

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

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

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

Просто,

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

ValidCollection

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

}

ValidatorContextAwareConstraintValidator

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

}

ValidCollectionTest

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("me@gmail.com");

        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 Атрибут необходим, когда бин не имеет ограничений.

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

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

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

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

PS Для получения дополнительной информации, проверьте этот SO пост.

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

Я реализовал этот подход в https://github.com/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();
}

РЕДАКТИРОВАТЬ: Обновлено для текущей версии библиотеки.

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

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

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

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

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

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

Возможен очень простой обходной путь. Вместо этого вы можете проверить коллекцию ваших классов, которые обертывают свойство простого значения. Чтобы это работало, нужно использовать @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());
  }

}
Другие вопросы по тегам