#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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
* @Controller
|
||||
* @ExposesResourceFor(Order.class)
|
||||
* @RequestMapping("/orders")
|
||||
* class OrderController {
|
||||
*
|
||||
* @RequestMapping
|
||||
*
|
||||
* @GetMapping
|
||||
* ResponseEntity orders(…) { … }
|
||||
*
|
||||
* @RequestMapping("/{id}")
|
||||
* ResponseEntity order(@PathVariable("id") … ) { … }
|
||||
*
|
||||
* @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)
|
||||
*/
|
||||
|
||||
@@ -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())) //
|
||||
|
||||
Reference in New Issue
Block a user