2010-11-29 10 views
34

Ich möchte in der Lage sein, so etwas wie zu tun:Hibernate Validierung von Sammlungen von Primitives

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

Mit anderen Worten, ich mag jedes Element in der Liste als E-Mail-Adresse validiert werden. Natürlich ist es nicht akzeptabel, eine Sammlung wie diese zu kommentieren.

Gibt es eine Möglichkeit, dies zu tun?

Antwort

50

Weder der JSR-303 noch der Hibernate-Validator verfügen über eine vorgefertigte Einschränkung, mit der die einzelnen Elemente der Collection validiert werden können.

Eine mögliche Lösung zur Behebung dieses Problems besteht darin, eine benutzerdefinierte @ValidCollection-Einschränkung und die entsprechende Validiererimplementierung ValidCollectionValidator zu erstellen.

Um jedes Element der Sammlung zu validieren, benötigen wir eine Instanz Validator innerhalb ValidCollectionValidator; Um eine solche Instanz zu erhalten, benötigen wir eine benutzerdefinierte Implementierung von ConstraintValidatorFactory.

Sehen Sie, wenn Sie wie die folgende Lösung ...

einfach,

  • copy-paste alle Java-Klassen (und Import relavent Klassen);
  • Fügen Sie validation-api, hibenate-validator, slf4j-log4j12 und testng jars auf dem Klassenpfad hinzu.
  • führen Sie den Testfall.

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

} 

Mitarbeiter

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

} 

-Team

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

} 

Output

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} 

Hinweis: - Wenn Bean Einschränkungen aufweist, geben Sie das constraints-Attribut der @ValidCollection-Einschränkung NICHT an. Das Attribut constraints ist erforderlich, wenn Bean keine Einschränkung hat.

+1

Was für eine tolle Antwort! Ich werde so schnell wie möglich arbeiten. Danke, bearbeiter06! – scrotty

+0

Sehr detaillierte und durchdachte Antwort! –

4

Danke für die tolle Antwort von becomputer06. Aber ich denke, die folgenden Anmerkungen zu ValidCollection Definition hinzugefügt werden soll:

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

Und ich understant noch nicht, was mit einer Sammlung primitiver Art Wrapper zu tun und zwingt Anmerkungen wie @size, @Min, @Max etc ., weil der Wert nicht über die Methode von becomputer06 weitergegeben werden kann.

Natürlich kann ich benutzerdefinierte Constraint-Annotationen für alle Fälle in meiner Anwendung erstellen, aber trotzdem muss ich Eigenschaften für diese Annotationen zu CollectionElementBean hinzufügen. Und es scheint eine schlechte Lösung zu sein.

14

Es ist nicht möglich, eine generische Wrapper-Annotation wie @EachElement zu schreiben, um Constraint-Annotations zu umbrechen - aufgrund der Einschränkungen von Java Annotations selbst. Sie können jedoch eine generische Constraint-Validator-Klasse schreiben, die die tatsächliche Validierung jedes Elements an einen vorhandenen Constraint-Validator delegiert. Sie müssen für jede Einschränkung eine Wrapper-Annotation schreiben, aber nur einen Validator.

Ich habe diesen Ansatz in jirutka/validator-collection implementiert (verfügbar in Maven Central). Zum Beispiel:

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

Diese Bibliothek ermöglicht es Ihnen leicht eine „Pseudo-Einschränkung“ zu schaffen für jede Validierung Einschränkung eine Sammlung von einfachen Typen mit Anmerkungen zu versehen, ohne für jede Sammlung einen zusätzlichen Validator oder unnötige Wrapper-Klassen zu schreiben. EachX constraint wird für alle standardmäßigen Bean Validation-Constraints und Hibernate-spezifischen Constraints unterstützt.

Um eine @EachAwesome für die eigene @Awesome Einschränkung zu erstellen, kopieren & nur die Annotation-Klasse einfügen, ersetzen @Constraint Annotation mit @Constraint(validatedBy = CommonEachValidator.class) und fügen Sie die Anmerkung @EachConstraint(validateAs = Awesome.class). Das ist alles!

EDIT: Aktualisiert für die aktuelle Version der Bibliothek.

+0

das sieht klasse aus, schade, es kann nicht getippt werden :(. Das hätte alles so viel eleganter gemacht – Stef

+0

@Stef Schau dir die aktuelle Version an.;) –

+0

@JakubJirutka Ich mache genau dasselbe für meine Sitte Constraint, aber ich bekomme eine Ausnahme von 'CommonEachValidator' Initialisierung, die' Awesome.class' sagt, die ich benutze, hat keinen Validator allein? Es verwendet nur eine '@ Pattern'-Bedingung und sonst nichts –

1

JSR-303 kann die Zieltypen der integrierten Integritätsbedingungen erweitern: Siehe 7.1.2. Overriding constraint definitions in XML.

Sie können eine ConstraintValidator<Email, List<String>> implementieren, die dasselbe wie die angegebenen Antworten ausführt und an den primitiven Validator delegiert. Dann können Sie Ihre Modelldefinition beibehalten und @Email auf List<String> anwenden.

+0

Interessanter Ansatz. Ich kann jedoch keinen Verweis auf ein Prüfpaketierungspaket finden, das die neuen "TYPE_USE" -Anmerkungen unterstützt. Ich fand nur diesen Beitrag, der erwähnt, dass Hibernate Validator 5.2 * es * unterstützen könnte: http://in.relation.to/2014/10/23/hibernate-validator-520-alpha-1-with-java-8-support -und-a-51-maintenance-release/ – bernie

12

Ich habe nicht genug Reputation, um dies auf die ursprüngliche Antwort zu kommentieren, aber vielleicht ist es erwähnenswert, dass JSR-308 ist in seiner endgültigen Release-Stufe und wird dieses Problem beheben, wenn es veröffentlicht wird! Es wird jedoch mindestens Java 8 benötigen.

Der einzige Unterschied wäre, dass die Validierungsannotation in die Typdeklaration eingeht.

Bitte lassen Sie mich wissen, wo Sie denken, ich könnte diese Informationen am besten für andere, die suchen. Vielen Dank!

P.S. Für weitere Informationen, check out this SO post.

+1

Dies ist die richtige Antwort, vorausgesetzt, dass Sie auch '@ Valid' zu Ihrer Methode hinzufügen (anstelle der kommentierten' // @ Email') –

+0

Link zur Dokumentation? –

0

Eine sehr einfache Problemumgehung ist möglich. Sie können stattdessen eine Sammlung Ihrer Klassen validieren, die die einfache Werteigenschaft umbrechen. Damit dies funktioniert, müssen Sie die Anmerkung @Valid für die Sammlung verwenden.

Beispiel:

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

}