Skip to content

Commit bf5b8c0

Browse files
committed
#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.
1 parent 3ad9904 commit bf5b8c0

File tree

3 files changed

+151
-39
lines changed

3 files changed

+151
-39
lines changed

src/main/asciidoc/server.adoc

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,13 @@ curl -v localhost:8080/employees \
199199
====
200200

201201
[[server.entity-links]]
202-
== [[fundamentals.obtaining-links.entity-links]] Using the `EntityLinks` interface
202+
== [[fundamentals.obtaining-links.entity-links]] Using the EntityLinks interface
203203

204-
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.
204+
So far, we have created links by pointing to the web-framework implementations (that is, the Spring MVC controllers) and inspected the mapping.
205+
In many cases, these classes essentially read and write representations backed by a model class.
205206

206-
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`).
207+
The `EntityLinks` interface now exposes an API to look up a `Link` or `LinkBuilder` based on the model types.
208+
The methods essentially return links that point either to the collection resource (such as `/people`) or to an item resource (such as `/people/1`).
207209
The following example shows how to use `EntityLinks`:
208210

209211
====
@@ -215,25 +217,46 @@ Link link = links.linkToItemResource(Customer.class, 1L);
215217
----
216218
====
217219

218-
`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:
220+
`EntityLinks` is available via dependency injection by activating either `@EnableHypermediaSupprt` or `@EnableEntityLinks` in your Spring MVC configuration.
221+
This will cause a variety of default implementations of `EntityLinks` being registered.
222+
The most fundamental one is `ControllerEntityLinks` that inspects SpringMVC and Spring WebFlux controller classes.
223+
If you want to register your own implementation of `EntityLinks`, check out <<server.entity-links.spi, this section>>.
224+
225+
[[server.entity-links.controller]]
226+
=== EntityLinks based on Spring MVC and WebFlux controllers
227+
228+
Activating entity links functionality causes all the Spring MVC and WebFlux controllers available in the current `ApplicationContext` to be inspected for the `@ExposesResourceFor(…)` annotation.
229+
The annotation exposes which model type the controller manages.
230+
Beyond that, we assume that you adhere to following the URI mapping setup and conventions:
231+
232+
* A type level `@ExposesResourceFor(…)` declaring which entity type the controller exposes collection and item resources for.
233+
* A class level base mapping that represents the collection resource.
234+
* An additional method level mapping that extends the mapping to append an identifier as additional path segment.
235+
236+
The following example shows an implementation of an `EntityLinks`-capable controller:
219237

220238
====
221239
[source, java]
222240
----
223241
@Controller
224-
@ExposesResourceFor(Order.class)
242+
@ExposesResourceFor(Order.class) <1>
243+
@RequestMapping("/orders") <2>
225244
class OrderController {
226245
227-
@GetMapping("/orders")
246+
@GetMapping <3>
228247
ResponseEntity orders(…) { … }
229248
230-
@GetMapping("/{id}")
249+
@GetMapping("{id}") <4>
231250
ResponseEntity order(@PathVariable("id") … ) { … }
232251
}
233252
----
253+
<1> The controller indicates it's exposing collection and item resources for the entity `Order`.
254+
<2> Its collection resource is exposed under `/orders`
255+
<3> That collection resource can handle `GET` requests. Add more methods for other HTTP methods at your convenience.
256+
<4> An additional controller method to handle a subordinate resource taking a path variable to expose an item resource, i.e. a single `Order`.
234257
====
235258

236-
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:
259+
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:
237260

238261
====
239262
[source, java]
@@ -243,22 +266,112 @@ class PaymentController {
243266
244267
private final EntityLinks entityLinks;
245268
246-
PaymentController(EntityLinks entityLinks) {
269+
PaymentController(EntityLinks entityLinks) { <1>
247270
this.entityLinks = entityLinks;
248271
}
249272
250273
@PutMapping(…)
251274
ResponseEntity payment(@PathVariable Long orderId) {
252275
253-
Link link = entityLinks.linkToItemResource(Order.class, orderId);
276+
Link link = entityLinks.linkToItemResource(Order.class, orderId); <2>
254277
255278
}
256279
}
257280
----
281+
<1> Inject `EntityLinks` made available by `@EnableEntityLinks` or `@EnableHypermediaSupport` in you configuration.
282+
<2> Use the APIs to build links by using the entity types instead of controller classes.
258283
====
259284

260-
You can then refer to the `Order` instances without referring to the `OrderController`.
285+
As you can see, you can refer to resources managing `Order` instances without referring to `OrderController` explicitly.
286+
287+
[[server.entity-links.api]]
288+
=== EntityLinks API in detail
289+
290+
Fundamentally, `EntityLinks` allows to build ``LinkBuilder``s and `Link` instances to collection and item resources of a entity type.
291+
Methods starting with `linkFor…` will produce `LinkBuilder` instances for you to extend and augment with additional path segments, parameters, etc.
292+
Methods starting with `linkTo` produce fully prepared `Link` instances.
293+
294+
While for collection resources providing an entity type is sufficient, links to item resources will need an identifier provided.
295+
This usually looks like this
296+
297+
.Obtaining a link to an item resource
298+
====
299+
[source, java]
300+
----
301+
entityLinks.linkToItemResource(order, order.getId());
302+
----
303+
====
304+
305+
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:
306+
307+
====
308+
[source, java]
309+
----
310+
Function<Order, Object> idExtractor = Order::getId; <1>
311+
312+
entityLinks.linkToItemResource(order, idExtractor); <2>
313+
----
314+
<1> The identifier extraction is externalized so that it can be held in a field or constant.
315+
<2> The link lookup using the extractor.
316+
====
317+
318+
[[server.entity-links.api.typed]]
319+
==== TypedEntityLinks
320+
321+
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.
322+
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.
323+
324+
.Using TypedEntityLinks
325+
====
326+
[source, java]
327+
----
328+
class OrderController {
329+
330+
private final TypedEntityLinks<Order> links;
331+
332+
OrderController(EntityLinks entityLinks) { <1>
333+
this.links = entityLinks.forType(Order::getId); <2>
334+
}
335+
336+
@GetMapping
337+
ResponseEntity<Order> someMethod(…) {
338+
339+
Order order = … // lookup order
340+
341+
Link link = links.linkToItemResource(order); <3>
342+
}
343+
}
344+
----
345+
<1> Inject an `EntityLinks` instance.
346+
<2> Indicate you're going to look up `Order` instances with a certain identifier extractor function.
347+
<3> Lookup item resource links based on a sole `Order` instance.
348+
====
349+
350+
[[server.entity-links.spi]]
351+
=== EntityLinks as SPI
352+
353+
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`.
354+
It's registered as primary bean so that it's always the sole injection candidate when you inject `EntityLinks` in general.
355+
`ControllerEntityLinks` is the default implementation that will be included in the setup, but users are free to implement and register their own implementations.
356+
Making those available to the `EntityLinks` instance available for injection is a matter of registering your implementation as Spring bean.
357+
358+
.Declaring a custom EntityLinks implementation
359+
====
360+
[source, java]
361+
----
362+
class CustomEntityLinksConfiguration {
363+
364+
@Bean
365+
MyEntityLinks myEntityLinks(…) {
366+
return new MyEntityLinks(…);
367+
}
368+
}
369+
----
370+
====
261371

372+
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.
373+
At the same time, it even exposes additional lookup methods for other types of resources.
374+
If you want to make use of these, simply inject `RepositoryEntityLinks` explicitly.
262375

263376
[[server.representation-model-assembler]]
264377
== [[fundamentals.resource-assembler]] Representation model assembler

src/main/java/org/springframework/hateoas/server/core/ControllerEntityLinks.java

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,32 @@
2525
import org.springframework.hateoas.server.LinkBuilder;
2626
import org.springframework.hateoas.server.LinkBuilderFactory;
2727
import org.springframework.util.Assert;
28+
import org.springframework.web.bind.annotation.RequestMapping;
2829

2930
/**
3031
* {@link EntityLinks} implementation which assumes a certain URI mapping structure:
3132
* <ol>
32-
* <li>A class-level mapping annotation that can contain template variables. The URI needs to expose the collection
33-
* resource, which means the controller has to expose a handler method mapped to an empty path: e.g.
34-
* {@code @RequestMapping(method = RequestMethod.GET)} in case of a Spring MVC controller.</li>
35-
* <li>Individual resources are exposed via a nested mapping consisting of the id of the managed entity, e.g. {@code
36-
* @RequestMapping("/{id}")}.<li>
33+
* <li>A class-level {@link ExposesResourceFor} annotation to declare that the annotated controller exposes collection
34+
* and item resources for.</li>
35+
* <li>An {@link RequestMapping} annotation to form the base URI of the collection resource.</li>
36+
* <li>A controller method with a mapping annotation to actually handle at least one HTTP method.</li>
37+
* <li>A controller method that maps a subordinate resource taking a path variable to identify an item resource.</li>
3738
* </ol>
39+
*
3840
* <pre>
39-
* @Controller
40-
* @ExposesResourceFor(Order.class)
41-
* @RequestMapping("/orders")
41+
* &#64;Controller
42+
* &#64;ExposesResourceFor(Order.class)
43+
* &#64;RequestMapping("/orders")
4244
* class OrderController {
43-
*
44-
* @RequestMapping
45+
*
46+
* &#64;GetMapping
4547
* ResponseEntity orders(…) { … }
46-
*
47-
* @RequestMapping("/{id}")
48-
* ResponseEntity order(@PathVariable("id") … ) { … }
48+
*
49+
* &#64;GetMapping("/{id}")
50+
* ResponseEntity order(@PathVariable("id") … ) { … }
4951
* }
5052
* </pre>
51-
*
53+
*
5254
* @author Oliver Gierke
5355
*/
5456
public class ControllerEntityLinks extends AbstractEntityLinks {
@@ -58,9 +60,9 @@ public class ControllerEntityLinks extends AbstractEntityLinks {
5860

5961
/**
6062
* Creates a new {@link ControllerEntityLinks} inspecting the configured classes for the given annotation.
61-
*
62-
* @param controllerTypes the controller classes to be inspected.
63-
* @param linkBuilderFactory the {@link LinkBuilder} to use to create links.
63+
*
64+
* @param controllerTypes the controller classes to be inspected, must not be {@literal null}.
65+
* @param linkBuilderFactory the {@link LinkBuilder} to use to create links, must not be {@literal null}.
6466
*/
6567
public ControllerEntityLinks(Iterable<? extends Class<?>> controllerTypes,
6668
LinkBuilderFactory<? extends LinkBuilder> linkBuilderFactory) {
@@ -82,12 +84,12 @@ private void registerControllerClass(Class<?> controllerType) {
8284
if (annotation != null) {
8385
entityToController.put(annotation.value(), controllerType);
8486
} else {
85-
throw new IllegalArgumentException(String.format("Controller %s must be annotated with @ExposesResourceFor!",
86-
controllerType.getName()));
87+
throw new IllegalArgumentException(
88+
String.format("Controller %s must be annotated with @ExposesResourceFor!", controllerType.getName()));
8789
}
8890
}
8991

90-
/*
92+
/*
9193
* (non-Javadoc)
9294
* @see org.springframework.hateoas.EntityLinks#linkTo(java.lang.Class)
9395
*/
@@ -96,7 +98,7 @@ public LinkBuilder linkFor(Class<?> entity) {
9698
return linkFor(entity, new Object[0]);
9799
}
98100

99-
/*
101+
/*
100102
* (non-Javadoc)
101103
* @see org.springframework.hateoas.EntityLinks#linkTo(java.lang.Class, java.lang.Object)
102104
*/
@@ -116,7 +118,7 @@ public LinkBuilder linkFor(Class<?> entity, Object... parameters) {
116118
return linkBuilderFactory.linkTo(controllerType, parameters);
117119
}
118120

119-
/*
121+
/*
120122
* (non-Javadoc)
121123
* @see org.springframework.hateoas.EntityLinks#getLinkToCollectionResource(java.lang.Class)
122124
*/
@@ -125,7 +127,7 @@ public Link linkToCollectionResource(Class<?> entity) {
125127
return linkFor(entity).withSelfRel();
126128
}
127129

128-
/*
130+
/*
129131
* (non-Javadoc)
130132
* @see org.springframework.hateoas.EntityLinks#getLinkToSingleResource(java.lang.Class, java.lang.Object)
131133
*/
@@ -134,7 +136,7 @@ public Link linkToItemResource(Class<?> entity, Object id) {
134136
return linkFor(entity).slash(id).withSelfRel();
135137
}
136138

137-
/*
139+
/*
138140
* (non-Javadoc)
139141
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
140142
*/

src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,7 @@ public void registersControllerForEntity() {
8181
assertThat(links.linkFor(Person.class)).isNotNull();
8282
}
8383

84-
/**
85-
* @see #43
86-
*/
87-
@Test
84+
@Test // #43
8885
public void returnsLinkBuilderForParameterizedController() {
8986

9087
when(linkBuilderFactory.linkTo(eq(ControllerWithParameters.class), (Object[]) any())) //

0 commit comments

Comments
 (0)