Search code examples
javacollectionsbean-validationhibernate-validator

Hibernate Validation of Collections of Primitives


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?


Solution

  • 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("me@gmail.com");
    
            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.