Issue
I am creating a shared component for Request Date constraints, Begin Date is before End Date. I want to take my current Validation request, and make it common, so I type in the (Begin and EndDate class members for any Class), and it will work. How can this be done? I use annotations above the request class, in ProductRequest below .
Note: How do I set Start and End date parameters in the annotation; they may not always be "Start/End" field members, sometimes they could be "Begin/Finish" in another class .
@DatesRequestConstraint
public class ProductRequest {
private Long productId;
private DateTime startDate;
private DateTime EndDate;
private List<String> productStatus;
}
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = ProductValidator.class)
@Documented
public @interface DatesRequestConstraint {
String message() default "Invalid dates request.";
Class <?> [] groups() default {};
Class <? extends Payload> [] payload() default {};
}
public class ProductValidator implements ConstraintValidator<DatesRequestConstraint, ProductRequest> {
@Override
public void initialize(DatesRequestConstraint constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(ProductRequest productRequest, ConstraintValidatorContext constraintValidatorContext) {
if (productRequest.getStartDate() != null &&
productRequest.getEndDate() != null &&
productRequest.getStartDate().isAfter(productRequest.getEndDate())) {
return false;
}
else return true;
}
Solution
You can:
- Implement
ConstraintValidator<DatesMatch, Object>
so that you can apply the@DatesMatch
annotation on any type; - Add custom
String
fields to the@DatesMatch
annotation where you can specify the names of the fields you want to validate; - Use reflection at runtime to access the field values by their specified name.
There's a similar example of class-level validation over multiple custom fields here: Baeldung: Spring MVC Custom Validation (scroll down to "9. Custom Class Level Validation").
Customized to your example, something like this should work:
@Constraint(validatedBy = DatesMatchValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DatesMatch {
String message() default "The dates don't match.";
String startField();
String endField();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface List {
DatesMatch[] value();
}
}
// Accept a list of items so that you can validate more than one pair of dates on the same object if needed
@DatesMatch.List({
@DatesMatch(
startField = "startDate",
endField = "endDate",
message = "The end date must be after the start date."
)
})
public class ProductRequest {
private Long productId;
private Instant startDate;
private Instant endDate;
private List<String> productStatus;
/* Getters and setters omitted */
}
public class DatesMatchValidator implements ConstraintValidator<DatesMatch, Object> {
private String startField;
private String endField;
public void initialize(DatesMatch constraintAnnotation) {
this.startField = constraintAnnotation.startField();
this.endField = constraintAnnotation.endField();
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
Instant startFieldValue = (Instant) new BeanWrapperImpl(value)
.getPropertyValue(startField);
Instant endFieldValue = (Instant) new BeanWrapperImpl(value)
.getPropertyValue(endField);
if (startFieldValue == null || endFieldValue == null) {
return true;
}
return endFieldValue.isAfter(startFieldValue);
}
}
Update: (in response to comment):
this answer is great, allows multiple pair of dates, however isn't type-string safe, person can type in whatever for the fields in the product fields
Implementing ConstraintValidator<DatesMatch, Object>
is meant as an easy catch-all solution you can apply to any class.
But you can absolutely do it in a more type-safe way by implementing a separate ConstraintValidator
for each type you want to validate (i.e. ConstraintValidator<DatesMatch, ProductRequest>
, ConstraintValidator<DatesMatch, AnotherRequest>
, ...) and then specify all of them in the @Constraint(validatedBy={...})
attribute:
@Constraint(validatedBy = {ProductRequestDatesMatchValidator.class, AnotherRequestDatesMatchValidator.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DatesMatch {
String message() default "Invalid dates request.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@DatesMatch(message = "Start and end dates do not match!")
public class ProductRequest {
private Long productId;
private Instant startDate;
private Instant endDate;
private List<String> productStatus;
/* Getters and setters omitted */
}
@DatesMatch(message = "Begin and finish dates do not match!")
public class AnotherRequest {
private Long productId;
private Instant beginDate;
private Instant finishDate;
private List<String> productStatus;
/* Getters and setters omitted */
}
public class ProductRequestDatesMatchValidator implements ConstraintValidator<DatesMatch, ProductRequest> {
@Override
public boolean isValid(ProductRequest value, ConstraintValidatorContext context) {
// No need to cast here
Instant startDate = value.getStartDate();
Instant endDate = value.getEndDate();
// You could reuse this logic between each implementation by putting it in a parent class or a utility method
if (startDate == null || endDate == null) {
return true;
}
return startDate.isBefore(endDate);
}
}
public class AnotherRequestDatesMatchValidator implements ConstraintValidator<DatesMatch, AnotherRequest> {
@Override
public boolean isValid(AnotherRequest value, ConstraintValidatorContext context) {
Instant beginDate = value.getBeginDate();
Instant finishDate = value.getFinishDate();
if (beginDate == null || finishDate == null) {
return true;
}
return beginDate.isBefore(finishDate);
}
}
Do note, however, that this is still not compile-time type-safe, as you could put the @DatesMatch
annotation on a class for which you haven't written an implementation and the validation will only fail at runtime.
(You could achieve compile-time type-safety using annotation processing, but this another topic for another time.)
Answered By - Bragolgirith
Answer Checked By - Terry (JavaFixing Volunteer)