Validieren wie Nick Knatterton- kombinieren, kombinieren

Zusammenkommen ist ein Beginn, zusammenbleiben ist ein Fortschritt, zusammenarbeiten ist ein Erfolg.

Henry Ford

Das Validieren in Java hat viele interessante aber selten benutze Möglichkeiten. In diesem Beitrag geht es um das Kombinieren der Validierungs-Annotationen. Ein zu prüfenden Datum mit verschiedenen Validierungen zu annotieren ist sicherlich bekannt.

@Min(42) @Max(42)
private int theAnswer;

In diesem Beitrag wollen wir uns aber eine weniger bekannte Variante anschauen.

Zur Demonstration validieren wir einen Eintrag in der Klasse AncestorProperties.

@ConfigurationProperties("ancestor")
@Validated @Data
public class AncestorProperties {
  private List<URI> gallerySources;
}

Diese Klasse wird von Spring Boot beim Start automatisch mit Property-Werten befüllt. Dabei werden nur Properties beachtet, die mit „ancestor.“ beginnen. Die Annotationen @Validated und @Data dienen zur Validierung und zur Bereitstellung von Gettern und Settern.

In der Liste gallerySources werden die Adressen von fiktiven Servern für Ahnenbildern konfiguriert. Dazu reicht eine Eintrag in unserer application.properties Datei.

# property for ancestor gallery sites
ancestor.gallerySources=https://ahnen-bilder.de,https://vorfahren-im-bild.org

Spring Boot sorgt dabei für das Zerschneiden der kommaseparierten Liste und der Konvertierung der Daten in URI Instanzen. Fehlerhafte Adressen werden so schon beim Programmstart erkannt und führen zu einer Fehlermeldung.

In dieser Forn werden jedoch drei Dinge nicht erkannt, die wir aus didaktischen Gründen hier betrachten wollen.

  1. Die Liste darf nicht leer sein
  2. Die Adressen müssen alle HTTPS unterstützen
  3. Es darf keine Localhost Adresse sein

Mit den dazu nötigen Validierungs-Annotationen sieht unsere Klasse wie folgt aus.

@ConfigurationProperties("ancestor")
@Validated @Data
public class AncestorProperties {
  @NotEmpty
  private List<@NotNull @Https @NotLocalhost URI> gallerySources;
}

Die Annotation @NotEmpty meldet einen Fehler, wenn die Liste leer ist, Die Annotation @NotNull meldet einen Fehler, wenn ein NULL Eintrag in der Liste existiert. Die Annotation @Https meldet einen Fehler, wenn das Schema der URI nicht HTTPS ist. Als letztes meldet die Annotationen @NotLocalhost einen Fehler, wenn die Domäne Localhost ist.

Die Annotationen @Https und @NotLocalhost sind keine Standard Annotationen. Sie stammen aus dem Projekt URI Validator. Beide Annotationen sind auf die Typen String, java.net.URL und java.net.URI anwendbar und beide Annotationen verwenden die Möglichkeit, neue Validierungs-Annotationen aus bestehenden zu komponieren.

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Constraint(validatedBy = {})
@Schema(regexp = "http")
public @interface Http {
  String message() default "{de.schegge.validator.HTTP.message}";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

Die hier dargestellte Annotation @Https beruht auf der Validierungs-Annotationen @Schema, die in Zeile vier der Definition erscheint. Diese Annotation prüft, ob ein URI Schema auf ein angegebenes reguläres Muster passt. Die Annotation @Https validiert also nicht selber, sondern ist nur eine praktische Kurzform, für eine häufig verwendete Variante.

Dieser Ansatz kann auch gewählt werden um die Validierung in der Beispiel-Klasse etwas kompakter zu schreiben. Die folgende Annotationen ersetzt die Annotationen @NotNull, @Https und @NotLocalhost.

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Constraint(validatedBy = {})
@NotNull @Https @NotLocalhost
public @interface SecureExternal {
  String message() default "keine sichere, externe Adresse";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

Das Beispiel schaut am Ende wie folgt aus und ist durch die spezifischere Annotation auch einfacher zu verstehen.

@ConfigurationProperties("ancestor")
@Validated @Data
public class AncestorProperties {
  @NotEmpty
  private List<@SecureExternal URI> gallerySources;
}

Aber es wäre schon enttäuschend, wenn da nicht noch etwas mehr möglich wäre. Vielleicht sollen die URL noch ein wenig mehr eingeschränkt werden? In dem Fall können wir nicht mehr die Annotation @NotLocalhost verwenden, sondern müssen auf die Basis Annotation @Host ausweichen. Damit wir jetzt nicht für jeden Anwendungsfall eine neue Annotation schreiben müssen, geben wir ein regulären Ausdruck direkt an.

@ConfigurationProperties("ancestor")
@Validated @Data
public class AncestorProperties {
  @NotEmpty
  private List<@SecureExternal(".*\\.de") URI> gallerySources;
}

Nun muss aber unsere Annotation den Wert an die @Host Annotation weiterreichen, weil diese ja die Validierung durchführt. Dafür gibt es die Standard Annotation @OverridesAttributes, deren Anwendung durch das folgende Beispiel klar wird.

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Constraint(validatedBy = {})
@NotNull @Https @NotLocalhost @Host(regexp=".*")
public @interface SecureExternal {
  String message() default "keine sichere, externe Adresse";
  @OverridesAttribute(constraint = Host.class, name = "regexp") 
  String value() default ".*"
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

Mit diesen Möglichkeiten kann man domänenspezifische Validierungs-Annotationen schreiben, die zur Verständlichkeit beitragen, aber immer noch eine Flexibilität bereitstellen.

Wer Interesse an dem URI Validator Projekt gefunden hat, ist eingeladen die Annotationen auszuprobieren und Feedback in Form von Lob, Kritik und Verbesserungsvorschlägen zu geben.