web-dev-qa-db-fra.com

Validation Hibernate des collections de primitifs

Je veux être capable de faire quelque chose comme:

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

En d'autres termes, je souhaite que chaque élément de la liste soit validé en tant qu'adresse électronique. Bien entendu, il n'est pas acceptable d'annoter une collection de ce type.

Y a-t-il un moyen de faire cela?

36
scrotty

Ni JSR-303, ni Hibernate Validator ne disposent de contraintes prédéfinies pouvant valider chaque élément de Collection.

Une solution possible pour résoudre ce problème consiste à créer une contrainte @ValidCollection personnalisée et l’implémentation du validateur correspondante ValidCollectionValidator.

Pour valider chaque élément de la collection, nous avons besoin d'une instance de Validator à l'intérieur de ValidCollectionValidator; et pour obtenir un tel exemple, nous avons besoin d'une implémentation personnalisée de ConstraintValidatorFactory.

Voyez si vous aimez la solution suivante ...

Simplement,

  • copier-coller toutes ces classes Java (et importer des classes relavent);
  • ajouter les binaires validation-api, hibenate-validator, slf4j-log4j12 et testng sur classpath;
  • lancez le 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<? 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;
    }

}

Employé

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

}

Équipe  

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

}

Chariot

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

}

Sortie

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}

Remarque: - Lorsque le bean a des contraintes, NE spécifiez PAS l'attribut constraints de la contrainte @ValidCollection. L'attribut constraints est nécessaire lorsque le bean n'a pas de contrainte.

53
Dipesh Rathod

Je n'ai pas assez de réputation pour commenter la réponse d'origine, mais il convient peut-être de noter sur cette question que JSR-308 est dans sa phase finale de publication et qu'il traitera ce problème dès sa publication! Il faudra au moins utiliser Java 8, cependant. 

La seule différence serait que l'annotation de validation irait à l'intérieur de la déclaration de type. 

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

S'il vous plaît laissez-moi savoir où vous pensez que je pourrais le mieux mettre cette information pour ceux qui cherchent. Merci!

P.S. Pour plus d'informations, consultez cet article SO .

16
daniel.caspers

Il n’est pas possible d’écrire une annotation générique de wrapper telle que @EachElement pour envelopper une annotation de contrainte, en raison des limitations des annotations Java elles-mêmes. Cependant, vous pouvez écrire une classe de validateur de contrainte générique qui délègue la validation réelle de chaque élément à un validateur de contrainte existant. Vous devez écrire une annotation wrapper pour chaque contrainte, mais pour un seul validateur.

J'ai implémenté cette approche dans jirutka/validator-collection (disponible dans Maven Central). Par exemple:

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

Cette bibliothèque vous permet de créer facilement une "pseudo contrainte" pour la contrainte de validation any afin d'annoter une collection de types simples, sans écrire de validateur supplémentaire ni de classes d'emballage inutiles pour chaque collection. La contrainte EachX est prise en charge pour toutes les contraintes de validation de bean standard et les contraintes spécifiques à Hibernate.

Pour créer un @EachAwesome pour votre propre contrainte @Awesome, copiez et collez simplement la classe d'annotation, remplacez l'annotation @Constraint par @Constraint(validatedBy = CommonEachValidator.class) et ajoutez l'annotation @EachConstraint(validateAs = Awesome.class). C'est tout!

// 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: mis à jour pour la version actuelle de la bibliothèque.

15
Jakub Jirutka

Merci pour la réponse de becomputer06. Mais je pense que les annotations suivantes devraient être ajoutées à la définition de ValidCollection:

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

Et je ne comprends toujours pas ce qu'il faut faire des collections de wrappers de type primitif et contraint les annotations telles que @Size, @Min, @Max etc.

Bien sûr, je peux créer des annotations de contraint personnalisé pour tous les cas de mon application, mais je dois malgré tout ajouter des propriétés pour ces annotations à CollectionElementBean. Et cela semble être une solution assez mauvaise.

4
Sergey Morozov

La JSR-303 a la capacité d’étendre les types cibles de contraintes intégrées: Voir 7.1.2. Remplacement des définitions de contrainte en XML .

Vous pouvez implémenter un ConstraintValidator<Email, List<String>> qui fait la même chose que les réponses données, en déléguant au validateur de primitives. Ensuite, vous pouvez conserver votre définition de modèle et appliquer @Email sur List<String>.

1
Markus Malkusch

Une solution de contournement très simple est possible. Vous pouvez plutôt valider une collection de vos classes qui encapsule la propriété valeur simple. Pour que cela fonctionne, vous devez utiliser l'annotation @Valid sur la collection.

Exemple:

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

}
0
aux