Hibernate Validation of Collections of Primitives
Asked Answered
B

6

41

I want to be able to do something like:

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

In other words, I want each item in the list to be validated as an email address. Of course, it is not acceptable to annotate a collection like this.

Is there a way to do this?

Brunhilda answered 29/11, 2010 at 21:20 Comment(0)
O
57

Neither JSR-303 nor Hibernate Validator has any ready-made constraint that can validate each elements of Collection.

One possible solution to address this issue is to create a custom @ValidCollection constraint and corresponding validator implementation ValidCollectionValidator.

To validate each element of collection we need an instance of Validator inside ValidCollectionValidator; and to get such instance we need custom implementation of ConstraintValidatorFactory.

See if you like following solution...

Simply,

  • copy-paste all these java classes (and import relavent classes);
  • add validation-api, hibenate-validator, slf4j-log4j12, and testng jars on classpath;
  • run the 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;
    }

}

Employee

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}

Note:- When bean has constraints do NOT specify the constraints attribute of @ValidCollection constraint. The constraints attribute is necessary when bean has no constraint.

Overthrow answered 1/12, 2010 at 20:55 Comment(2)
What a great answer! I will work with this asap. Thank you, becomputer06!Brunhilda
Very detailed and elaborate answer!Norite
K
26

I don't have a high enough reputation to comment this on the original answer, but perhaps it is worth noting on this question that JSR-308 is in its final release stage and will address this problem when it is released! It will at least require Java 8, however.

The only difference would be that the validation annotation would go inside the type declaration.

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

Please let me know where you think I could best put this information for others who are looking. Thanks!

P.S. For more info, check out this SO post.

Kurzawa answered 3/8, 2015 at 21:16 Comment(4)
This is the right answer, provided you also add @Valid on your method (instead of the commented //@Email)Territorialism
Link to documentation?Kurzawa
Here is the documentation of container element constraints with Hibernate Validator.Tinny
@Valid is not necessary anymore on the method or the container starting with Hibernate Validator 6. Another thing to note: this does not currently work in Kotlin because Kotlin doesn't retain the annotations on the elements. youtrack.jetbrains.net/issue/KT-13228.Tinny
L
15

It’s not possible to write a generic wrapper annotation like @EachElement to wrap any constraint annotation — due to limitations of Java Annotations itself. However, you can write a generic constraint validator class which delegates actual validation of every element to an existing constraint validator. You have to write a wrapper annotation for every constraint, but just one validator.

I’ve implemented this approach in jirutka/validator-collection (available in Maven Central). For example:

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

This library allows you to easily create a “pseudo constraint” for any validation constraint to annotate a collection of simple types, without writing an extra validator or unnecessary wrapper classes for every collection. EachX constraint is supported for all standard Bean Validation constraints and Hibernate specific constraints.

To create an @EachAwesome for your own @Awesome constraint, just copy&paste the annotation class, replace @Constraint annotation with @Constraint(validatedBy = CommonEachValidator.class) and add the annotation @EachConstraint(validateAs = Awesome.class). That’s all!

// 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: Updated for the current version of library.

Leonelleonelle answered 15/4, 2013 at 19:14 Comment(6)
this looks awesome mate, too bad it can't be typed :(. That would have made everything so much more elegantEsparto
@Esparto Look at the current version. ;)Leonelleonelle
@JakubJirutka I am doing the exact same thing for my custom constraint, but I get an exception from CommonEachValidator initialization saying Awesome.class that I am using, does not have a validator on its own? It is only using a @Pattern constraint and nothing elseCagliostro
@JakubJirutka Yeah I get the same message for a custom validator. It would be really nice for instance to have an example of your great framework that validates a Set<String>. That way it explicitly is out there for people to copy...unless I am missing something about your doco. which I dont think I am. Thanks in advance.Huebner
@JakubJirutka okay it works for me now (my error) but I do not get the error messages for ALL my failed entries in my Set...so I am sort of back to square one...in other words, if I have a Set<String> of 1 valid and 2 invalid values, will it STOP the validation after the frist invalid, or will it validate ALL of them and return the errors of the entire Set<String> which is what I am looking for. Thanks in advance...and example would be awesome...cause this is I am pretty sure a common use case...to get ALL the contents validated regardless where a validation fails.Huebner
sorry to labor the point, but the validation is a fail fast. At the 1st validation failure, it stops. Is there anyway to configure it to carry on?Huebner
F
4

Thanks for great answer from becomputer06. But I think the following annotations should be added to ValidCollection definition:

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

And I still don't understant what to do with collections of primitive type wrappers and constrains annotations like @Size, @Min, @Max etc., because value can't be passed through becomputer06's way.

Of course, I can create custom contraint annotations for all cases in my application, but anyway I have to add properties for these annotations to CollectionElementBean. And it seems to be a bad enough solution.

Favus answered 23/9, 2012 at 10:21 Comment(0)
S
1

JSR-303 has the ability to extend the target types of built in constraints: See 7.1.2. Overriding constraint definitions in XML.

You can implement a ConstraintValidator<Email, List<String>> which does the same thing as the given answers, delegating to the primitive validator. Then you can keep your model definition and apply @Email on List<String>.

Sherrillsherrington answered 3/1, 2014 at 3:23 Comment(1)
Interesting approach. However, I can't find any reference to a validator package that supports the new TYPE_USE annotations. I only found this post that mentions that Hibernate Validator 5.2 might support it: in.relation.to/2014/10/23/…Tonkin
D
0

A very simple workaround is possible. You can instead validate a collection of your classes that wrap the simple value property. For this to work you need to use @Valid annotation on the collection.

Example:

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

}
Distant answered 20/6, 2017 at 12:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.