2010-11-29 4 views
34

Я хочу быть в состоянии сделать что-то вроде:Hibernate Валидация коллекций примитивов

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

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

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

ответ

50

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

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

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

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

Просто

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

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

} 

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("[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 необходим, если бит не имеет ограничений.

+1

Какой отличный ответ! Я буду работать с этим как можно скорее. Спасибо, becomputer06! – scrotty 2010-12-02 05:36:43

+0

Очень подробный и продуманный ответ! – 2012-10-01 20:20:06

4

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

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

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

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

14

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

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

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

Эта библиотека позволяет легко создать «псевдо ограничение» для любое ограничение проверки аннотировать коллекцию простых типов, без написания дополнительного валидатор или ненужные классы-оболочки для каждой коллекции. Ограничение EachX поддерживается для всех стандартных ограничений проверки Bean и ограничений 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: Обновлен для текущей версии библиотеки.

+0

Это выглядит потрясающе, слишком плохо, что его нельзя набирать :(Это сделало бы все более элегантным – Stef 2013-05-01 10:42:05

+0

@Stef Посмотрите на текущую версию.;) – 2014-05-09 13:17:21

1

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

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

12

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

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

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

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

P.S. Для получения дополнительной информации, check out this SO post.

0

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

}