Convalida ibernata delle collezioni di primitivi

Voglio essere in grado di fare qualcosa come:

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

In altre parole, voglio che ogni elemento nell’elenco sia convalidato come indirizzo email. Ovviamente, non è accettabile annotare una raccolta come questa.

C’è un modo per fare questo?

Né JSR-303 né Hibernate Validator hanno alcun vincolo già pronto che possa convalidare ogni elemento della Collezione.

Una ansible soluzione per risolvere questo problema è creare un vincolo @ValidCollection personalizzato e l’implementazione di validatore corrispondente ValidCollectionValidator .

Per convalidare ogni elemento della collezione abbiamo bisogno di un’istanza di Validator all’interno di ValidCollectionValidator ; e per ottenere tale istanza abbiamo bisogno di implementazione personalizzata di ConstraintValidatorFactory .

Vedi se ti piace seguire la soluzione …

Semplicemente,

  • copia-incolla tutte queste classi java (e importa le classi relavent);
  • aggiungere validation-api, hibenate-validator, slf4j-log4j12 e testng jar su classpath;
  • eseguire il test-case.

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

Dipendente

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

Squadra

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

Carrello della spesa

 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("[email protected]"); 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()); } } } 

Produzione

 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-formsd 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 i bean hanno dei vincoli NON specificare l’attributo constraints del vincolo @ValidCollection . L’attributo constraints è necessario quando bean non ha vincoli.

Non è ansible scrivere un’annotazione generica del wrapper come @EachElement per racchiudere qualsiasi annotazione sui vincoli, a causa delle limitazioni della stessa Annotazione Java. Tuttavia, è ansible scrivere una class di validatore di vincoli generici che deleghi la convalida effettiva di ogni elemento a un validatore di vincoli esistente. Devi scrivere un’annotazione wrapper per ogni vincolo, ma solo un validatore.

Ho implementato questo approccio in jirutka / validator-collection (disponibile in Maven Central). Per esempio:

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

Questa libreria consente di creare facilmente uno “pseudo vincolo” per qualsiasi vincolo di validazione per annotare una raccolta di tipi semplici, senza scrivere un validatore aggiuntivo o classi wrapper non necessarie per ogni raccolta. EachX vincolo EachX è supportato per tutti i vincoli di convalida Bean standard e i vincoli Hibernate specifici.

Per creare un @EachAwesome per il tuo vincolo @Awesome , copia e incolla la class di annotazione, sostituisci @Constraint annotation con @Constraint(validatedBy = CommonEachValidator.class) e aggiungi l’annotazione @EachConstraint(validateAs = Awesome.class) . È tutto!

 // 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[] payload() default {}; String someAttribute(); } 

EDIT: aggiornato per la versione corrente della libreria.

Non ho una reputazione abbastanza alta per commentare questo sulla risposta originale, ma forse vale la pena notare su questa domanda che JSR-308 è nella sua fase di rilascio finale e risolverà questo problema quando verrà rilasciato! Richiederà almeno Java 8, comunque.

L’unica differenza sarebbe che l’annotazione di validazione andasse all’interno della dichiarazione del tipo.

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

Per favore fatemi sapere dove pensate che potrei mettere meglio queste informazioni per gli altri che stanno cercando. Grazie!

PS Per maggiori informazioni, controlla questo post SO .

Grazie per l’ottima risposta di becomputer06. Ma penso che le seguenti annotazioni dovrebbero essere aggiunte alla definizione di ValidCollection:

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

E continuo a non capire cosa fare con insiemi di wrapper di tipo primitivo e limitare annotazioni come @Size, @Min, @Max ecc., Perché il valore non può essere passato attraverso il modo di becomputer06.

Ovviamente, posso creare annotazioni di contraffazione personalizzate per tutti i casi nella mia applicazione, ma in ogni caso devo aggiungere le proprietà per queste annotazioni a CollectionElementBean. E sembra essere una soluzione abbastanza brutta.

JSR-303 ha la capacità di estendere i tipi di target di vincoli integrati: Vedi 7.1.2. Sovrascrivere le definizioni dei vincoli in XML .

È ansible implementare un ConstraintValidator> che fa la stessa cosa delle risposte date, delegando al validatore primitivo. Quindi puoi mantenere la definizione del modello e applicare @Email su List .

Una soluzione molto semplice è ansible. Puoi invece convalidare una raccolta delle tue classi che racchiuda la proprietà del valore semplice. Affinché questo funzioni è necessario utilizzare @Valid annotazione @Valid nella raccolta.

Esempio:

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