Hibernate Validation of Collections of Primitives

Hibernate Validation of Collections of Primitives

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.

How to use Java Bean Validators (JSR-303/JSR-349) on elements of an array/list/collection

There is no easy generic solution as of Bean Validation 1.0/1.1. You could implement a custom constraint like @NoNullElements:

@NoNullElements
private List<String> myStrings;

The constraint's validator would iterate over the list and check that no element is null. Another approach is to wrap your String into a more domain-specific type:

public class EmailAddress {

@NotNull
@Email
private String value;

//...
}

And apply cascaded validation to the list via @Valid:

@Valid
private List<EmailAddress> addresses;

Having such a domain-specific data type is often helpful anyways to convey a data element's meaning as it is passed through an application.

In the future a generic solution for the issue may be to use annotations on type parameters as supported by Java 8 but that's only an idea at this point:

private List<@NotNull String> myStrings;


Related Topics



Leave a reply



Submit