2010-11-29 15 views
34

Quiero ser capaz de hacer algo como:Hibernate Validación de las colecciones de primitivas

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

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

¿Hay alguna manera de hacerlo?

Respuesta

50

Ni JSR-303 ni Hibernate Validator tienen ninguna restricción preconfigurada que pueda validar cada elemento de la Colección.

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

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

ver si te gusta siguiente solución ...

Simplemente,

  • copiar y pegar todas estas clases de Java (y clases relavent de importación);
  • 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<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; 
    } 

} 

Empleado

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

} 

equipo

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

} 

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 constraints de la restricción @ValidCollection. El atributo constraints es necesario cuando bean no tiene ninguna restricción.

+1

Qué gran respuesta! Trabajaré con esto lo antes posible. Gracias, becomputer06! – scrotty

+0

¡Respuesta muy detallada y elaborada! . –

4

Gracias por la respuesta genial de becomputer06. pero creo que las siguientes anotaciones, debe añadirse a ValidCollection definición:

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

Y todavía no understant qué hacer con colecciones de envoltorios de tipo primitivo y limita las anotaciones como @Size, @Min, @Max etc. ., porque el valor no puede pasarse por el camino 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.

14

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 propias 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.

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

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

Esta biblioteca le permite crear fácilmente un “pseudo restricción” para ninguna restricción validación para anotar una colección de tipos simples, sin necesidad de escribir un validador adicional o clases de envoltura innecesarios para cada colección. EachX constraint 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 propio @Awesome restricción, simplemente copia & pegar la clase de anotación, reemplace @Constraint anotación con @Constraint(validatedBy = CommonEachValidator.class) y añadir 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(); 
} 

EDIT: Actualización de la versión actual de la biblioteca.

+0

esto parece compañera impresionante, es una pena que no se puede escribir :(Eso habría hecho que todo sea mucho más elegante – Stef

+0

@Stef vistazo a la versión actual;). –

+0

@JakubJirutka estoy haciendo exactamente lo mismo por mi costumbre restricción, pero obtengo una excepción de la inicialización de 'CommonEachValidator' diciendo' Awesome.class' que estoy usando, ¿no tiene un validador por sí mismo? Solo está usando una restricción '@ Pattern' y nada más –

1

JSR-303 tiene la capacidad de ampliar los tipos de destino de las restricciones incorporadas: Consulte 7.1.2. Overriding constraint definitions in XML.

se puede implementar un ConstraintValidator<Email, List<String>> que hace lo mismo que las respuestas dadas, delegando en el validador primitiva. Luego puede mantener la definición de su modelo y aplicar @Email en List<String>.

+0

Enfoque interesante. Sin embargo, no puedo encontrar ninguna referencia a un paquete de validador que admita las nuevas anotaciones 'TYPE_USE'. Solo encontré esta publicación que menciona que Hibernate Validator 5.2 * podría * admitirlo: http://in.relation.to/2014/10/23/hibernate-validator-520-alpha-1-with-java-8-support -and-a-51-maintenance-release/ – bernie

12

No tengo una reputación lo suficientemente alta para comentar esto en la respuesta original, pero tal vez valga la pena señalar que JSR-308 está 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 declaración de tipo.

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

Háganme saber si cree que puedo poner mejor esta información para otras personas que están buscando. ¡Gracias!

P.S. Para más información, check out this SO post.

+1

Esta es la respuesta correcta, siempre que también agregue '@ Válido' en su método (en lugar del comentario' // @ Email') –

+0

¿Enlace a la documentación? –

0

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, debe utilizar 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<EmailAddress> getEmailAddresses(){ 
    return this.emails.stream().map(EmailAddress::new).collect(toList()); 
    } 

} 
Cuestiones relacionadas