#891 - Overhauled reference documentation on EntityLinks.

Added new paragraphs on EntityLinks, ControllerEntityLinks, TypedEntityLinks and using EntityLinks as SPI.

Polished Javadoc of ControllerEntityLinks and its unit tests.
This commit is contained in:
Oliver Drotbohm
2019-03-22 16:49:05 +01:00
parent 3ad9904705
commit bf5b8c0e76
3 changed files with 152 additions and 40 deletions

View File

@@ -199,11 +199,13 @@ curl -v localhost:8080/employees \
====
[[server.entity-links]]
== [[fundamentals.obtaining-links.entity-links]] Using the `EntityLinks` interface
== [[fundamentals.obtaining-links.entity-links]] Using the EntityLinks interface
So far, we have created links by pointing to the web-framework implementations (that is, the Spring MVC controllers) and inspected the mapping. In many cases, these classes essentially read and write representations backed by a model class.
So far, we have created links by pointing to the web-framework implementations (that is, the Spring MVC controllers) and inspected the mapping.
In many cases, these classes essentially read and write representations backed by a model class.
The `EntityLinks` interface now exposes an API to look up a `Link` or `LinkBuilder` based on the model types. The methods essentially return links that point either to the collection resource (such as `/people`) or to a single resource (such as `/people/1`).
The `EntityLinks` interface now exposes an API to look up a `Link` or `LinkBuilder` based on the model types.
The methods essentially return links that point either to the collection resource (such as `/people`) or to an item resource (such as `/people/1`).
The following example shows how to use `EntityLinks`:
====
@@ -215,25 +217,46 @@ Link link = links.linkToItemResource(Customer.class, 1L);
----
====
`EntityLinks` is available for dependency injection by activating either `@EnableHypermediaSupprt` or `@EnableEntityLinks` in your Spring MVC configuration. Activating this functionality causes all the Spring MVC controllers available in the current `ApplicationContext` to be inspected for the `@ExposesResourceFor(…)` annotation. The annotation exposes which model type the controller manages. Beyond that, we assume that you follow the URI mapping convention of a class level base mapping and assume that you have controller methods handling an appended `/{id}`. The following example shows an implementation of an `EntityLinks`-capable controller:
`EntityLinks` is available via dependency injection by activating either `@EnableHypermediaSupprt` or `@EnableEntityLinks` in your Spring MVC configuration.
This will cause a variety of default implementations of `EntityLinks` being registered.
The most fundamental one is `ControllerEntityLinks` that inspects SpringMVC and Spring WebFlux controller classes.
If you want to register your own implementation of `EntityLinks`, check out <<server.entity-links.spi, this section>>.
[[server.entity-links.controller]]
=== EntityLinks based on Spring MVC and WebFlux controllers
Activating entity links functionality causes all the Spring MVC and WebFlux controllers available in the current `ApplicationContext` to be inspected for the `@ExposesResourceFor(…)` annotation.
The annotation exposes which model type the controller manages.
Beyond that, we assume that you adhere to following the URI mapping setup and conventions:
* A type level `@ExposesResourceFor(…)` declaring which entity type the controller exposes collection and item resources for.
* A class level base mapping that represents the collection resource.
* An additional method level mapping that extends the mapping to append an identifier as additional path segment.
The following example shows an implementation of an `EntityLinks`-capable controller:
====
[source, java]
----
@Controller
@ExposesResourceFor(Order.class)
@ExposesResourceFor(Order.class) <1>
@RequestMapping("/orders") <2>
class OrderController {
@GetMapping("/orders")
@GetMapping <3>
ResponseEntity orders(…) { … }
@GetMapping("/{id}")
@GetMapping("{id}") <4>
ResponseEntity order(@PathVariable("id") … ) { … }
}
----
<1> The controller indicates it's exposing collection and item resources for the entity `Order`.
<2> Its collection resource is exposed under `/orders`
<3> That collection resource can handle `GET` requests. Add more methods for other HTTP methods at your convenience.
<4> An additional controller method to handle a subordinate resource taking a path variable to expose an item resource, i.e. a single `Order`.
====
The controller exposes that it manages `Order` instances and exposes handler methods that are mapped to our convention. When youy enable `EntityLinks` through `@EnableEntityLinks` in your Spring MVC configuration, you can create links to the controller, as follows:
With this in place, when youy enable `EntityLinks` through `@EnableEntityLinks` or `@EnableHypermediaSupport` in your Spring MVC configuration, you can create links to the controller, as follows:
====
[source, java]
@@ -243,22 +266,112 @@ class PaymentController {
private final EntityLinks entityLinks;
PaymentController(EntityLinks entityLinks) {
PaymentController(EntityLinks entityLinks) { <1>
this.entityLinks = entityLinks;
}
@PutMapping(…)
ResponseEntity payment(@PathVariable Long orderId) {
Link link = entityLinks.linkToItemResource(Order.class, orderId);
Link link = entityLinks.linkToItemResource(Order.class, orderId); <2>
}
}
----
<1> Inject `EntityLinks` made available by `@EnableEntityLinks` or `@EnableHypermediaSupport` in you configuration.
<2> Use the APIs to build links by using the entity types instead of controller classes.
====
As you can see, you can refer to resources managing `Order` instances without referring to `OrderController` explicitly.
[[server.entity-links.api]]
=== EntityLinks API in detail
Fundamentally, `EntityLinks` allows to build ``LinkBuilder``s and `Link` instances to collection and item resources of a entity type.
Methods starting with `linkFor…` will produce `LinkBuilder` instances for you to extend and augment with additional path segments, parameters, etc.
Methods starting with `linkTo` produce fully prepared `Link` instances.
While for collection resources providing an entity type is sufficient, links to item resources will need an identifier provided.
This usually looks like this
.Obtaining a link to an item resource
====
[source, java]
----
entityLinks.linkToItemResource(order, order.getId());
----
====
If you find yourself repeating those method calls the identifier extraction step can be pulled out into a reusable `Function` to be reused throughout different invocations:
====
[source, java]
----
Function<Order, Object> idExtractor = Order::getId; <1>
entityLinks.linkToItemResource(order, idExtractor); <2>
----
<1> The identifier extraction is externalized so that it can be held in a field or constant.
<2> The link lookup using the extractor.
====
[[server.entity-links.api.typed]]
==== TypedEntityLinks
As controller implementations are often grouped around entity types, you'll very often find yourself using the same extractor function (see <<server.entity-links.api>> for details) all over the controller class.
We can centralize the identifier extraction logic even more by obtaining a `TypedEntityLinks` instance poviding the extractor once, so that the actually lookups don't have to deal with the extraction anymore at all.
.Using TypedEntityLinks
====
[source, java]
----
class OrderController {
private final TypedEntityLinks<Order> links;
OrderController(EntityLinks entityLinks) { <1>
this.links = entityLinks.forType(Order::getId); <2>
}
@GetMapping
ResponseEntity<Order> someMethod(…) {
Order order = … // lookup order
Link link = links.linkToItemResource(order); <3>
}
}
----
<1> Inject an `EntityLinks` instance.
<2> Indicate you're going to look up `Order` instances with a certain identifier extractor function.
<3> Lookup item resource links based on a sole `Order` instance.
====
[[server.entity-links.spi]]
=== EntityLinks as SPI
The `EntityLinks` instance created by `@EnableEntityLinks` / `@EnableHypermediaSupport` is of type `DelegatingEntityLinks` which will in turn pick up all other `EntityLinks` implementations available as beans in the `ApplicationContext`.
It's registered as primary bean so that it's always the sole injection candidate when you inject `EntityLinks` in general.
`ControllerEntityLinks` is the default implementation that will be included in the setup, but users are free to implement and register their own implementations.
Making those available to the `EntityLinks` instance available for injection is a matter of registering your implementation as Spring bean.
.Declaring a custom EntityLinks implementation
====
[source, java]
----
class CustomEntityLinksConfiguration {
@Bean
MyEntityLinks myEntityLinks(…) {
return new MyEntityLinks(…);
}
}
----
====
You can then refer to the `Order` instances without referring to the `OrderController`.
An example for the extensibility of this mechanism is Spring Data REST's https://github.com/spring-projects/spring-data-rest/blob/3a0cba94a2cc8739375ecf24086da2f7c3bbf038/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java[`RepositoryEntityLinks`], which uses the repository mapping information to create links pointing to resources backed by Spring Data repositories.
At the same time, it even exposes additional lookup methods for other types of resources.
If you want to make use of these, simply inject `RepositoryEntityLinks` explicitly.
[[server.representation-model-assembler]]
== [[fundamentals.resource-assembler]] Representation model assembler

View File

@@ -25,30 +25,32 @@ import org.springframework.hateoas.server.ExposesResourceFor;
import org.springframework.hateoas.server.LinkBuilder;
import org.springframework.hateoas.server.LinkBuilderFactory;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* {@link EntityLinks} implementation which assumes a certain URI mapping structure:
* <ol>
* <li>A class-level mapping annotation that can contain template variables. The URI needs to expose the collection
* resource, which means the controller has to expose a handler method mapped to an empty path: e.g.
* {@code @RequestMapping(method = RequestMethod.GET)} in case of a Spring MVC controller.</li>
* <li>Individual resources are exposed via a nested mapping consisting of the id of the managed entity, e.g. {@code
* @RequestMapping("/{id}")}.<li>
* <li>A class-level {@link ExposesResourceFor} annotation to declare that the annotated controller exposes collection
* and item resources for.</li>
* <li>An {@link RequestMapping} annotation to form the base URI of the collection resource.</li>
* <li>A controller method with a mapping annotation to actually handle at least one HTTP method.</li>
* <li>A controller method that maps a subordinate resource taking a path variable to identify an item resource.</li>
* </ol>
*
* <pre>
* @Controller
* @ExposesResourceFor(Order.class)
* @RequestMapping("/orders")
* &#64;Controller
* &#64;ExposesResourceFor(Order.class)
* &#64;RequestMapping("/orders")
* class OrderController {
*
* @RequestMapping
*
* &#64;GetMapping
* ResponseEntity orders(…) { … }
*
* @RequestMapping("/{id}")
* ResponseEntity order(@PathVariable("id") … ) { … }
*
* &#64;GetMapping("/{id}")
* ResponseEntity order(@PathVariable("id") … ) { … }
* }
* </pre>
*
*
* @author Oliver Gierke
*/
public class ControllerEntityLinks extends AbstractEntityLinks {
@@ -58,9 +60,9 @@ public class ControllerEntityLinks extends AbstractEntityLinks {
/**
* Creates a new {@link ControllerEntityLinks} inspecting the configured classes for the given annotation.
*
* @param controllerTypes the controller classes to be inspected.
* @param linkBuilderFactory the {@link LinkBuilder} to use to create links.
*
* @param controllerTypes the controller classes to be inspected, must not be {@literal null}.
* @param linkBuilderFactory the {@link LinkBuilder} to use to create links, must not be {@literal null}.
*/
public ControllerEntityLinks(Iterable<? extends Class<?>> controllerTypes,
LinkBuilderFactory<? extends LinkBuilder> linkBuilderFactory) {
@@ -82,12 +84,12 @@ public class ControllerEntityLinks extends AbstractEntityLinks {
if (annotation != null) {
entityToController.put(annotation.value(), controllerType);
} else {
throw new IllegalArgumentException(String.format("Controller %s must be annotated with @ExposesResourceFor!",
controllerType.getName()));
throw new IllegalArgumentException(
String.format("Controller %s must be annotated with @ExposesResourceFor!", controllerType.getName()));
}
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.EntityLinks#linkTo(java.lang.Class)
*/
@@ -96,7 +98,7 @@ public class ControllerEntityLinks extends AbstractEntityLinks {
return linkFor(entity, new Object[0]);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.EntityLinks#linkTo(java.lang.Class, java.lang.Object)
*/
@@ -116,7 +118,7 @@ public class ControllerEntityLinks extends AbstractEntityLinks {
return linkBuilderFactory.linkTo(controllerType, parameters);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.EntityLinks#getLinkToCollectionResource(java.lang.Class)
*/
@@ -125,7 +127,7 @@ public class ControllerEntityLinks extends AbstractEntityLinks {
return linkFor(entity).withSelfRel();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.EntityLinks#getLinkToSingleResource(java.lang.Class, java.lang.Object)
*/
@@ -134,7 +136,7 @@ public class ControllerEntityLinks extends AbstractEntityLinks {
return linkFor(entity).slash(id).withSelfRel();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/

View File

@@ -81,10 +81,7 @@ public class ControllerEntityLinksUnitTest extends TestUtils {
assertThat(links.linkFor(Person.class)).isNotNull();
}
/**
* @see #43
*/
@Test
@Test // #43
public void returnsLinkBuilderForParameterizedController() {
when(linkBuilderFactory.linkTo(eq(ControllerWithParameters.class), (Object[]) any())) //