Validación de hibernación de colecciones de primitivas

Quiero ser capaz de hacer algo como:

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

En otras palabras, quiero que cada elemento de la lista se valide como una dirección de correo electrónico. Por supuesto, no es aceptable anotar una colección como esta.

¿Hay alguna forma de hacer esto?

Ni JSR-303 ni Hibernate Validator tienen ninguna restricción preestablecida que pueda validar cada elemento de Collection.

Una posible solución para solucionar este problema es crear una restricción @ValidCollection personalizada y la implementación del validador correspondiente ValidCollectionValidator .

Para validar cada elemento de la colección, necesitamos una instancia de Validator dentro de ValidCollectionValidator ; y para obtener dicha instancia, necesitamos una implementación personalizada de ConstraintValidatorFactory .

Vea si le gusta seguir la solución …

Simplemente,

  • copiar y pegar todas estas clases de Java (e importar clases relavent);
  • agregue validation-api, hibenate-validator, slf4j-log4j12 y testng jar en classpath;
  • ejecutar el caso de prueba.

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

Empleado

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

Equipo

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

Carrito 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()); } } } 

Salida

 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: – Cuando bean tiene restricciones, NO especifique el atributo de constraints la constraints @ValidCollection . El atributo de constraints es necesario cuando bean no tiene restricción.

No es posible escribir una anotación de envoltura genérica como @EachElement para ajustar cualquier anotación de restricción, debido a las limitaciones de las @EachElement anotaciones de Java. Sin embargo, puede escribir una clase de validador de restricción genérica que delegue la validación real de cada elemento a un validador de restricción existente. Debe escribir una anotación de contenedor para cada restricción, pero solo un validador.

Implementé este enfoque en jirutka / validator-collection (disponible en Maven Central). Por ejemplo:

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

Esta biblioteca le permite crear fácilmente una “pseudo-restricción” para cualquier restricción de validación para anotar una colección de tipos simples, sin escribir un validador adicional o clases de contenedor innecesarias para cada colección. EachX restricción EachX es compatible con todas las restricciones de validación de Bean estándar y las restricciones específicas de Hibernate.

Para crear un @EachAwesome para su propia restricción @Awesome , solo copie y pegue la clase de anotación, reemplace la anotación @Constraint(validatedBy = CommonEachValidator.class) con @Constraint(validatedBy = CommonEachValidator.class) y agregue la anotación @EachConstraint(validateAs = Awesome.class) . ¡Eso es todo!

 // 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(); } 

EDITAR: actualizado para la versión actual de la biblioteca.

No tengo una reputación lo suficientemente alta como para comentar esto en la respuesta original, pero tal vez valga la pena señalar que JSR-308 se encuentra en su etapa final de lanzamiento y abordará este problema cuando se lance. Sin embargo, al menos requerirá Java 8.

La única diferencia sería que la anotación de validación iría dentro de la statement de tipo.

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

Por favor, hágame saber dónde cree que mejor podría poner esta información para otros que están buscando. ¡Gracias!

PD Para obtener más información, consulte esta publicación SO .

Gracias por la excelente respuesta de becomputer06. Pero creo que las siguientes anotaciones deben agregarse a la definición de ValidCollection:

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

Y todavía no entiendo qué hacer con las colecciones de envoltorios de tipo primitivo y las anotaciones de restricción como @Size, @Min, @Max, etc., porque el valor no se puede pasar a través de la manera de becomeputer06.

Por supuesto, puedo crear anotaciones de contraint personalizadas para todos los casos en mi aplicación, pero de todos modos tengo que agregar propiedades para estas anotaciones a CollectionElementBean. Y parece ser una solución lo suficientemente mala.

JSR-303 tiene la capacidad de extender los tipos de destino de las restricciones integradas: Ver 7.1.2. Reemplazando las definiciones de restricciones en XML .

Puede implementar un ConstraintValidator> que haga lo mismo que las respuestas dadas, delegando en el validador primitivo. Luego puede mantener la definición de su modelo y aplicar @Email en List .

Una solución muy simple es posible. En su lugar, puede validar una colección de sus clases que envuelva la propiedad del valor simple. Para que esto funcione, debes usar la anotación @Valid en la colección.

Ejemplo:

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