Files
spring-hateoas-examples/affordances
Greg Turnquist 04e0dcf4d0 Polishing
2017-12-05 17:20:27 -06:00
..
2017-12-05 12:29:03 -06:00
2017-12-05 12:29:03 -06:00
2017-12-05 17:20:27 -06:00

= Spring HATEOAS - Affordances Example

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 will create hypermedia-driven operations in the form of *templates*.

For starters, here is the basic definition:

[source,java]
----
Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
class Employee implements Identifiable<Long> {

	@Id @GeneratedValue
	private Long id;
	private String firstName;
	private String lastName;
	private String role;

	/**
	 * Useful constructor when id is not yet known.
	 */
	Employee(String firstName, String lastName, String role) {

		this.firstName = firstName;
		this.lastName = lastName;
		this.role = role;
	}

	public Optional<Long> getId() {
		return Optional.ofNullable(this.id);
	}
}
----

This domain object should look quite familiar by now.

A corresponding Spring Data JPA repository is defined:

[source,java]
----
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
----

From here, we need to build a resource assembler.

First of all, we need to extend `SimpleIdentifiableResourceAssembler` and hook it to `Employee`:

[source,java]
----
@Component
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {

	/**
	 * 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))));
	}
}
----

This code uses the same `getCollectionBuilder()` to point to the collection (`employees`) and connect to the controller's `newEmployee`
Spring MVC method.

So you want to round this out by defining the controller.

[source,java]
----
@RestController
class EmployeeController {

	private final EmployeeRepository repository;
	private final EmployeeResourceAssembler assembler;

	EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) {

		this.repository = repository;
		this.assembler = assembler;
	}

	...
}
----

For starters, you can declare a controller like this:

* `@RestController` makes the entire controller render responses as direct JSON and not rendered templates.
* Injects `EmployeeRepository` and `EmployeeResourceAssembler` through constructor injection.

Next, you need to define endpoints for the aggregate collection:

[source,java]
----
@RestController
class EmployeeController {

	...

	@GetMapping("/employees")
	ResponseEntity<Resources<Resource<Employee>>> findAll() {
		return ResponseEntity.ok(
			assembler.toResources(repository.findAll()));
	}

	@PostMapping("/employees")
	ResponseEntity<?> newEmployee(@RequestBody Employee employee) {

		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;

		/**
		 * Assume any {@link ObjectMapper} starts with {@literal _hal} and ends with {@literal Mapper}.
		 */
		@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;
		}

		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;
		}
	}
}
----

There is lot packed in here:

* `@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.

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.

IMPORTANT: We are working on simplifying the means to select different *and* multiple hypermedia formats.

Before launching the application, you'll want to pre-load some test data:

[source,java]
----
@Component
class DatabaseLoader {

	/**
	 * 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.

If you ping `/employees/1`, you can see an individual entry:

[source,javascript]
----
{
  "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 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.

This information could _also_ be used on your web site to generate update forms:

[source,html]
----
<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>
----

This is just one example of an update form.

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.

IMPORTANT: With HAL-FORMS, there is no URI in the template itself. It's presumed to operate on the *self* link.

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.

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.