Create copy for Affordances API
This commit is contained in:
@@ -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.
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user