Спящий валидация коллекций примитивов - PullRequest
36 голосов
/ 30 ноября 2010

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

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

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

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

Ответы [ 6 ]

53 голосов
/ 01 декабря 2010

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

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

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

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

Просто

  • скопируйте и вставьте все эти классы Java (и импортируйте соответствующие классы);
  • добавить валидации api, hibenate-validator, slf4j-log4j12 и testng jar на 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;
    }

}

Employee

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

}

ShoppingCart

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

18 голосов
/ 04 августа 2015

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

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

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

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

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

15 голосов
/ 15 апреля 2013

Невозможно написать общую аннотацию оболочки, такую ​​как @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();
}

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

4 голосов
/ 23 сентября 2012

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

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

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

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

1 голос
/ 03 января 2014

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

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

0 голосов
/ 20 июня 2017

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

}
...