I'm currently working on a custom ConstraintValidator
to check an array of objects which have a timespan associated with them for overlaps in their timespan. The validation logic is working, however, I am uncertain how to add a "This object's timeslot overlaps with another object's timeslot" message to every object in violation of the validation logic.
I've tried several approaches described here: https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidatorContext.html
Specifically those described in the buildConstraintViolationWithTemplate
method docs.
Here is the relevant section of the code:
@Override
public boolean isValid(List<Shift> shifts, ConstraintValidatorContext context) {
List<Integer> overlappingShiftIndices = determineOverlappingShifts(shifts);
if (!overlappingShiftIndices.isEmpty()) {
log.debug("Overlap validation failed.");
context.disableDefaultConstraintViolation();
// Moving the error from form-level to fields
for (int index : overlappingShiftIndices) {
context.buildConstraintViolationWithTemplate("{com.generali.standbyscheduler.validation.shiftlist.overlap}")
.addBeanNode()
.inIterable().atIndex(index)
.addConstraintViolation();
}
return false;
}
log.debug("Overlap validation succeeded.");
return true;
}
As you can see I tried the .addBeanNode().inIterable().atIndex(index)
approach here. When looking at the ConstraintViolation
s the property path displays as list[index]
. Is this correct?
I plan on using this to access the determined violations from a BindingResult
in a Thymeleaf template and am uncertain whether the violations will be accessible this way. The list will be a property of another bean, so I'm expecting to read the violations using a path like propertyNameOfList[index]
. Or would it be propertyNameOfList.list[index]
or something else?
I got the same problem, when trying to validate if certain fields are unique within a object list. My own solution (I found none on the internet :/ ):
You have to overwrite the current PropertyNode and add the index number by using .addPropertyNode(null).inIterable().atIndex(index)
. Example:
ConstraintAnnotation:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueBusinessIndexValidator.class)
public @interface UniqueEntries {
String message() default ValidationMessages.REQUIRED_UNIQUE_INDEX;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ConstraintValidator:
public class UniqueBusinessIndexValidator implements ConstraintValidator<UniqueEntries, List<HasBusinessIndex>> {
@Override
public boolean isValid(List<HasBusinessIndex> collection, ConstraintValidatorContext context) {
if (collection == null || collection.isEmpty()) {
return true;
}
Map<String, List<Integer>> indexesMap = new HashMap<>();
for (int runner = 0; runner < collection.size(); runner++) {
String businessIndex = collection.get(runner).getBusinessIndex();
if (indexesMap.containsKey(businessIndex)) {
indexesMap.get(businessIndex).add(runner);
} else {
indexesMap.put(businessIndex, new ArrayList<>(List.of(runner)));
}
}
boolean isValid = indexesMap.values().stream().noneMatch(indexes -> indexes.size() > 1);
if (!isValid) {
indexesMap.values()
.stream()
.filter(index -> index.size() > 1)
.forEach(index -> addUniqueBusinessIndexkennungViolation(context, index));
}
return isValid;
}
private void addUniqueBusinessIndexkennungViolation(ConstraintValidatorContext context, List<Integer> indexes) {
for (Integer index : indexes) {
context.buildConstraintViolationWithTemplate(ValidationMessages.REQUIRED_UNIQUE_INDEX)
.addPropertyNode(null)
.inIterable()
.atIndex(index)
.addPropertyNode("businessIndex")
.addConstraintViolation()
.disableDefaultConstraintViolation();
}
}