Validação de hibernação de collections de primitivos

Eu quero ser capaz de fazer algo como:

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

Em outras palavras, quero que cada item da lista seja validado como um endereço de e-mail. Claro, não é aceitável anotar uma coleção como essa.

Existe uma maneira de fazer isso?

Nem o JSR-303 nem o Hibernate Validator possuem qualquer restrição pronta que possa validar cada elemento do Collection.

Uma solução possível para resolver esse problema é criar uma restrição personalizada @ValidCollection e a implementação do validador correspondente ValidCollectionValidator .

Para validar cada elemento da coleção, precisamos de uma instância do Validator dentro do ValidCollectionValidator ; e para obter tal instância, precisamos da implementação customizada do ConstraintValidatorFactory .

Veja se você gosta da seguinte solução …

Simplesmente,

  • copie e cole todas essas classs java (e importe classs relavent);
  • adicione os jarros validation-api, hibenate-validator, slf4j-log4j12 e testng no classpath;
  • execute o caso de teste.

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, 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> violations = new HashSet> (); 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> constraintDescriptors = beanDescriptor.getConstraintDescriptors(); for(ConstraintDescriptor< ?> constraintDescriptor : constraintDescriptors) { if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) { return true; } } Set 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 getInstance(Class 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; } } 

Empregado

 public class Employee { private String firstName; private String lastName; private List 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 getEmailAddresses() { return emailAddresses; } public void setEmailAddresses(List emailAddresses) { this.emailAddresses = emailAddresses; } } 

Equipe

 public class Team { private String name; private Set members; public String getName() { return name; } public void setName(String name) { this.name = name; } @ValidCollection(elementType=Employee.class) public Set getMembers() { return members; } public void setMembers(Set members) { this.members = members; } } 

Carrinho de compras

 public class ShoppingCart { private List items; @ValidCollection(elementType=String.class, constraints={NotBlank.class}) public List getItems() { return items; } public void setItems(List 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 ()); se.getEmailAddresses().add("segmail.com"); Employee me = new Employee(); me.setEmailAddresses(new ArrayList ()); me.getEmailAddresses().add("me@gmail.com"); Team team = new Team(); team.setMembers(new HashSet()); team.getMembers().add(se); team.getMembers().add(me); Validator validator = getValidator(); Set> violations = validator.validate(team); for(ConstraintViolation violation : violations) { logger.info(violation.getMessage()); } } @Test public void beanNotConstrained() { ShoppingCart cart = new ShoppingCart(); cart.setItems(new ArrayList ()); cart.getItems().add("JSR-303 Book"); cart.getItems().add(""); Validator validator = getValidator(); Set> violations = validator.validate(cart, Default.class); for(ConstraintViolation violation : violations) { logger.info(violation.getMessage()); } } } 

Saída

 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} 

Nota: – Quando o bean possui restrições, NÃO especifique o atributo de restrições da restrição @ValidCollection . O atributo de constraints é necessário quando o bean não possui restrição.

Não é possível escrever uma anotação de wrapper genérica como @EachElement para quebrar qualquer anotação de restrição – devido às limitações das Anotações Java em si. No entanto, você pode escrever uma class de validador de restrição genérica que delega a validação real de cada elemento para um validador de restrição existente. Você precisa escrever uma anotação de wrapper para cada restrição, mas apenas um validador.

Eu implementei essa abordagem em jirutka / validator-collection (disponível no Maven Central). Por exemplo:

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

Essa biblioteca permite que você crie facilmente uma “pseudo-restrição” para qualquer restrição de validação para anotar uma coleção de tipos simples, sem escrever um validador extra ou classs de invólucro desnecessárias para cada coleção. EachX restrição EachX é suportada por todas as restrições Bean Validation padrão e restrições específicas do Hibernate.

Para criar um @EachAwesome para sua própria restrição @Awesome , apenas copie e cole a class de anotação, substitua a anotação @Constraint(validatedBy = CommonEachValidator.class) por @Constraint(validatedBy = CommonEachValidator.class) e inclua a anotação @EachConstraint(validateAs = Awesome.class) . Isso é tudo!

 // 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: atualizado para a versão atual da biblioteca.

Eu não tenho uma reputação alta o suficiente para comentar isso na resposta original, mas talvez seja interessante notar nesta questão que o JSR-308 está em seu estágio final de lançamento e irá resolver este problema quando for lançado! Pelo menos, exigirá o Java 8.

A única diferença seria que a anotação de validação iria dentro da declaração de tipo.

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

Por favor, deixe-me saber onde você acha que eu poderia colocar melhor essa informação para os outros que estão procurando. Obrigado!

PS Para mais informações, confira este post SO .

Obrigado pela ótima resposta do becomputer06. Mas acho que as seguintes annotations devem ser adicionadas à definição de ValidCollection:

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

E eu ainda não entendo o que fazer com collections de wrappers de tipos primitivos e restringe annotations como @Size, @Min, @Max etc., porque o valor não pode ser passado pelo caminho do becomputer06.

É claro que posso criar annotations de restrição personalizadas para todos os casos em meu aplicativo, mas, de qualquer forma, preciso adicionar propriedades para essas annotations ao CollectionElementBean. E parece ser uma solução ruim o suficiente.

O JSR-303 tem a capacidade de estender os tipos de destino das restrições incorporadas: Veja 7.1.2. Substituindo definições de restrição em XML .

Você pode implementar um ConstraintValidator> que faz a mesma coisa que as respostas dadas, delegando ao validador primitivo. Então você pode manter sua definição de modelo e aplicar @Email na List .

Uma solução muito simples é possível. Em vez disso, você pode validar uma coleção de suas classs que envolvem a propriedade de valor simples. Para que isso funcione, você precisa usar a anotação @Valid na coleção.

Exemplo:

 public class EmailAddress { @Email String email; public EmailAddress(String email){ this.email = email; } } public class Foo { /* Validation that works */ @Valid List getEmailAddresses(){ return this.emails.stream().map(EmailAddress::new).collect(toList()); } }