“Die Zeit verweilt lange genug für denjenigen, der sie nutzen will.”
Leonardo da Vinci
Im ersten Teil dieses Beitrags wurde ein Ansatz zur Validierung von Datumsangaben vorgestellt, der auf der Angabe von expliziten Zeitfenstern beruht. Damit keine Angaben weit in der Zukunft getätigt werden können, wird der Zeitraum direkt in der Validierungs-Annotation beschränkt.
@FuturePresentWindow("P3D") private LocalDate begin; @FuturePresentWindow("P4W") private LocalDate end;
In dem obigen Beispiel ist für den Parameter begin
nur eine Datumsangaben in den nächsten drei Tagen inklusive dem aktuellen Datum zulässig und für das Attribut end
nur eine Datumsangaben in den nächsten vier Wochen.
Die erste Implementierung hatte den Nachteil, dass sie nur für den Type LocalDate
bereitstand. In diesem Beitrag wird beschrieben, welche Änderungen nötig sind um auch weitere Typen zu unterstützen. Zusätzlich werden auch Zeitfenster in die Vergangenheit implementiert.
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {}) public @interface DateWindow { String message() default "{de.schegge.validator.DateWindow.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String value(); boolean past() default false; boolean present() default false; }
Alle Möglichkeiten der Validierung werden über die Annotation DateWindow
bereitgestellt. Das Zeitfenster wird weiterhin im value
Parameter als Duration angegeben. Das Attribut past
bestimmt, ob sich das Zeitfenster in die Zukunft oder die Vergangenheit erstreckt und das Attribut present
bestimmt, ob das aktuelle Datum zum Zeitfenster gehört oder nicht. Da die Gruppe der validierbaren Typen nicht eingeschränkt werden soll, bleibt die Liste validatedBy
leer.
Da die Verwendung dieser Annotation mit ihren Attributen nicht sehr ergonomisch ist, gibt es vier weitere Annotationen, die auf dieser basieren. Dies sind @PastWindow
, @PastPresentWindow
, @FuturePresentWindow
und @FutureWindow
.
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR,PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {}) @DateWindow(value = "P1D", past = true, present = true) public @interface PastPresentWindow { @OverridesAttribute(constraint = DateWindow.class) String message() default "{de.schegge.validator.PastPresentWindow.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @OverridesAttribute(constraint = DateWindow.class) String value(); }
Wie schon im Beitrag Validieren wie Nick Knatterton- kombinieren, kombinieren verwenden auch diese Validierungs-Annotationen den @OverridesAttribute
Mechanismus um die Parameter der @DateWindow
Annotation zu überschreiben. Hier am Beispiel der @PastPresentWindow
Annotation sieht man, wie sie mit der @DateWindow
annotiert ist und für past
und present
die passenden Werte gesetzt wurden. Damit die korrekte Fehlermeldung verwendet wird und das Zeitfenster frei wählbar bleibt, überschreiben diese Attribute die gleichnamigen Attribute von @DateWindow
.
Damit die Implementierungen gefunden werden, müssen ihre Namen in der Datei META-INF/services/javax.validation.ConstraintValidator
eingefügt werden.
de.schegge.validator.window.LocalLocalDateWindowValidator de.schegge.validator.window.LocalLocalDateTimeWindowValidator de.schegge.validator.window.OffsetDateTimeWindowValidator de.schegge.validator.window.ZonedDateTimeWindowValidator
Es stehen also vier Implementierung für die Typen LocalDate
, LocalDateTime
, OffsetDateTime
und ZonedDateTime
zur Verfügung.
Sie basieren alle auf einer gemeinsamen Basisklasse, die sich um die Auswertung der Attribute der Annotation kümmert.
public abstract class AbstractDateWindowValidator<T extends Temporal> implements ConstraintValidator<DateWindow, T> { protected Period period; protected boolean past; protected boolean present; @Override public void initialize(DateWindow constraintAnnotation) { period = Period.parse(constraintAnnotation.value()); if (period.isNegative()) { throw new IllegalArgumentException("period is negativ: " + period); } present = constraintAnnotation.present(); past = constraintAnnotation.past(); } }
Die vier Implementierungen sind sich ähnlich und werden hier am Beispiel der OffsetDateTime
Implementierung erklärt.
public class OffsetDateTimeWindowValidator extends AbstractDateWindowValidator<OffsetDateTime> { @Override public boolean isValid(OffsetDateTime value, ConstraintValidatorContext context) { return value == null || compareDates(value, OffsetDateTime.now(value.getOffset()).truncatedTo(DAYS)); } protected boolean compareDates(OffsetDateTime value, OffsetDateTime now) { OffsetDateTime copy = value.truncatedTo(DAYS); if (present && now.isEqual(copy)) { return true; } if (past) { return now.isAfter(copy) && ((OffsetDateTime) period.subtractFrom(now)).isBefore(copy); } return now.isBefore(copy) && ((OffsetDateTime) period.addTo(now)).isAfter(copy); } }
In der isValid
Methode wird erst geprüft ob der Wert null ist, denn dies ist ein valider Wert. Ansonsten wird die Methode compareDates
mit dem zu prüfenden Wert und dem aktuellen Datum aufgerufen. Zur Prüfung werden die Werte auf den jeweiligen Datumsanteil reduziert und dann das entsprechende Zeitfenster in die Vergangenheit oder Zukunft geprüft.
Damit ist die Implementierung der Zeitfenster-Validierung auch schon beendet und kann direkt eingesetzt werden.
<dependency> <groupId>de.schegge</groupId> <artifactId>time-window-validator</artifactId> <version>0.1.2</version> </dependency>