“When I say Hypertext, I mean the simultaneous presentation of information and controls such that the information becomes the affordance through which the user obtains choices and selects actions”
Roy Fielding
Bei der Verwendung von REST Schnittstellen beginnt die Evolution eigener APIs mit der Erstellung einiger HTTP Endpunkte die Informationen im JSON Format austauschen. Die nächste Stufe ist die Abkehr von der Denkweise in Aktionen hin zum Verständnis, dass über REST Resourcen addressiert und manipuliert werden.
Ein großer Sprung ist danach die Verwendung von HATEOAS um die semantische Verknüpfung zwischen verschiedenen Resourcen auszudrücken.
{ "name": "Margarethe Dorothee Marie /Trüttner/", "gender": "female", "_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" }, } }
Im Fall der hier vorliegenden Ahnendaten sind die Verknüpfungen den Verwandtschaftsgraden nachempfunden. Hier also Vater, Mutter, Ehemann, Ehefrau und Kind. Im Kontext eines KITA Trägers könnten dies Links zu einzelnen Einrichtungen sein. Bei einem Kassensystem könnten dies Links zu den Bezahlmethoden sein, die einem speziellen Kunden angeboten werden sollen.
Mit HATEOAS geht es aber noch einen Schritt weiter, denn die anfängliche Frage steht noch immer im Raum. Was kann ich mit der Ahnen Resource http://www.schegge.de/genealogy/anchestors/P23
anstellen? Der Self-Link gibt nur die Information, dass ich die Resource unter dieser Adresse finde und mit GET
lesen kann. Was aber passiert, wenn ich den Link mit PUT
oder DELETE
aufrufe?
Spring HATEOAS unterstützt die Entwickler, die Möglichkeiten der API an der Resource besser zu kommunizieren. Dazu wird neben dem JSON Attribute _links
noch das JSON Attribute _templates
in die Response eingefügt.
Damit das auch funktioniert, muss der bisherige RestController
angepasst werden.
@RestController @RequestMapping(path = "/api/v1", produces = HAL_JSON_VALUE) public class AncestorController { @GetMapping("/partners/{id}") public EntityModel<AncestorDto> findById(@PathVariable @Min(1) Long id) { return service.findById(id).map(this::createEntityModel).orElseThrow(); } private EntityModel<AncestorDto> createEntityModel(AncestorDto ancestorDto) { return new EntityModel<>(ancestorDto, linkTo(methodOn(AncestorController.class).findById(ancestorDto.getId())).withSelfRel(), linkTo(methodOn(AncestorController.class).findAll(null, null, null)).withRel("ancestors").expand() ); } }
Bislang produzierte der RestController
den Content-Type “application/hal+json
” der auf “application/prs.hal-forms+json
” geändert werden muss. .Dafür wird die @RequestMapping
Annotation am AncestorController
angepasst.
@RestController @RequestMapping(path = "/api/v1", produces = HAL_FORMS_JSON_VALUE) public class AncestorController { }
Die bisherigen HATEOAS Links werden in der Methode createEntityModel mit der linkTo Methode erzeugt.
Damit der RestController
die weiteren Möglichkeiten der Self-Links kommunizieren kann, werden zwei Affordance
Instanzen dem Link hinzugefügt. Dies geschieht, wie bei Spring HATEOAS üblich, mit schwarzer Magie in Form von Reflections. Mit den Ausdrücken methodOn(AncestorController.class).update
und afford(methodOn(AncestorController.class).remove
werden zwei weitere, hier nicht dargestellte Endpoint Methoden ausgewertet.
private EntityModel<AncestorDto> createEntityModel(AncestorDto ancestorDto) { return new EntityModel<>(ancestorDto, linkTo(methodOn(AncestorController.class).findById(ancestorDto.getId())).withSelfRel() .andAffordance(afford(methodOn(AncestorController.class).update(ancestorDto.getId(), ancestorDto, null))) .andAffordance(afford(methodOn(AncestorController.class).remove(ancestorDto.getId()))), linkTo(methodOn(AncestorController.class).findAll(null, null, null)).withRel("ancestors").expand() ); }
Die bisherige Darstellung der REST Resource für Margarethe Dorothee Marie Trüttner erweitert sich nun wie folgt.
{ "name": "Margarethe Dorothee Marie /Trüttner/", "gender": "female", "_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" }, } "_templates": { "default": { "method": "put", "properties": [ { "name": "gender" }, { "name": "name" } ] }, "remove": { "method": "delete" } } }
Innerhalb des Attributes _templates
sind zwei Einträge zu finden. Der erste Eintrag besagt, dass mit der HTTP Methode PUT
die Resource über die Attribute name
und gender
geändert werden kann. Der zweite Eintrag besagt, dass diese Resource mit der HTTP Methode DELETE
gelöscht werden kann.
Was unterscheidet nun diesen HATEOAS Affordance Ansatz von Beschreibungsformaten wie Swagger oder OpenAPI? Die Beschreibungsformate definieren ein statisches Modell unabhängig von dem Status der tatsächlichen Resourcen. Der HATEOAS Affordance Ansatz besitzt die Möglichkeit den Status der Resource einzubeziehen.
Der Eintrag von Margarethe Dorothee Marie Trüttner sollte nicht gelöscht werden können, weil ihr Fehlen die Stammbäume ihrer Kinder vom ursprünglichen Stammbaum abreißen würde. Ein DELETE
sollte also nur für Einträge erlaubt sein, die nur als Kind angefügt wurden und dies kann in der createEntityModel
Methode berücksichtigt werden. Wird diese Methode entsprechend angepasst, dann wird weder für Margarethe Dorothee Marie Trüttner, ihrem Ehemann oder ihren Eltern die DELETE Methode angeboten.
Selbstverständlich kann der Nutzer trotzdem versuchen den Eintrag von Margarethe Dorothee Marie Trüttner über ein DELETE zu löschen. Dann erhält er eine Antwort mit dem Fehlercode 409 Conflict
, da vor dem Löschen natürlich geprüft wird, ob dies möglich und gestattet ist.
Der HATEOAS Affordance Ansatz macht die REST API noch selbsterklärender und ist in bestehende Spring HATEOAS Lösungen sehr einfach zu integrieren. Fehlen nur noch die Nutzer, die mit all dem umgehen können.