Datumsvalidierung mit Zeitfenstern (2)

“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>

Leave a Comment