REST in Peace

Im vorläufig letzten Beitrag zum Thema Stammbäume geht es diesmal um die Generierung der grafischen Stammbäume mit Hilfe der REST-Schnittstelle, ein paar kleinen Verbesserungen unserer Sourcen und den Einsatz von HATEOAS in unserer Beispielanwendung.

Bevor wir uns dem Thema HATEOAS zuwenden und damit der verbesserten Verlinkung unserer Resourcen mit Spring Boot Mitteln, bereinigen wir erst einmal ein paar Sünden in unserer Anwendung. Spring liefert viele Möglichkeiten die Bestandteile unserer Anwendungen durch Dependency Injection lose zu koppeln. Solche Anwendungen haben die angenehme Eigenschaft, dass sie einfach zu erweitern, besser zu ändern und gut zu testen sind. Also ziehen wir im ersten Schritt die Aufrufe unseres GedcomReader aus dem Controller, kapseln ihn in der Klasse GedcomAnchestorService und verwenden ihn über ein Interface AnchestorService.

@RestController
public class AncestorController {
  @Autowired
  private AnchestorService service;

  @RequestMapping(path = "/ancestor/{id}", method = GET, produces = APPLICATION_JSON_VALUE)
  public PersonResource ancestor(@PathVariable("id") String id) throws IOException {
    PersonResource resource = new PersonResource(service.getById(id).orElseThrow(() -> new PersonNotFoundException(id)));
...

Über die Annotation @Autowired fügt Spring eine passenden Instanz in das Member service ein. In diesem Fall ein Objekt vom Typ GedcomAnchestorService.

@Service
public class GedcomAncestorService implements AncestorService {
  private GedCom gedcom;

  public GedcomAncestorService(@Value("${gedcom}") String source) {
    GedComReader reader = new GedComReader();
    Reader input = new InputStreamReader(getClass().getResourceAsStream(source), UTF_8);
    gedcom = reader.read(input);
...

Die Annotation @Service hilft Spring dabei Klassen für den Dependency Mechanismus zu finden. Die Annotation @Value am Konstruktorparameter source bedeutet, dass der Wert dieses Parameters aus einer Property mit dem gedcom gelesen wird, die in der Konfigurationdatei  application.properties unserer Applikation zu finden ist. Damit konnten wir uns nun auch der hässlichen Konstante mit dem Namen unserer GEDCOM Datei entledigen. Soll unser Service eine neue GEDCOM Datei verwenden, dann ändern wir ab jetzt nur noch die Konfigurationsdatei.

Da wir die ganze Zeit mit Unicode arbeiten wollen wir unsere Geschlechtsangaben etwas hübscher gestalten, in dem wir in unseren JSON Ausgaben nicht mehr MALE, FEMALE und UNKNWON verwenden, sondern passende Symbole. Dabei hilf eine einfache Jackson Annotation an unserer Enum.

public static enum Sex {
  @JsonProperty("-") UNKNOWN, //
  @JsonProperty("♂") MALE, //
  @JsonProperty("♀") FEMALE
}

Hypermedia As The Engine Of Application State (HATEOAS) liefert uns eine gute Möglichkeit innerhalb unserer Ahnenstruktur zu navigieren, ohne große Programmlogik dafür bereitzustellen. Wir wollen für jede Person, soweit vorhanden, Links auf ihre Elten, Kinder und Ehepartner liefern, damit der Benutzer mit Hilfe dieser Links einfach in der Verwandschaft navigieren kann.

Spring Boot liefert alles, was der Entwickler benötigt mit einer eleganten Integration in das Gesamtkonzept. Damit eine Resource eine Liste von Links zurückliefern kann, muss sie von der Klasse ResourceSupport erben. Dafür ändern wir die unsere PersonResource Klasse ab und entfernen alle jetzt unnötigen Attribute.

public class PersonResource extends ResourceSupport {
  private final Person person;

  public PersonResource(Person person) {
    this.person = person;
  }

  public Person getPerson() {
    return person;
  }
}

Damit mein Ahne Johann Christoph Kaiser mit einer Selbstreferenz in der Form

"_links": {
  "self": { "href": "http://localhost:8080/ancestor/gebdas/1208699983" }
}

erhält, fügen wir folgende Codezeile in den Controller ein.

resource.add(linkTo(methodOn(AncestorController.class).anchestor(organisation, identifier)).withSelfRel());

In dieser Zeile passiert sehr viel Magie mit Hilfe der Klasse ControllerLinkBuilder. Die beiden Methoden linkTo und methodOn nutzen die Annotationen an unserer ancestor Methode um den Link zu erzeugen. Aus der Pfad Angabe „/ancestor/{organisation}/{identifier}“, den übergebenen Parametern organisation und identifier und den laufenden Server auf http://localhost:8080 braut der Spring Framework den Link zusammen. Der Methodenaufruf withSelfRel sorg für den Namen „self“ dieses Links.

Damit wir nun noch mehr Links erhalten müssen wir nur ein paar ähnliche Aufrufe hinzufügen.

resource.add(linkTo(methodOn(AncestorController.class).ancestor(fatherOrg, fatherId)).withRel("father"));
resource.add(linkTo(methodOn(AncestorController.class).ancestor(motherOrg, motherId)).withRel("mother"));
resource.add(linkTo(methodOn(AncestorController.class).ancestor(childOrg, childId)).withRel("child"));

Hierbei ist dann aber jeweils vorher zu prüfen, ob die Personen tatsächlich existieren. Hat man die Logik für Eltern, Kinder und Eheparter eingebaut, dann erhält man im Falle von Johann Friedrich Kaiser die folgende Antwort vom REST-Service

{
  "person": {
    "sex": "♂",
    "name": "Johann Christoph /Kaiser/",
    "birth": { "place": "Bremen, Deutschland" },
    "urn": "urn:gina:gebdas-1208699983"
  },
  "_links": {
    "self": { "href": "http://localhost:8080/ancestor/gebdas/1208699983" },
    "wife": { "href": "http://localhost:8080/ancestor/gebdas/1208699982" },
    "child": [
      { "href": "http://localhost:8080/ancestor/schegge.de/I85" },
      { "href": "http://localhost:8080/ancestor/schegge.de/I73" }
    ],
    "father": { "href": "http://localhost:8080/ancestor/schegge.de/I98" },
    "mother": { "href": "http://localhost:8080/ancestor/schegge.de/I97" },
    "tree": { "href": "http://localhost:8080/tree/I56" },
  }
}

Über die Einträge in der _links Struktur kann man nun die engsten Verwandten von Johann Christoph adressieren. Gibt es mehrere Einträge zu einer Relation, wie etwa bei den Kindern, dann wird ein Array zurückgeliefert. Die beiden Einträge die auf „schegge.de/I85“, bzw. „schegge.de/I73“ enden, verlinken auf meinen Vater und meinen Onkel. Da beide noch nicht in GEDBAS aufgeführt sind, produziert die Anwendung eine etwas andere URI.

Die  letzte Relation „tree“ führt uns jetzt endlich zum Thema Stammbaumgenerierung. Damit die REST-Schnittstelle einen SVG Stammbaum generieren kann, ist nur eine neue Methoden im Controller nötig.

@RequestMapping(method = GET, path = "/tree/{id}", produces = "image/svg+xml")
public void tree(@PathVariable("id") String id, HttpServletResponse response) throws IOException {
  Person person = service.getById(id).orElseThrow(() -> new PersonNotFoundException(id));
  response.setContentType("image/svg+xml");
  response.setCharacterEncoding("UTF-8");
  FamilyPrinter3 printer = new FamilyPrinter3();
  PrintWriter writer = new PrintWriter(new OutputStreamWriter(response.getOutputStream(), UTF_8));
  printer.print(service, person, writer);
  writer.flush();
  response.flushBuffer();
}

Hier wird direkt in den OutputStream der HttpServletResponse geschrieben, um  den generierten SVG Inhalt zurückzuliefern. Möchte man lieber direkt auf eine Rastergrafik im PNG Format zugreifen, dann müssen wir eine weitere Methode zum Controller hinzufügen.

@RequestMapping(method = GET, path = "/tree/{id}", produces = MediaType.IMAGE_PNG_VALUE)
public void treePNG(@PathVariable("id") String id, HttpServletResponse response) throws IOException, TranscoderException {
  Person person = service.getById(id).orElseThrow(() -> new PersonNotFoundException(id));
  response.setContentType("image/png");
  FamilyPrinter3 printer = new FamilyPrinter3();
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, UTF_8));
  printer.print(service, person, writer);
  writer.flush();

  PNGTranscoder transcoder = new PNGTranscoder();
  TranscoderInput input = new TranscoderInput(new ByteArrayInputStream(out.toByteArray()));
  TranscoderOutput output = new TranscoderOutput(response.getOutputStream());
  transcoder.transcode(input, output);
  transcoder.addTranscodingHint(ImageTranscoder.KEY_WIDTH, 1000f);
  response.flushBuffer();
}

Diese Methode verwendet den PNGTranscoder aus der Batik Bibliothek um eine Bild mit der Breite von 1000 Pixel zu generieren. Unschön in dieser beispielhaften Implementierung ist das Zwischenspeichern der SVG Datei in einem ByteArrayOutputStream, weil diese Dateien natürlich recht groß werden könnten.

An diesen beiden letzten Controller Methoden kann man ein weiteres interessantes Feature der REST Schnittstellen bemerken. Der Aufruf unterscheidet sich nur durch den unterschiedlichen Mime-Type, den die beiden produzieren. Fordert ein Client einen Stammbaum mit dem Accept Header „image/png“ an, bekommt er ein PNG Bild, ansonsten bekommt er ein SVG Bild. Andere Grafik Formate könnten auch unterstützt werden, sogare eine Implementierung für text/plain wäre denkbar.

+-------------------------+
| Johann Christoph Kaiser |
|   -------------------   |
|         *1848           |
+-------------------------+

Damit wäre der vorerst letzte Beitrag zum Thema Stammbäume am Ende angelangt. Nun heißt es den Code aufräumen und den ein oder anderen Fehler in der Grafik Generierung beheben. Es ist nicht auszuschließen, dass nicht schon bald ein neuer Beitrag erscheint. Vielleicht über das Persistieren von Ahnendaten mit Hibernate und der Einsatz von NoSQL Graphdatenbanken.