“Two men look out a window. One sees mud, the other sees the stars.”
Oscar Wilde
Der Bean Validation 2.0 (JSR380) Framework nimmt dem Java Entwickler viel Arbeit ab. Die Prüfung auf valide Daten reduziert sich mittlerweile auf einige Annotationen an den zu prüfenden Konstrukten. Auch im Bereich der Datumsprüfungen gibt es reichlich Unterstützung. Aber manchmal kann es ein wenig mehr sein.
Sollen Datumsangaben auf einen validen Zeitraum geprüft werden, dann existieren im Framework folgende vier Annotationen.
@Future
@FutureOrPresent
@Past
@PastOrPresent
Die ersten beiden Annotationen prüfen ob die Datumsangabe in der Zukunft liegt, exklusive oder inklusive des aktuellen Datums und die anderen beiden entsprechend für die Vergangenheit. Eine @Present
Annotation existiert nicht, weil ein Input der das aktuelle Datum liefern muss sinnlos ist. Solch ein Input ist unnötig, weil er berechnet werden kann.
Im folgenden Beispiel ist das Attribut begin
mit der Annotation @Future
versehen. Damit ist ein Wert in der Vergangenheit für dieses Feld nicht mehr möglich. Der Wert 2018-08-24 für dieses Attribut führt zu einem Validierungsfehler.
@Future private LocalDate begin;
Leider ist die Zukunft nicht nach oben beschränkt, so dass im Allgemeinen noch eine Menge unerwünschter Datumsangaben erfolgen können. Dieses Attribut kann beispielsweise mit Datumsangaben aus dem Juni 2122 befüllt werden. Solche Datumsangaben sind nur für das Logbuch der USCSS Nostromo geeignet und nicht für den Termin der nächsten Mitgliederversammlung.
Um dieses Problem zu umgehen werden gerne fixe Obergrenzen geprüft, aber seit dem Millennium-Bug sollte diese Art der Prüfung verpönt sein. Schon so manche Legacy Software ist schlicht stehengeblieben, weil eine vergessene Obergrenze überschritten wurde.
Bei vielen Datumsangaben kann ein sinnvolles Zeitfenster angegeben werden, in dem sich das Datum befinden muss. Beispielsweise soll der Termin der nächsten Mitgliederversammlung innerhalb der nächsten drei Monate stattfinden. Es wäre also schön eine Annotation zu besitzen, bei der ein Zeitfenster vorgegeben werden kann.
@FutureWindow("P3M") private LocalDate begin;
In diesem Beispiel ist das Attribut mit einer selbstgeschriebenen Validierung versehen, die nur Datumsangaben in den nächsten drei Monaten zulässt. Der Parameter wirkt etwas kryptisch, ist aber eine valide Zeitdauer im ISO 8601 Format. Der ISO Standard kennt die beiden folgenden Varianten für Angaben in Jahren, Monaten und Tagen P[n]Y[n]M[n]D
und Angaben in Wochen P[n]W
. Leere Angaben für Jahre, Monate oder Tage können dabei entfallen.
Damit die Validierung verwendet werden kann, wird eine Annotation @FutureWindow
und eine entsprechende ConstraintValidator
Implementierung benötigt.
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = LocalDateWindowValidator.class) public @interface FutureWindow { String message() default "{de.schegge.validation.FutureWindow.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String value(); }
Die Annotation besitzt dabei keine ungewöhnlichen Abweichungen zu anderen Validierungs-Annotationen. Sie besitzt ein zusätzliches value
Attribute für die Angabe des Zeitraums und verweist auf die Implementierung LocalDateWindowValidator
für LocalDate
Instanzen.
public class LocalDateWindowValidator implements ConstraintValidator<FutureWindow, LocalDate> { private Period period; @Override public void initialize(FutureWindow constraintAnnotation) { period = Period.parse(constraintAnnotation.value()); if (period.isNegative()) { throw new IllegalArgumentException("period is negativ: " + period); } } @Override public boolean isValid(LocalDate value, ConstraintValidatorContext context) { if (value == null) { return true; } LocalDate now = LocalDate.now(); return (now.isBefore(value) || now.isEqual(value)) && ((LocalDate) period.addTo(now)).isAfter(value); } }
Die Implementierung der LocalDateWindowValidator
ist dank der Standardklassen LocalDate
und Period
sehr einfach. In der initialize
Methode wird die Period
Instanz initialisiert. Da sie die ISO 8601 Darstellung lesen kann, reicht der Aufruf ihrer parse Methode. In dieser Implementierung wirft die initialize
Methode eine IllegalArgumentException
, wenn der Zeitraum ein negatives Vorzeichen hat. Ein @FutureWindow
mit negativen Vorzeichen entspricht einem @PastWindow
und zum besseren Verständnis des Source Codes sollte dann auch ein @PastWindow
genutzt werden.
In der isValid
Methode wird der aktuelle Datumswert geprüft. Ein null
Wert ist ein valider Wert und liefert ein positives Ergebnis. Einige ältere Validatoren liefern an dieser Stelle false
zurück. Dies ist unglücklich, weil so die die Möglichkeit entfällt Inhalt und Existenz voneinander getrennt zu validieren. Mit @FutureWindow validierte Attribute können also optional sein. Benötigt man Pflichtfelder, dann prüft man zusätzlich die Existenz.
@NonNull @FutureWindow("P3M") private LocalDate begin;
Am Ende der isValid
Methode wird der prüfende Wert gegen das aktuelle Datum LocalDate.now()
geprüft. Zuerst wird geprüft ob der Wert gleich dem aktuellen Datum oder dahinter liegt. Ansonsten wird die Zeitdauer auf das aktuelle Datum addiert und geprüft, ob dieses Datum hinter dem geprüften liegt.
Damit ist die Datumsvalidierung mit Zeitfenstern auch schon abgeschlossen. In einem späteren Beitrag gibt es weitere Details zur Implementierung der Zeitfenster Annotationen. Dann werden neben @FutureWindow
auch @FuturePresentWindow
, @PastWindow
, @PastPresentWindow
bereitgestellt, sowie Implementierungen der Validierungen für die restlichen Standard Java Zeitklassen.
1 thought on “Datumsvalidierung mit Zeitfenstern”
Comments are closed.