From fe4a5923f84bec46d2181d4368c85aae867a307d Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Tue, 5 Dec 2017 12:27:19 -0600 Subject: [PATCH] Create copy for Affordances API --- affordances/README.adoc | 873 +++++++++--------- .../examples/EmployeeResourceAssembler.java | 6 +- 2 files changed, 433 insertions(+), 446 deletions(-) diff --git a/affordances/README.adoc b/affordances/README.adoc index 8b06223..e5b962d 100644 --- a/affordances/README.adoc +++ b/affordances/README.adoc @@ -1,564 +1,553 @@ -= Spring HATEOAS - Hypermedia Example += Spring HATEOAS - Affordances Example -This guide shows a more detailed foray into linking resources with hypermedia. It includes automated links, custom ones, -and retaining legacy links to support older clients. +This guide shows the opportunities to build hypermedia that http://amundsen.com/blog/archives/1109[affords] operations using https://rwcbook.github.io/hal-forms/[HAL-FORMS]. Before proceeding, have you read these yet? . link:../basics[Spring HATEOAS - Basic Example] . link:../api-evolution[Spring HATEOAS - API Evolution Example] +. link:../hypermedia[Spring HATEOAS - Hypermedia Example] You may wish to read them first before reading this one. NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. +== Building "affordances" + +For starters, what is an *affordance*? It's what the hypermedia lets you _do_. With a HAL document, you are provided with data and links, but nothing else. + +This is the beauty of REST. By not having one megaspec, REST is able to adjust and adapt by adopting new mediatypes. So far, we've seen HAL, a lightweight +mediatype that inludes data and links. However, HAL doesn't illustrate what can be done with links. It's possible to use content negotation against +the links to see what REST verbs are supported, but even with that discovery, we still wouldn't know all the characteristics of a resource's properties. + +HAL-FORMS, an extension of HAL, attempts to bridge this gap. It supports the HAL concept of data + links, but introduces another element, *$$_templates$$*. +*$$_templates$$* make it possible to show all the operations possible as the attributes needed for each operation. + == Defining Your Domain -This example takes off where Basics and API Evolution end: an employee payroll system. Only this time, you'll introduce -a new domain: *managers*. - -You'll explore how to create create REST representations for a manager and tie it into employees. +This example takes off where Basics and API Evolution end: an employee payroll system. Only this time, you will create hypermedia-driven operations in the form of *templates*. For starters, here is the basic definition: [source,java] ---- -@Data +Data @Entity -@NoArgsConstructor -class Manager implements Identifiable { +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +class Employee implements Identifiable { @Id @GeneratedValue private Long id; - private String name; + private String firstName; + private String lastName; + private String role; /** - * To break the recursive, bi-directional interface, don't serialize {@literal employees}. + * Useful constructor when id is not yet known. */ - @JsonIgnore - @OneToMany(mappedBy = "manager") - private List employees = new ArrayList<>(); + Employee(String firstName, String lastName, String role) { - Manager(String name) { - this.name = name; + this.firstName = firstName; + this.lastName = lastName; + this.role = role; + } + + public Optional getId() { + return Optional.ofNullable(this.id); } } ---- -This is very similar to `Employee`: - -* Uses the same `@Data` Lombok annotation to reduce boilerplate in defining a mutable value object. -* They are stored in a JPA data store using `@Entity`, `@Id`, and `@GeneratedValue`. -* Has a `@NoArgsConstructor` to support Jackson's serializers. - -But it contains a new aspect: a 1-to-many relationship with `Employee` in the form a `List`. - -This domain object initializes the field with an empty list to avoid NPEs. The JPA `@OneToMany` annotation indicates -that the relationship between `Manager` and `Employee` is stored in the database tables in the `Employee` entity's -*manager* property, i.e. the manager's primary key will be stored as a foreign key in the *EMPLOYEE* table. - -WARNING: Bi-directional relationships can be modeled in JPA, but you must carefully handle this. Jackson tends to -navigate as far as possible when serializing, so you have to tell it to stop with the `@JsonIgnore` directive. Otherwise, -it will generate a stack overflow exception when hopping Manager -> Employee -> Manager -> etc. - -A handy constructor is also added to support link:src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java[loading the database] -with sample data. +This domain object should look quite familiar by now. A corresponding Spring Data JPA repository is defined: [source,java] ---- -interface ManagerRepository extends CrudRepository { +interface EmployeeRepository extends CrudRepository { } ---- -To round things out, you need to make some updates to the `Employee` domain object: +From here, we need to build a resource assembler. -[source,java] ----- -@Data -@Entity -@NoArgsConstructor -class Employee implements Identifiable { - - @Id @GeneratedValue - private Long id; - private String name; - private String role; - - /** - * To break the recursive, bi-directional relationship, don't serialize {@literal manager}. - */ - @JsonIgnore - @OneToOne - private Manager manager; - - Employee(String name, String role, Manager manager) { - - this.name = name; - this.role = role; - this.manager = manager; - } -} ----- - -This is very similar to what you saw in *Basics*, except that now there is a 1-to-1 JPA relationship in the *manager* field. - -The constructor call has also been updated. Finally, the same stack overflow is blocked from this end by also putting a `@JsonIgnore` -Jackson annotation on the *manager* field. - -With these changes in place, you can now define a `ResourceAssembler` for the `Manager`: +First of all, we need to extend `SimpleIdentifiableResourceAssembler` and hook it to `Employee`: [source,java] ---- @Component -class ManagerResourceAssembler extends SimpleIdentifiableResourceAssembler { +class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { - ManagerResourceAssembler() { - super(ManagerController.class); + /** + * Link the {@link Employee} domain type to the {@link EmployeeController} using this + * {@link SimpleIdentifiableResourceAssembler} in order to generate both {@link org.springframework.hateoas.Resource} + * and {@link org.springframework.hateoas.Resources}. + */ + EmployeeResourceAssembler() { + super(EmployeeController.class); + } + + ... +} +---- + +This links the domain type of `Employee` to the Spring MVC controller (you'll build further down) `EmployeeController`. This makes it possible to +build links. + +Next you need to define the links for a single resource `Employee` (denoted by Spring HATEOAS's `Resource`): + +[source,java] +---- +@Component +class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { + + ... + + /** + * Define links to add to every {@link Resource}. + * + * @param resource + */ + @Override + protected void addLinks(Resource resource) { + + resource.getContent().getId() + .ifPresent(id -> { + resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel() + .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id)))); + }); + + resource.add(getCollectionLinkBuilder().withRel(this.getRelProvider().getCollectionResourceRelFor(this.getResourceType()))); + } + ... +} +---- + +A link is built by looking up the `getCollectionLinkBuilder()` to find the collection name (using Spring HATEOAS's `RelProvider` to turn `Employee` into `employees`), +followed by a slash ("/"), and the content of the `Resource`. Because this resource implements `Identifiable`, Spring +HATEOAS know how to get the `id` of the resource. This URI is converted into a Spring HATEOAS *self* `Link` through `withSelfRel()`. + +When it comes to HAL documents, this is all we need. HAL is based on data combined with simple links. + +Affordances is about chaining related links together to support richer mediatypes. In this case, we have HAL-FORMS support. This means +we can connect the *GET* link to its related *PUT* link using the `andAffordance(afford(methodOn(...))`. + +The `methodOn()` API works just like the other examples show. But the `afford()` operation, based on web-specific technology (in this +case Spring MVC), is able to look up details about the endpoint and flesh out the *_templates* section of a HAL-FORMS document. + +Similarly, you need to link the aggregate *GET* link to the corresponding *POST* link. See below: + +[source,java] +---- +@Component +class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { + + ... + /** + * Define links to add to {@link Resources} collection. + * + * @param resources + */ + @Override + protected void addLinks(Resources> resources) { + resources.add(getCollectionLinkBuilder().withSelfRel() + .andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))); } } ---- -If you follow the same paradigm of extending Spring HATEOAS's `SimpleIdentifiableResourceAssembler` and applying the `Manager` type, -you can easily inherit links for */managers* and */managers/{id}* +This code uses the same `getCollectionBuilder()` to point to the collection (`employees`) and connect to the controller's `newEmployee` +Spring MVC method. -Before we go any further, we need to define those links! +So you want to round this out by defining the controller. [source,java] ---- @RestController -class ManagerController { +class EmployeeController { - private final ManagerRepository repository; - private final ManagerResourceAssembler assembler; + private final EmployeeRepository repository; + private final EmployeeResourceAssembler assembler; - ManagerController(ManagerRepository repository, ManagerResourceAssembler assembler) { + EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) { this.repository = repository; this.assembler = assembler; } - /** - * Look up all managers, and transform them into a REST collection resource using - * {@link ManagerResourceAssembler#toResources(Iterable)}. Then return them through - * Spring Web's {@link ResponseEntity} fluent API. - * - * NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different - * default accept header, which allows XML to get requested first, so "produces" - * forces it to HAL JSON for all clients. - */ - @GetMapping(value = "/managers", produces = MediaTypes.HAL_JSON_VALUE) - ResponseEntity>> findAll() { - return ResponseEntity.ok( - assembler.toResources(repository.findAll())); - - } - - /** - * Look up a single {@link Manager} and transform it into a REST resource using - * {@link ManagerResourceAssembler#toResource(Object)}. Then return it through - * Spring Web's {@link ResponseEntity} fluent API. - * - * See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument. - * - * @param id - */ - @GetMapping(value = "/managers/{id}", produces = MediaTypes.HAL_JSON_VALUE) - ResponseEntity> findOne(@PathVariable long id) { - return ResponseEntity.ok( - assembler.toResource(repository.findOne(id))); - } + ... } ---- -This controller should look familar, since it's almost identical to `EmployeeController` as seen in link:../api-evolution[API Evolution]. -You have simply swapped */employees* with */managers* and plugged in `ManagerRepository` and `ManagerResourceAssembler`. +For starters, you can declare a controller like this: -IMPORTANT: It's not a requirement to use a `ResourceAssembler`. But having one place to define all links for a given domain object -ensures a consistent representation. +* `@RestController` makes the entire controller render responses as direct JSON and not rendered templates. +* Injects `EmployeeRepository` and `EmployeeResourceAssembler` through constructor injection. -With the basic routes defined, you could say we have an operational REST service. But it's not fleshed out very well. To truly -power up the hypermedia and serve clients, you need to add links _between_ the relevant domain types. +Next, you need to define endpoints for the aggregate collection: -NOTE: Up until this point, we've been using the term "domain types" or "domain objects". This is lingo found in Domain Driven Design. -What you are building are *REST resources* and how the various mediatypes they are represented in. The paradigm of REST is -to construct resources that contain both data for the client to consume as well as controls to navigate to related data. - -The first link to navigate from a `Manager` resource to its related `Employee` resources would be a */managers/{id}/employees* -route. Since a controller that yields employee objects would be found in the `EmployeeController`, we need to make the following alterations: - -.EmployeeController [source,java] ---- @RestController class EmployeeController { ... - - /** - * Find an {@link Employee}'s {@link Manager} based upon employee id. Turn it into a context-based link. - * - * @param id - * @return - */ - @GetMapping(value = "/managers/{id}/employees", produces = MediaTypes.HAL_JSON_VALUE) - public ResponseEntity>> findEmployees(@PathVariable long id) { + + @GetMapping("/employees") + ResponseEntity>> findAll() { return ResponseEntity.ok( - assembler.toResources(repository.findByManagerId(id))); + assembler.toResources(repository.findAll())); } -} ----- -We've added another route, but how are we getting the data? Oh yeah, we need to add another finder! + @PostMapping("/employees") + ResponseEntity newEmployee(@RequestBody Employee employee) { -[source,java] ----- -interface EmployeeRepository extends CrudRepository { - - List findByManagerId(Long id); - -} ----- - -With Spring Data, we can define a new finder _just by writing it's method signature!_ This custom finder will navigate by property -and find a list of employees pointed at the chosen manager id. - -NOTE: Navigation by property is analogous to writing `select EMPLOYEE.* from EMPLOYEE join MANAGER on MANAGER.PK = EMPLOYEE.FK where MANAGER.PK == :id`. -It makes it super simple to navigate over JPA relationships and find what we need. - -This newly minted route needs to be added to every `Manager` representation we render. To do that, we need to make an alteration -to `ManagerResourceAssembler`: - -[source,java] ----- -@Component -class ManagerResourceAssembler extends SimpleIdentifiableResourceAssembler { + return repository.save(employee).getId() + .map(this::findOne) + .map(HttpEntity::getBody) + .flatMap(ResourceSupport::getId) + .map(Link::getHref) + .map(href -> { + try { + return new URI(href); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + }) + .map(uri -> ResponseEntity.noContent().location(uri).build()) + .orElse(ResponseEntity.badRequest().body("Unable to create " + employee)); + } ... +} +---- + +This fragment of the controller shows: + +* A *GET* call for the aggregate collection is defined. It uses the repository's `findAll()` method and transforms it into a `Resources>` +using the `EmployeeResourceAssembler`. +* A *POST* call for creating new employees is also defined, on the same URI. `@RequestBody` tells Spring MVC to deserialize the request body into an `Employee` object, +which is then sent through the repository's `save()` operation. From there, you grab the `Optional` *id* + +The premise is that the *POST* endpoint is related to the *GET* endpoint. In other words, the URI at `/employees` services a *GET* call while _also affording_ a *POST* call. + +To get this operational, you must do one additional step--reconfigure hypermedia. By default, Spring Boot sets things up for HAL. To switch to HAL-FORMS, you need to create this: + +[source,java] +---- +@Configuration +@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) +public class HypermediaConfiguration { + + @Bean + public static HalObjectMapperConfigurer halObjectMapperConfigurer() { + return new HalObjectMapperConfigurer(); + } + + private static class HalObjectMapperConfigurer + implements BeanPostProcessor, BeanFactoryAware { + + private BeanFactory beanFactory; - /** - * Retain default links provided by {@link SimpleIdentifiableResourceAssembler}, but add extra ones to each {@link Manager}. - * - * @param resource - */ - @Override - protected void addLinks(Resource resource) { /** - * Retain default links. + * Assume any {@link ObjectMapper} starts with {@literal _hal} and ends with {@literal Mapper}. */ - super.addLinks(resource); + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof ObjectMapper && beanName.startsWith("_hal") && beanName.endsWith("Mapper")) { + postProcessHalObjectMapper((ObjectMapper) bean); + } + return bean; + } - // Add custom link to find all managed employees - resource.add(linkTo(methodOn(EmployeeController.class).findEmployees(resource.getContent().getId())).withRel("employees")); + private void postProcessHalObjectMapper(ObjectMapper objectMapper) { + try { + Jackson2ObjectMapperBuilder builder = this.beanFactory.getBean(Jackson2ObjectMapperBuilder.class); + builder.configure(objectMapper); + } catch (NoSuchBeanDefinitionException ex) { + // No Jackson configuration required + } + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } } - - ... -} - ----- - -`SimpleIdentifiableResourceAssembler` has methods to alter a resource representation for single items or collections. It has pre-baked -renderings to create a self link to a single item as well as a link back to the collection. In this code, you are extending that -method and invoking `super.addLinks()` in order to include those links. Then you add the link to the manager's employees you just created. - -IMPORTANT: You can either _add_ to the links defined by `SimpleIdentifiableResourceAssembler` as shown, or you can totally replace them by _not_ -invoking `super.addLinks()`. Your choice. - -There is a corresponding combination of a route/repository finder/assembler to allow an employee to find his or her manager. It's left as an exericise -for you to discover it in `ManagerController`, `ManagerRepository`, and `EmployeeResourceAssembler`. - -== Augmenting Representations - -Some critics of REST will point to certain toolkits or coded solutions and argue that "hopping" can be inefficient. A common example is -a relational set of tables that through 3NF (3rd Normal Form) split up data between a parent/child relationship. In essence, part of the data -is in the parent table, part in the child table. The parent table's data is shown along with a link to navigate to the child table's data. - -This is a false comparison, because REST wholely supports merging data if it makes sense. In DDD, such items are referred to as *aggregates*. -Nothing about a REST resource is confined by the rules of 3NF, written forty years ago. That can simply be shortfall of certain -toolkits (but not Spring HATEOAS!) - -What if you wanted a detailed `Employee` representation that included the `Manager` details? No problem! Just model it. - -[source,java] ----- -@Value -@JsonPropertyOrder({"id", "name", "role", "manager"}) -public class EmployeeWithManager { - - @JsonIgnore - private final Employee employee; - - public Long getId() { - return this.employee.getId(); - } - - public String getName() { - return this.employee.getName(); - } - - public String getRole() { - return this.employee.getRole(); - } - - public String getManager() { - return this.employee.getManager().getName(); - } - } ---- -This _immutable_ value object (thanks to Lombok's `@Value` annotation) is initialized with an `Employee` object. It defines -how it gets rendered through various getter methods. It also subtly does _not_ render the `Employee` object itself. +There is lot packed in here: -IMPORTANT: `Employee` and `Manager` both have a *name* field. With combined representations, there has to be agreement on how these -two fields will appear. In this case, `Employee.name` is kept and `Manager.name` is turned into *manager*. +* `@Configuration` makes this class automatically picked up by Spring Boot's component scanning. +* `@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)` activates Spring HATEOAS's hypermedia support, setting the format to HAL-FORMS. +* When you use this annotation, all of Spring Boot's autoconfigured hypermedia support is disabled. You are taking over, so the rest of the code is + about finding any registered `ObjectMapper` beans in the app context and registering the HAL-FORMS support through builtin callbacks. -To support this, we can write the corresponding route in `EmployeeController`: +WARNING: You currently cannot support more than one hypermedia-based mediatype as this point in time. If you try to use both `HAL` and `HAL_FORMS` in the annotation, +Spring Boot will fail to launch. -[source,java] ----- -@GetMapping(value = "/employees/detailed", produces = MediaTypes.HAL_JSON_VALUE) -public ResponseEntity>> findAllDetailedEmployees() { +IMPORTANT: We are working on simplifying the means to select different *and* multiple hypermedia formats. - return ResponseEntity.ok( - employeeWithManagerResourceAssembler.toResources( - StreamSupport.stream(repository.findAll().spliterator(), false) - .map(EmployeeWithManager::new) - .collect(Collectors.toList()))); -} - -@GetMapping(value = "/employees/{id}/detailed", produces = MediaTypes.HAL_JSON_VALUE) -public ResponseEntity> findDetailedEmployee(@PathVariable Long id) { - - Employee employee = repository.findOne(id); - - return ResponseEntity.ok( - employeeWithManagerResourceAssembler.toResource( - new EmployeeWithManager(employee))); -} ----- - -This shows both a collection of "detailed" employees as well as a single one. The collection fetches all employees, uses a Java 8 -stream to convert each `Employee` into an `EmployeeWithManager`, and wraps it into a Spring HATEOAS `Resources` collection. - -The single employee version does the corresponding transformation against a single `Employee`. - -To support building REST resources, you also need a `ResourceAssembler` for `EmployeeWithManager`. This should appear very -familiar by now: +Before launching the application, you'll want to pre-load some test data: [source,java] ---- @Component -class EmployeeWithManagerResourceAssembler extends SimpleResourceAssembler { +class DatabaseLoader { /** - * Define links to add to every individual {@link Resource}. + * Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run + * only after the app is operational, the database will be up. + * + * @param repository + */ + @Bean + CommandLineRunner init(EmployeeRepository repository) { + return args -> { + repository.save(new Employee("Frodo", "Baggins", "ring bearer")); + repository.save(new Employee("Bilbo", "Baggins", "burglar")); + }; + } + +} +---- + +This little database loader will: + +* Be picked up by component scanning due to the `@Component` annotation. +* The `CommandLineRunner` bean is executed by Spring Boot after the entire application context is up. +* Inside that chunk of code, the injected `EmployeeRepository` is used to create a couple database entries. + +NOTE: The database for this example is `H2`, an in-memory database that always starts up empty. If you switch to a persistent store, you probably need +to include the extra step to delete old data or you'll get multiple entries. + +If you launch the application and `GET /employees`, you can expect the following HAL-FORMS result: + +[source,javascript] +---- +{ + "_embedded": { + "employees": [...] + }, + "_links": { + "self": { + "href": "http://localhost:8080/employees" + } + }, + "_templates": { + "default": { + "title": null, + "method": "post", + "contentType": "", + "properties":[ + { + "name": "firstName", + "required": true + }, + { + "name": "id", + "required": true + }, + { + "name": "lastName", + "required": true + }, + { + "name": "role", + "required": true + } + ] + } + } +} +---- + +This fragment of JSON can be described as follows: + +* The *_embedded* chunk has been shrunk down for space reasons. It contains an array of `Employee` resources, which you'll see in more detail further down. +* The *_links* section is just like a HAL document, showing the *self* link to `localhost:8080/employees` that you declared. +* The *_templates* section is the HAL-FORMS extension that shows the *affordance* defined that pointed to the `newEmployee` method, which was mapped onto the *POST* method. +** Inside the template, the method is clearly marked *post*. +** The properties are: *firstName*, *id*, *lastName*, and *role*, and all marked as *required*. +** The other characteristics (title, contentType) are not filled out. There are more attributes, but nothing (yet) that can be gleaned from a plain old Spring MVC route. + +This template data is enough information for you to generate an HTML form on a web page using a little JavaScript. Possibly one like this: + +[source,html] +---- +
+ + + + + +
+---- + +You can also define affordances at the individual resource level. In this situation, you can start first by defining the controller methods: + +[source,java] +---- +@RestController +class EmployeeController { + + ... + + @GetMapping("/employees/{id}") + ResponseEntity> findOne(@PathVariable long id) { + + return repository.findById(id) + .map(assembler::toResource) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PutMapping("/employees/{id}") + ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable long id) { + + Employee employeeToUpdate = employee; + employeeToUpdate.setId(id); + + return repository.save(employeeToUpdate).getId() + .map(this::findOne) + .map(HttpEntity::getBody) + .flatMap(ResourceSupport::getId) + .map(Link::getHref) + .map(href -> { + try { + return new URI(href); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + }) + .map(uri -> ResponseEntity.noContent().location(uri).build()) + .orElse(ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate)); + } + + ... +} +---- + +This augments the same REST controller with a *GET* operation for an individual `Employee` and also defines the corresponding *PUT* operation +to update/edit. + +Take your team to read both flows. The key part you must define, is the corresponding `EmployeeResourceAssembler.toResource(Employee)` method. +In the heart of that method, is its `addLinks(Resource resource)` method: + +[source,java] +---- +@Component +class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler { + + ... + + /** + * Define links to add to every {@link Resource}. * * @param resource */ @Override - protected void addLinks(Resource resource) { + protected void addLinks(Resource resource) { - resource.add(linkTo(methodOn(EmployeeController.class).findDetailedEmployee(resource.getContent().getId())).withSelfRel()); - resource.add(linkTo(methodOn(EmployeeController.class).findOne(resource.getContent().getId())).withRel("summary")); - resource.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); + resource.getContent().getId() + .ifPresent(id -> resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel() + .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))))); + + resource.add(getCollectionLinkBuilder().withRel(this.getRelProvider().getCollectionResourceRelFor(this.getResourceType()))); } - /** - * Define links to add to the {@link Resources} collection. - * - * @param resources - */ - @Override - protected void addLinks(Resources> resources) { - - resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withSelfRel()); - resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); - resources.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); - resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); - } + ... } ---- -This has a handful of differences from the `ResourceAssembler` objects you've built up to this point: +In this situation, you are taking an individual `Resource`, extracting the contents (i.e. the `Employee` itself), grabbing +its `Optional` *id*, and if present, creating a *self* link to it. Again, because `Employee` implements `Identifiable`, `slash(Employee)` +is able to extract the *id* field to form a proper link. This defines the *GET* endpoint as a Spring HATEOAS `Link`. -* Since the routes are different than traditional */employees* and */employees/{id}*, it makes no sense to use `SimpleIdentifiableResourceAssembler`. - So instead, you want to fall back to `SimpleResourceAssembler`, in which NO links are defined out of the box. -* Because there are no defined routes, you are in full control. -** `addLinks(Resource resource)` defines links for single items -** `addLinks(Resources> resources)` defines links for collections +Using this *self* link, you then add an affordance to the controller's `updateEmployee` method. Spring HATEOAS's Affordances API +is able to inspect the Spring MVC annotations and extract information to render a HAL-FORMS endpoint. -In this case, single `EmployeeWithManager` items include a self link to itself, a hop to it's parallel record that only has `Employee` info known as *summary*, -and a link to the detailed collection. To avoid semantic confusion, this is called *detailedEmployees* given *employees* is the common reference to -a collection of summary `Employee` records. +If you restart the application and ping `/employees/1`, you can see an individual entry: -It also makes sense to add links from the other existing REST resources to this detailed `EmployeeWithManager`. - -WARNING: Even though `addLinks(Resources> resources)` gives you access to a single item's `Resource` object, - it is recommended to NOT manipulate individual item links this way. Instead, use the other method. - -Is this the _only_ way to display a detailed record? Not at all. Spring MVC supports request parameters, so it's not that difficult -to code something like this: - -[source,java] +[source,javascript] ---- -@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE) -public ResponseEntity findOne(@PathVariable long id, - @RequestParam(value = "detailed", required = false, - defaultValue = false) boolean detailed) { - - if (detailed) { - Employee employee = repository.findOne(id); - - return ResponseEntity.ok( - employeeWithManagerResourceAssembler.toResource( - new EmployeeWithManager(employee))); - } else { - return ResponseEntity.ok( - assembler.toResource(repository.findOne(id))); - } +{ + "id": 1, + "firstName": "Frodo", + "lastName": "Baggins", + "role": "ring bearer", + "_links": { + "self": { + "href": "http://localhost:8080/employees/1" + }, + "employees": { + "href": "http://localhost:8080/employees" + } + }, + "_templates": { + "default": { + "title": null, + "method": "put", + "contentType": "", + "properties": [ + { + "name": "firstName", + "required": true + }, + { + "name": "id", + "required": true + }, + { + "name": "lastName", + "required": true + }, + { + "name": "role", + "required": true + } + ] + } + } } ---- -This type of solution allows serving two different representations from the same URI based on an optional `?detailed=true` -parameter. +* This is very similar to what you saw before, only there is no *_embedded* element. Instead, the resource's data is at the top level. +* There are two links: *self* for the canonical link to itself and *employees* to lead back to the aggregate root. +* The method of this template is *put* instead of *post*, indicating this is for updates. +* All the properties are listed, being the same as shown at the aggregate root. -There are tradeoffs either way, but this option lends itself to supporting existing routes that you may already have. +This information could _also_ be used on your web site to generate update forms: -To find the other places where detailed `EmployeeWithManager` links have been added, inspect all the `ResourceAssembler` objects -in the example's code base. - -== Don't Forget the Root URI - -In order to "start at the top" and hop, you must include a `RootController`: - -[source,java] +[source,html] ---- -@RestController -class RootController { - - @GetMapping("/") - ResponseEntity root() { - - ResourceSupport resourceSupport = new ResourceSupport(); - - resourceSupport.add(linkTo(methodOn(RootController.class).root()).withSelfRel()); - resourceSupport.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); - resourceSupport.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); - resourceSupport.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); - - return ResponseEntity.ok(resourceSupport); - } - -} +
+ + + + + +
---- -Because there is no data at the top, just links, returning back a `ResourceSupport` is perfect. This allows defining all the top links. +This is just one example of an update form. -And it's easy to go into the various `ResourceAssemblers` and add a link back to the top as needed. It's up to you to see which -bits of hypermedia serve such a link. +NOTE: `method="put"` isn't exactly valid HTML5. Either you can handle that in your code, or you have some sort of filter like Spring MVC's +`HiddenHttpMethodFilter` that lets you construct it as `
`, which converts a *POST* into a *PUT* before +invoking the code. -== Legacy Routes +IMPORTANT: With HAL-FORMS, there is no URI in the template itself. It's presumed to operate on the *self* link. -What if you started with one set of routes and migrated things to another set? This is the type of scenario that drives people screaming -to version their APIs. +With the Affordances API, you can link related methods. And with HAL-FORMS support, it's possible to turn those relationships into automated +bits of HTML to enhance the user experience without having to inject domain knowledge into the client layer. -Instead of shouting "don't version APIs" from the rooftops, and appealing to the authority of Roy Fielding, it's better to see -how it's not that hard to support both old and new routes. - -For this example, assume that before the `Manager` entity and it's `ManagerController` existed, there was a `Supervisor` with a -matching `SupervisorController`. It had similar data but fewer links. A bit more RPC-like. If the original `Supervisor` entity -was gone, we can add a DTO to represent the old format based on `Manager` like this: - -[source,java] ----- -/** - * Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an after - * thought. - * - * @author Greg Turnquist - */ -@Value -@JsonPropertyOrder({"id", "name", "employees"}) -class Supervisor { - - @JsonIgnore - private final Manager manager; - - public Long getId() { - return this.manager.getId(); - } - - public String getName() { - return this.manager.getName(); - } - - public List getEmployees() { - return manager.getEmployees().stream() - .map(employee -> employee.getName() + "::" + employee.getRole()) - .collect(Collectors.toList()); - } -} ----- - -This representation assumes old record had: - -* Supervisor's *id*, *name* and a somewhat sloppy display of employee's name and role. -* It's powered by the new `Manager` object, so no need to store multiple copies of data. -* The `Manager` itself is not rendered thanks to the `@JsonIgnore` annotation. - -To honor the old route (*/supervisors/{id}*), create a new controller: - -[source,java] ----- -/** - * Represent an older controller that has since been replaced with {@link ManagerController}. - * This controller is used to provide legacy routes, i.e. backwards compatibility. - * - * @author Greg Turnquist - */ -@RestController -public class SupervisorController { - - private final ManagerController controller; - - public SupervisorController(ManagerController controller) { - this.controller = controller; - } - - @GetMapping(value = "/supervisors/{id}", produces = MediaTypes.HAL_JSON_VALUE) - public ResponseEntity> findOne(@PathVariable Long id) { - - Resource managerResource = controller.findOne(id).getBody(); - Resource supervisorResource = new Resource<>( - new Supervisor(managerResource.getContent()), - managerResource.getLinks()); - - return ResponseEntity.ok(supervisorResource); - } -} ----- - -In this example, the assumption is that there was a route for individual supervisors, but not a link for a collection. -This controller has that route, and serves up a `Resource` record. But instead of fetching the data directly, -it leverages the `ManagerController`. - -Is that a good idea or a bad idea? - -Again, there are tradeoffs. This example is meant to illustrate other options. In this case, leveraging `ManagerController` -allows all links to be generated courtesy of the `ManagerResourceAssembler`. When a `ResponseEntity>` object -is returned by the controller, its wrapped REST resource is extracted by Spring MVC's `getBody()` method. - -A new `Supervisor` REST resource is constructed by injecting the `Manager` into a `Supervisor` DTO. The provided links are -then copied into that `Resource` object. - -Hence, this controller will respond to calls for */supervisors/{id}*, but provide links onto the new system should the client -want to gracefully start migrating. - -IMPORTANT: This example also assumes the clients can handle new links as long as the legacy ones are also there. For -a different scenario, that assumption can be adjusted. - -With this amount of linking between related objects and DTOs, it's easy to see how Spring HATEOAS can be used to model -a link-driven API. And with the flexible nature of REST, more links can be added in the future along with additional representations. -As long as the existing links are maintained, clients can have a much easier path of migration. +And that's a key part of REST--reducing the amount of domain knowledge found in the client, allowing the client to more easily adapt to +changes on the server. \ No newline at end of file diff --git a/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java b/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java index f1917f0..f7dd5da 100644 --- a/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java +++ b/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java @@ -46,10 +46,8 @@ class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler resource) { resource.getContent().getId() - .ifPresent(id -> { - resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel() - .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id)))); - }); + .ifPresent(id -> resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel() + .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))))); resource.add(getCollectionLinkBuilder().withRel(this.getRelProvider().getCollectionResourceRelFor(this.getResourceType()))); }