Create copy for Affordances API

This commit is contained in:
Greg Turnquist
2017-12-05 12:27:19 -06:00
parent a128c72f6c
commit fe4a5923f8
2 changed files with 433 additions and 446 deletions

View File

@@ -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<Long> {
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
class Employee implements Identifiable<Long> {
@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<Employee> 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<Long> 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<Employee>`.
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<Manager, Long> {
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
----
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<Long> {
@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<Manager> {
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
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<Employee>`):
[source,java]
----
@Component
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
...
/**
* Define links to add to every {@link Resource}.
*
* @param resource
*/
@Override
protected void addLinks(Resource<Employee> 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<Employee>`. 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<Employee> {
...
/**
* Define links to add to {@link Resources} collection.
*
* @param resources
*/
@Override
protected void addLinks(Resources<Resource<Employee>> 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<Resources<Resource<Manager>>> 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<Resource<Manager>> 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<Resources<Resource<Employee>>> findEmployees(@PathVariable long id) {
@GetMapping("/employees")
ResponseEntity<Resources<Resource<Employee>>> 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<Employee, Long> {
List<Employee> 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<Manager> {
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<Resource<Employee>>`
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<Manager> 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<Resources<Resource<EmployeeWithManager>>> 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<Resource<EmployeeWithManager>> 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<EmployeeWithManager> {
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]
----
<form method="post" action="http://localhost:8080/employees">
<input type="text" id="firstName" name="firstName" placeHolder="firstName" />
<input type="text" id="id" name="id" placeHolder="id" />
<input type="text" id="lastName" name="lastName" placeHolder="lastName" />
<input type="text" id="role" name="role" placeHolder="role" />
<input type="submit" value="Submit" />
</form>
----
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<Resource<Employee>> 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<Employee> resource)` method:
[source,java]
----
@Component
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
...
/**
* Define links to add to every {@link Resource}.
*
* @param resource
*/
@Override
protected void addLinks(Resource<EmployeeWithManager> resource) {
protected void addLinks(Resource<Employee> 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<Resource<EmployeeWithManager>> 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<Employee>`, 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<T>`.
So instead, you want to fall back to `SimpleResourceAssembler<EmployeeWithManager>`, in which NO links are defined out of the box.
* Because there are no defined routes, you are in full control.
** `addLinks(Resource<EmployeeWithManager> resource)` defines links for single items
** `addLinks(Resources<Resource<EmployeeWithManager>> 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<Resource<EmployeeWithManager>> resources)` gives you access to a single item's `Resource<T>` 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<ResourceSupport> 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);
}
}
<form method="put" action="http://localhost:8080/employees/1">
<input type="text" id="firstName" name="firstName" placeHolder="firstName" />
<input type="text" id="id" name="id" placeHolder="id" />
<input type="text" id="lastName" name="lastName" placeHolder="lastName" />
<input type="text" id="role" name="role" placeHolder="role" />
<input type="submit" value="Submit" />
</form>
----
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 `<form method="post" _method="put" ...>`, 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<String> 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<Resource<Supervisor>> findOne(@PathVariable Long id) {
Resource<Manager> managerResource = controller.findOne(id).getBody();
Resource<Supervisor> 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<Supervisor>` 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<Resource<Manager>>` 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<Supervisor>` 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.

View File

@@ -46,10 +46,8 @@ class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Empl
protected void addLinks(Resource<Employee> 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())));
}