Affordance: Was kann ich mit der REST Resource anstellen?

„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.