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;
}
CodePudding user response:
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:
// 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 {
public Long productId;
public Instant startDate;
public Instant endDate;
public List<String> productStatus;
}
@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();
}
}
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);
}
}
(untested)
CodePudding user response:
You can annotate startDate and endDate with custom annotations something like:
@StartDateField
private DateTime startDate;
@EndDateField
private DateTime endDate;
Then in your isValid()
, you can access both startDate and endDate fields by their annotations by iterating over all class fields (in your case, all ProductRequest fields) and checking the following:
field.isAnnotationPresent(StartDateField.class)
field.isAnnotationPresent(EndDateField.class)
The complete code could be as follows:
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.*;
import java.util.Arrays;
import java.util.List;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({ ANNOTATION_TYPE.TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = ProductValidator.class)
@Documented
@interface DatesRequestConstraint {
String message() default "Invalid dates request.";
Class <?> [] groups() default {};
Class <? extends Payload> [] payload() default {};
}
@DatesRequestConstraint
class ProductRequest {
private Long productId;
@StartDateField
private DateTime startDate;
@EndDateField
private DateTime EndDate;
private List<String> productStatus;
}
@Target({ ElementType.FIELD })
@Retention(RUNTIME)
@Documented
@interface StartDateField {
}
@Target({ ElementType.FIELD })
@Retention(RUNTIME)
@Documented
@interface EndDateField {
}
public class ProductValidator implements ConstraintValidator<DatesRequestConstraint, Object> {
@Override
public void initialize(DatesRequestConstraint constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(Object requestObject, ConstraintValidatorContext constraintValidatorContext) {
DateTime startDate = getDateFieldByAnnotation(requestObject, StartDateField.class);
DateTime endDate = getDateFieldByAnnotation(requestObject, EndDateField.class);
if (startDate != null &&
endDate != null &&
startDate.isAfter(endDate)) {
return false;
} else return true;
}
private DateTime getDateFieldByAnnotation(Object requestObject, Class<? extends Annotation> annotationClass) {
return Arrays.stream(requestObject.getClass().getDeclaredFields()).filter(field -> field.isAnnotationPresent(annotationClass)).map(field -> {
try {
return field.get(requestObject);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}).map(DateTime.class::cast).findAny().orElse(null);
}
}