Skip to content

Latest commit

 

History

History

affordances

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Spring HATEOAS - Affordances Example

This guide shows the opportunities to build hypermedia that affords operations using HAL-FORMS.

Before proceeding, have you read these yet?

You may wish to read them first before reading this one.

Note
This example uses Project Lombok to reduce writing Java code.

Building "affordances"

For starters, what is an affordance? Doing a little archeology, Mike Admundsen, a REST advocate, has an article detailing the word’s origins, going back at least to 1986:

The affordances of the environment are what it offers …​ what it provides or furnishes, either for good or ill.
The verb 'to afford' is found in the dictionary, but the noun 'affordance' is not. I have made it up (page 126).
— The Ecological Approach to Visual Perception (Gibson)

It then appeared in a psychology paper in 1988:

…​the term affordance refers to the perceived and actual properties of the thing, primarily those fundamental properties
that determine just how the thing could possibly be used. (pg 9)
— The Design|Psychology of Everyday Things (Norman)

Finally, it can be found in none other than one of Roy Fielding’s presentations on hypermedia in 2008:

When I say Hypertext, I mean the simultaneous presentation of information and controls such that the information becomes
the affordance through which the user obtains choices and selects actions (slide #50).
— Slide presention on REST (Fielding)

In all these situations, "affordance" refers to the available actions provided by the surrounding environment. In the context of REST, these are actions detailed by the hypermedia. With a HAL document, you are provided very simple affordances. The links are shown but nothing else about them. What you can do with the links and what it takes to interact with them is not detailed.

You can use content negotation to discover what HTTP verbs are supported. And you can take a shot at supplying properties based on existing data records. But the beauty of REST is that by not having a single megaspec, you can adjust and adapt by adopting new media types. HAL has been well received, but perhaps there is something better?

HAL-FORMS, an extension of HAL, attempts to bridge this gap. It supports HAL’s lightweight of concept of data and links, but introduces another element, _templates. _templates make it possible to show all the operations possible as well as the attributes needed for each operation.

The following bits of code show how to use Spring HATEOAS’s Affordances API to produce HAL-FORMS documents.

Defining Your Domain

This example takes off where Basics and API Evolution end: an employee payroll system.

Here is the basic definition:

@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
class Employee {

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

This domain object should look quite familiar by now.

A corresponding Spring Data JPA repository is defined:

interface EmployeeRepository extends CrudRepository<Employee, Long> {
}

This barebones repository provides standard Spring Data CRUD operations.

Next, you define a REST controller:

@RestController
class EmployeeController {

	private final EmployeeRepository repository;

	EmployeeController(EmployeeRepository repository) {
		this.repository = repository;
	}

	...
}

This controller has these characteristics:

  • @RestController makes the entire controller render responses as direct JSON and not rendered templates.

  • Injects EmployeeRepository through constructor injection.

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

@RestController
class EmployeeController {

	...

	@GetMapping("/employees")
	ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {

		List<EntityModel<Employee>> employeeResources = StreamSupport.stream(repository.findAll().spliterator(), false)
				.map(employee -> EntityModel.of(employee,
						linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
								.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
								.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
						linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
				.collect(Collectors.toList());

		return ResponseEntity.ok(CollectionModel.of( //
				employeeResources, //
				linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()
						.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))));
	}

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

		Employee savedEmployee = repository.save(employee);

		return EntityModel.of(savedEmployee,
				linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel()
						.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, savedEmployee.getId())))
						.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(savedEmployee.getId()))),
				linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")).getLink(IanaLinkRelations.SELF)
						.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));
	}

	...
}

Look at these controller details:

  • A GET call for the aggregate collection is laid out. It uses the repository’s findAll() method and transforms it into a CollectionModel<EntityModel<Employee>>.

  • 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, it’s wrapped as a Resource with links added to itself and to the aggregate root. Finally a Location response header

  • Buried in both endpoints is the new .andAffordance() API. Instead of linkTo(), you instead use the afford() API to show related information.

Affordances is about chaining related links together to support richer media types. In this case, Spring HATEOAS has HAL-FORMS support. This means you can connect the GET link to its related POST link using the andAffordance(afford(methodOn(…​)). A given link can also connect to multiple affordances. That’s why this example also shows linking to the deleteEmployee endpoint as well.

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.

The premise is that the POST endpoint and the DELETE endpoint are related to the GET endpoint. In other words, the URI at /employees services a GET call while also affording a POST call and a DELETE call. And with the Affordances API, it captures the important details found in the related Spring MVC endpoint. When fetching a list of employees, there are two sets of links, the links for each individual entry along with the aggregate links.

In the aggregate links, you can see a self link to the collection, but connected, i.e. afforded to the newEmployee() endpoint. For each individual employee in the collection, there is a self link to itself along with an affordance to the updateEmployee() endpoint, that you’ll define next.

Check it out below:

@RestController
class EmployeeController {

	...

	@GetMapping("/employees/{id}")
	ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {

		return repository.findById(id)
				.map(employee -> EntityModel.of(employee,
						linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
								.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
								.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
						linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
				.map(ResponseEntity::ok) //
				.orElse(ResponseEntity.notFound().build());
	}

	@PutMapping("/employees/{id}")
	ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable long id) {

		Employee employeeToUpdate = employee;
		employeeToUpdate.setId(id);

		Employee updatedEmployee = repository.save(employeeToUpdate);

		return EntityModel.of(updatedEmployee,
				linkTo(methodOn(EmployeeController.class).findOne(updatedEmployee.getId())).withSelfRel()
						.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, updatedEmployee.getId())))
						.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(updatedEmployee.getId()))),
				linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")).getLink(IanaLinkRelations.SELF)
						.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 will look very similar, but focused on single item employees.

Note
Are you sensing a repeat of code just seen? Like how the same links and affordances are defined here as was shown for each individual part of the aggregate root? It’s possible to refactor this code into a ResourceAssembler to define a single location, but for simplicity, it has been left out of this example.

To round out this controller, you must also code the deleteEmployee() operation:

 @RestController
 class EmployeeController {

	...

	@DeleteMapping("/employees/{id}")
	ResponseEntity<?> deleteEmployee(@PathVariable long id) {

		repository.deleteById(id);

		return ResponseEntity.noContent().build();
	}

	...
}

This operation is quite simple. It deletes based upon id then returns an HTTP 204 No Content response.

Our controller has been made more sophisticated by linking related operations together. However, to take advantage of this, you must shift gears and use a different hypermedia-based media type. This demands on additional step. By default, Spring Boot sets things up for HAL. To switch to HAL-FORMS, you need to create this:

@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HypermediaConfiguration {

}

There is lot packed in this tiny class:

  • @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.

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

@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 Spring Boot’s 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:

{
  "_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:

<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>
Important
Spring HATEOAS doesn’t provide the JavaScript to do this. This hypermedia format, though, has all the information you need to create it yourself. Or deploy somebody’s 3rd party library that speaks HAL-FORMS.

Are you wondering why Spring HATEOAS doesn’t simply render an HTML form straight up? There are other media types designed for this, especially XHTML. Using the Affordances API, we plan to add support in the future, allowing you to negotiate for the format you prefer.

Do you want HAL? HAL-FORMS? SIREN? XHTML? Whatever format you need, the relation between endpoints doesn’t have to change. Simply what you configure the server to render.

To round things out, you can also interrogate a single employee resource as shown below:

{
  "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
        }
      ]
    },
    "deleteEmployee": {
      "title": null,
      "method": "delete",
      "contentType": "",
      "properties": []
    }
  }
}
  • 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 the default 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 is a second template, deleteEmployee with a method of delete. It has no properties meaning all you need is the URI to delete an existing employee.

This information could easily be used on your web site to generate an update form:

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

You could also craft another form based on the deleteEmployee template:

<form method="delete" action="http://localhost:8080/employees/1">
	<input type="submit" value="Submit" />
</form>

These are just a couple ways to render forms based on the hypermedia’s templates.

Note
method="put" and method="delete" aren’t valid HTML5. Either you 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 needed in the client. Teach your clients a little more knowledge about the protocol, and they won’t have to know as much about the domain. Instead, push the relevant forms straight out to the end user, are more easily adapt to changes on the server!