Stammbäume ins Netz mit GINA

Die Stammbäume entstanden auf meinem Notebook bislang recht primitiv als Produkt eines JUnit Test.

@Test
public void printMyTree() {
  FamilyPrinter3 printer = new FamilyPrinter3();
  printer.setDebug(false);
  printer.setShowSiblings(false);
  printer.print(gedcom, ME, getWriter(ME));
}

Das obige Beispiel erstellt meinen Stammbaum, ohne Darstellung von Geschwistern und ohne Hilfslinien zur Prüfung des Layouts. In der Konstanten ME ist mein vollständiger Name im GEDCOM Format als String hinterlegt.

Es wäre aber doch viel schöner, wenn es eine Service geben würde, der über eine REST Schnittstelle, Ahnendaten und Stammbaumgrafiken liefern könnte. Solch einen Service kann man schnell und einfach mit Spring Boot 2 erstellen. Es funktioniert sogar so einfach, dass ich die Beispielapplikation in einem Bruchteil der Zeit zum Laufen gebracht habe, die ich zum Schreiben dieses Beitrages benötigte.

Zunächst einmal erstellen wir die Startklasse für unseren REST-Service und tatsächlich reichen dafür sechs Zeilen Code aus.

@SpringBootApplication
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

Dann definieren wir die Schnittstelle mit Hilfe eines Controller, der die Anfragen entgegen nimmt und die passenden Antworten generiert. Beginnen wollen wir mit einer Anfrage, die uns für eine ID einen Personen-Datensatz im Json Format zurückliefert.

@RestController 
public class AncestorController { 
  private static final String SOURCE = "Kaiser-Familienstammbaum.ged"; 

  @GetMapping("/anchestor/{id}", produces = MediaType.APPLICATION_JSON_VALUE) 
  public PersonResource anchestor(@PathVariable("id") String personId) throws IOException { 
    GedComReader reader = new GedComReader(); 
    Reader input = new InputStreamReader(getClass().getResourceAsStream(SOURCE), UTF_8); 
    GedCom gedcom = reader.read(input); 
    Person Person = gedcom.findById(personId).orElseThrow(() -> new PersonNotFoundException(personId))
    PersonResource resource = new PersonResource(person); 
    resource.setUri(SERVER.resolve(Person.getUrn()); 
    return resource; 
  } 
}

In der ersten Zeile teilt die Annotation @RestController unserer Spring Boot Applikation mit, dass es sich bei dieser Klasse um einen REST-Controller handelt. In der fünften Zeile wird die Methode anchestor, wiederum durch eine Annotation @RequestMapping, für die GET Anfragen  mit dem Pfad /anchestor/{id} bereitgestellt. Der Platzhalter {id} wir in der Parameterliste durch eine dritte Annotation @PathVariable der Variablen personId zugewiesen. Innerhalb unserer Methoden lesen wir unsere Stammbaum Datei aus, suchen die entsprechende Person und liefern als Ergebnis eine Instanz vom Type PersonResource zurück. Diese Zwischenklasse existiert, um unsere interne Person Klasse nicht mit zusätzlichen Informationen zu befrachten, die wir nur für die REST-Schnittstelle verwenden wollen. In diesem Fall erst einmal mit einer URI, die diese Person eindeutig identifiziert. Wie wir in nächsten Beitrag sehen werden, kann man dies mit Spring Boot HATEOAS noch viel eleganter lösen.

"_links": {
  "self": { "href": "http://www.schegge.de/genealogy/anchestors/P23"},
  "father": { "href": "http://www.schegge.de/genealogy/anchestors/P21"},
  "mother": { "href": "http://www.schegge.de/genealogy/anchestors/P22"},
  "husband": { "href": "http://www.schegge.de/genealogy/anchestors/P17"},
  "children": { 
    "href": "http://www.schegge.de/genealogy/anchestors/P24", 
    "href": "http://www.schegge.de/genealogy/anchestors/P25" }
}

Die _links Struktur enthält eine Reihe von Links auf, im wahrsten Sinne des Wortes, verwandte Resourcen. Der Link mit der Relation self, verweist auf die Person selbst, sodass man auch ohne die den Original Link wieder auf diese Personeninformationen gelangen kann. Die anderen Relationen stehen für die Familienmitglieder und können ggf. auch fehlen. Der Vorteil von HATEOAS ist es, auch ohne besondere Programmlogik einfach zwischen verschiedenen Informationen navigieren zu können und in diesem Fall die Großeltern oder Enkel zu finden.

REST bietet die Möglichkeit anhand des HTTP Status Code der Antwort, etwas über ihre Qualität auszusagen. Wird die gewünschte Informationen geliefert, erhält man den Status 200 OK zurück, fehlt die Information bekommt man die 404 Not Found, geht alles schief die 500 Internal Server Error und manchmal, wenn jemand humorvoll ist, auch den Status Code 418.

gedcom.findById(personId).orElseThrow(() -> new PersonNotFoundException(personId))

Die obige Zeile sucht innerhalb unserer GEDCOM Struktur nach einer Person mit der entsprechenden Personen ID. Wir keine Person gefunden und das zurück gelieferte Optional ist leer, dann wir eine PersonNotFoundException geworfen. Normalerwese führt dies zu einem HTTP Status Code 500 Internal Server Error, den wir natürlich nicht zurück liefern wollen. Auch dafür bietet Spring Boot eine elegante Lösung.

@ResponseStatus(HttpStatus.NOT_FOUND)
public class PersonNotFoundException extends IOException {
  public PersonNotFoundException(String id) {
    super(id);
  }
}

Die Annotation @ResponseStatus signalisiert der Applikation, dass hier der HTTP Status Code 404 Not Found verwendet werden soll. Eine aufrufende Anwendung erkennt dann, dass keine Informationen zur Person vorhanden sind.

Wenn eine Person gefunden wird und eine PersonResource von der Methode zurückgegeben wird, dann wandelt die Applikation die Antwort in JSON um und liefert eine Antwort mit dem Status Code 200. Damit wir keine unnötigen Informationen ausliefern ergänzen wir unsere POJOs um einige Jackson Annotationen. Jackson wird von Spring Boot standardmäßig zur JSON Konvertierung verwendet.

@JsonInclude(Include.NON_EMPTY)
public class Person {
  private Sex sex;
  private String name;

  @JsonInclude(value = Include.CUSTOM, valueFilter = EmptyEventFilter.class)
  private List<Event> birth = new ArrayList<>(1);
  
  @JsonInclude(value = Include.CUSTOM, valueFilter = EmptyEventFilter.class)
  private List<Event> death = new ArrayList<>();
  @JsonIgnore
  private Family family;
  private String description;
  private String submitterId;
  private String id;
  private List<String> spouse = new ArrayList<>();
  private List<String> child = new ArrayList<>();
  ...
}

Die Annotation @JsonInclude(Include.NON_EMPTY) weißt Jackson an, für diese Klasse nur Attribute zu beachten, die “leer” sind. Das bedeutet, dass alle null Values ignoriert werden, sowie leere String, Collection und Optional Instanzen. Ohne diese Annotationen, oder einen entsprechenden globalen Anweisung  würde uns Jackson auch diese Attribute unnötigerweise liefern.

{ "name": null, "sex": null, "id"="P23", "spouse": [ "F4", "F5" ], "child": [ ] }

Im obigen Beispiel werden name, sex und child mit ausgeliefert, obwohl sie keine Information enthalten. Mit Hilfe der Annotation wird daraus

{ "id"="P23", "spouse": [ "F4", "F5" ] }

Mit @JsonIgnore werden Attribute gänzlich ignoriert, wenn ihr Inhalt für keine Antwort benötigt wird. @JsonInclude ist eine sehr leistungsfähige Annotation, die hier verwendet wird, um die Listen birth und death auf nicht leere Ereignisse zu prüfen. Es ist der Faulheit des Entwicklers geschuldet, dass diese beiden Listen immer mindestens ein Ereignis enthalten, auch wenn keine Informationen über Geburt und Tod dieser Person vorliegen. Mehrere Ereignisse können vorliegen, wenn sich unterschiedliche Quellen nicht einig sind über Ort und Zeit des Ereignisses. Das Attribute valueFilter verweist hier auf die Klasse EmptyEventFilter, mit der jedes Event in der Liste geprüft wird. Ist das Event nach den Kriterien der Klasse EmptyEventFilter leer, d.h. sind Ort und Zeit beide null , dann wir es ausgelassen.

public class EmptyEventFilter {
  @Override
  public boolean equals(Object other) {
    Event event = (Event) other;
    return event == null || (event.getDate() == null && event.getPlace() == null);
  }
}

Nach dem Start steht uns die Applikation unter der URL http://localhost:8080 zur Verfügung und wir können unsere REST Schnittstelle testen. Ein sehr angenehmes Tool dafür ist das hier abgebildete Postman.

Im Hauptbereich des Fensters ist der Aufruf GET http://localhost:8080/anchestor/P23 und die dazugehörige Antwort zu sehen. Es handelt sich um Margarethe Dorothee Marie Trütner aus Schwaförden, die sich in meiner GEDCOM Datei hinter der ID P23 verbirgt. Für eine REST Resource ist das insoweit auch ganz in Ordnung, da sie auf unserem fiktiven System unter der URI http://www.schegge.de/genealogy/anchestors/P23 dauerhaft zu finden ist und von anderen Resourcen auch so addressiert wird.

"mother": "http://www.schegge.de/genealogy/ancestors/P23"

Schön wäre aber eine etwas allgemeinere Form z.B. über einen Uniform Resource Name (URN). Leider sind Ahnen keine Personen, die eine eindeutige Personalausweisnummer besitzen. Bei manchem ist sogar Name oder Geburtsdatum unbekannt. Daher fällt ein simples Schema wie urn:IDD:T220001293:Erika:Mustermann aus. Eine andere Variante wäre eine öffentliche Referenz der eigenen Ahneninformationen anzugeben. Viele meiner Ahnen habe ich z.B. über die Seite http://gedbas.genealogy.net  gefunden. Für weitere Recherchen ist es natürlich immer interessant, ob dort neue Informationen zu finden sind, oder vielleicht sogar weitere Ahnen hinzugefügt wurden. Daher habe ich für einige meiner Ahnen diese Verweisquelle auch innerhalb der GEDCOM Datei gespeichert. Mittlerweile sind bei GEDBAS 18.200.825 Personen und 6.763.867 Familien gespeichert. Warum also nicht GEDBAS, als Referenz, in das eigene Schema einfügen?

Hier also ein Ad-Hoc Schema für die Adressierungen von Genealogischen Personen Referenzen. Weil das in Deutsch komisch klingt und kein schönes Akronym ergibt verwende ich  Global Identification Number for Anchestors (GINA), womit nun auch der Spitzname meiner Freundin publik geworden wäre. Das Schema hat die folgende Syntax

urn:GINA:<organisation><local-identifier>

wobei Organisation eine Datenquelle wie z.B. GEDBAS ist und Local Identifier eine Adressierung innerhalb dieser Organisation darstellt. Für meine Vorfahrin Margarethe Dorothee Marie Trüttner, die man auch unter dem Link http://gedbas.genealogy.net/person/show/1139353466 finden kann, ergäbe sich also urn:GINA:GEBDAS-1139353466. Wem diese Art der Benennung bekannt vorkommt, sie entspricht der ISBN (urn:ISBN:3-8273-7019-1), bei der Gruppen– und Verlagsnummer ihre Entsprechung in der Organisation finden und die Titelnummer in dem Local Identifier.

Die Person Instanzen speichern im Attribute urn den jeweiligen Verweis auf eine Referenzquelle und diese werden dann verwendet, um einen eindeutigen Link zu generieren. Aus unserem Link

"mother": "http://www.schegge.de/genealogy/ancestors/P23"

wird also

"mother": "http://www.schegge.de/genealogy/ancestors/gebdas/1139353466"

Eine neue Methode in unserem Controller liefert auch Informationen für diese REST Anfragen

@RequestMapping("/ancestor/{organisation}/{identifier}", method = GET, produces = MediaType.APPLICATION_JSON_VALUE) 
public PersonResource anchestor(@PathVariable("organisation") String organisation, (@PathVariable("identifier") String identifier) throws IOException { 
  GedComReader reader = new GedComReader(); 
  Reader input = new InputStreamReader(getClass().getResourceAsStream(SOURCE), UTF_8); 
  GedCom gedcom = reader.read(input); 
  Person Person = gedcom.findByUrn(organisation, identifier).orElseThrow(() -> new PersonNotFoundException(organisation, identifier)) ;
  PersonResource resource = new PersonResource(person); 
  resource.setUri(SERVER.resolve(Person.getUrn()); 
  return resource; 
}

Mit einigen weiteren Methoden im Controller werden wir im nächsten Beitrag im Controller unsere REST-Schnittstelle beibringen, Stammbäume in JSON, SVG oder PNG Format zu erzeugen.