2. Fundamentals
This section covers the basics of Spring HATEOAS and its fundamental domain abstractions.
2.1. Links
The fundamental idea of hypermedia is to enrich the representation of a resource with hypermedia elements. The simplest form of that are links. They indicate a client that it can navigate to a certain resource. The semantics of a related resource are defined in a so-called link relation. You might have seen this in the header of an HTML file already:
<link href="theme.css" rel="stylesheet" type="text/css" />
As you can see the link points to a resource theme.css
and indicates that it is a style sheet.
Links often carry additional information, like the media type that the resource pointed to will return.
However, the fundamental building blocks of a link are its reference and relation.
Spring HATEOAS lets you work with links through its immutable Link
value type.
Its constructor takes both a hypertext reference and a link relation, the latter being defaulted to the IANA link relation self
.
Read more on the latter in Link relations.
Link link = Link.of("/something");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
link = Link.of("/something", "my-rel");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("my-rel"));
Link
exposes other attributes as defined in RFC-8288.
You can set them by calling the corresponding wither method on a Link
instance.
Find more information on how to create links pointing to Spring MVC and Spring WebFlux controllers in Building links in Spring MVC and Building links in Spring WebFlux.
2.2. URI templates
For a Spring HATEOAS Link
, the hypertext reference can not only be a URI, but also a URI template according to RFC-6570.
A URI template contains so-called template variables and allows expansion of these parameters.
This allows clients to turn parameterized templates into URIs without having to know about the structure of the final URI, it only needs to know about the names of the variables.
Link link = Link.of("/{segment}/something{?parameter}");
assertThat(link.isTemplated()).isTrue(); (1)
assertThat(link.getVariableNames()).contains("segment", "parameter"); (2)
Map<String, Object> values = new HashMap<>();
values.put("segment", "path");
values.put("parameter", 42);
assertThat(link.expand(values).getHref()) (3)
.isEqualTo("/path/something?parameter=42");
1 | The Link instance indicates that is templated, i.e. it contains a URI template. |
2 | It exposes the parameters contained in the template. |
3 | It allows expansion of the parameters. |
URI templates can be constructed manually and template variables added later on.
UriTemplate template = UriTemplate.of("/{segment}/something")
.with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM);
assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");
2.3. Link relations
To indicate the relationship of the target resource to the current one so-called link relations are used.
Spring HATEOAS provides a LinkRelation
type to easily create String
-based instances of it.
2.3.1. IANA link relations
The Internet Assigned Numbers Authority contains a set of predefined link relations.
They can be referred to via IanaLinkRelations
.
Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);
assertThat(link.getRel()).isEqualTo(LinkRelation.of("next"));
assertThat(IanaLinkRelation.isIanaRel(link.getRel())).isTrue();
2.4. Representation models
To easily create hypermedia enriched representations, Spring HATEOAS provides a set of classes with RepresentationModel
at their root.
It’s basically a container for a collection of Link
s and has convenient methods to add those to the model.
The models can later be rendered into various media type formats that will define how the hypermedia elements look in the representation.
For more information on this, have a look at Media types.
RepresentationModel
class hierarchyclass RepresentationModel class EntityModel class CollectionModel class PagedModel EntityModel -|> RepresentationModel CollectionModel -|> RepresentationModel PagedModel -|> CollectionModel
The default way to work with a RepresentationModel
is to create a subclass of it to contain all the properties the representation is supposed to contain, create instances of that class, populate the properties and enrich it with links.
class PersonModel extends RepresentationModel<PersonModel> {
String firstname, lastname;
}
The generic self-typing is necessary to let RepresentationModel.add(…)
return instances of itself.
The model type can now be used like this:
PersonModel model = new PersonModel();
model.firstname = "Dave";
model.lastname = "Matthews";
model.add(Link.of("https://myhost/people/42"));
If you returned such an instance from a Spring MVC or WebFlux controller and the client sent an Accept
header set to application/hal+json
, the response would look as follows:
{ "_links" : { "self" : { "href" : "https://myhost/people/42" } }, "firstname" : "Dave", "lastname" : "Matthews" }
2.4.1. Item resource representation model
For a resource that’s backed by a singular object or concept, a convenience EntityModel
type exists.
Instead of creating a custom model type for each concept, you can just reuse an already existing type and wrap instances of it into the EntityModel
.
EntityModel
to wrap existing objectsPerson person = new Person("Dave", "Matthews");
EntityModel<Person> model = EntityModel.of(person);
2.4.2. Collection resource representation model
For resources that are conceptually collections, a CollectionModel
is available.
Its elements can either be simple objects or RepresentationModel
instances in turn.
CollectionModel
to wrap a collection of existing objectsCollection<Person> people = Collections.singleton(new Person("Dave", "Matthews"));
CollectionModel<Person> model = CollectionModel.of(people);
While an EntityModel
is constrained to always contain a payload and thus allows to reason about the type arrangement on the sole instance, a CollectionModel
's underlying collection can be empty.
Due to Java’s type erasure, we cannot actually detect that a CollectionModel<Person> model = CollectionModel.empty()
is actually a CollectionModel<Person>
because all we see is the runtime instance and an empty collection.
That missing type information can be added to the model by either adding it to the empty instance on construction via CollectionModel.empty(Person.class)
or as fallback in case the underlying collection might be empty:
Iterable<Person> people = repository.findAll();
var model = CollectionModel.of(people).withFallbackType(Person.class);