Datumsvalidierung mit Zeitfenstern

 „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.