Skip to content

Commit 9f10970

Browse files
committed
#859 - Fix RepresentationModelProcessor to smoothly register with Spring MVC.
Some more polishing on internal of Processor and Invoker machinery regarding resource -> model, etc.
1 parent cae49e7 commit 9f10970

17 files changed

+593
-151
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas;
17+
18+
import org.springframework.context.annotation.Bean;
19+
import org.springframework.context.annotation.Configuration;
20+
21+
/**
22+
* @author Greg Turnquist
23+
*/
24+
// tag::code[]
25+
@Configuration
26+
public class PaymentProcessingApp {
27+
28+
@Bean
29+
PaymentProcessor paymentProcessor() {
30+
return new PaymentProcessor();
31+
}
32+
}
33+
// end::code[]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas;
17+
18+
import org.springframework.hateoas.server.RepresentationModelProcessor;
19+
import org.springframework.hateoas.support.Order;
20+
21+
/**
22+
* @author Greg Turnquist
23+
*/
24+
// tag::code[]
25+
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> { // <1>
26+
27+
@Override
28+
public EntityModel<Order> process(EntityModel<Order> model) {
29+
30+
model.add( // <2>
31+
new Link("/payments/{orderId}").withRel(LinkRelation.of("payments")) //
32+
.expand(model.getContent().getOrderId()));
33+
34+
return model; // <3>
35+
}
36+
}
37+
// end::code[]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.support;
17+
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.Value;
20+
21+
/**
22+
* @author Greg Turnquist
23+
*/
24+
@Value
25+
@RequiredArgsConstructor
26+
public class Customer {
27+
private final long customerId;
28+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.support;
17+
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.Value;
20+
21+
/**
22+
* @author Greg Turnquist
23+
*/
24+
@Value
25+
@RequiredArgsConstructor
26+
public class Order {
27+
private final long orderId;
28+
private final String state;
29+
}
30+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.support;
17+
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.Value;
20+
21+
/**
22+
* @author Greg Turnquist
23+
*/
24+
@Value
25+
@RequiredArgsConstructor
26+
public class Payment {
27+
private final Order order;
28+
private final double amount;
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.support;
17+
18+
import org.springframework.web.bind.annotation.RestController;
19+
20+
/**
21+
* @author Greg Turnquist
22+
*/
23+
@RestController
24+
public class PaymentController {
25+
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"orderId" : "42",
3+
"state" : "AWAITING_PAYMENT",
4+
"_links" : {
5+
"self" : {
6+
"href" : "http://localhost/orders/999"
7+
}
8+
}
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"orderId" : "42",
3+
"state" : "AWAITING_PAYMENT",
4+
"_links" : {
5+
"self" : {
6+
"href" : "http://localhost/orders/999"
7+
},
8+
"payments" : { // <1>
9+
"href" : "/payments/42" // <2>
10+
}
11+
}
12+
}

src/main/asciidoc/server.adoc

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[[server]]
22
= Server-side support
3+
:code-dir: ../../../src/docs/java/org/springframework/hateoas
4+
:resource-dir: ../../../src/docs/resources/org/springframework/hateoas
35

46
[[server.link-builder]]
57
== [[fundamentals.obtaining-links]] [[fundamentals.obtaining-links.builder]] Building links
@@ -223,6 +225,66 @@ CollectionModel<PersonModel> model = assembler.toCollectionModel(people);
223225
----
224226
====
225227

228+
[[server.processors]]
229+
== Representation Model Processors
230+
231+
Sometimes you need to tweak and adjust hypermedia representations after they have been <<server.representation-model-assembler,assembled>>.
232+
233+
A perfect example is when you have a controller that deals with order fulfillment, but you need to add links related to making payments.
234+
235+
Imagine having your ordering system producing this type of hypermedia:
236+
237+
====
238+
[source, json, tabsize=2]
239+
----
240+
include::{resource-dir}/docs/order-plain.json[]
241+
----
242+
====
243+
244+
You wish to add a link so the client can make payment, but don't want to mix details about your `PaymentController` into
245+
the `OrderController`.
246+
247+
Instead of polluting the details of your ordering system, you can write a `RepresentationModelProcessor` like this:
248+
249+
====
250+
[source, java, tabsize=2]
251+
----
252+
include::{code-dir}/PaymentProcessor.java[tag=code]
253+
----
254+
<1> This processor will only be applied to `EntityModel<Order>` objects.
255+
<2> Manipulate the existing `EntityModel` object by adding an unconditional link.
256+
<3> Return the `EntityModel` so it can be serialized into the requested media type.
257+
====
258+
259+
Register the processor with your application:
260+
261+
====
262+
[source, java, tabsize=2]
263+
----
264+
include::{code-dir}/PaymentProcessingApp.java[tag=code]
265+
----
266+
====
267+
268+
Now when you issue a hypermedia respresentation of an `Order`, the client receives this:
269+
270+
====
271+
[source, java, tabsize=2]
272+
----
273+
include::{resource-dir}/docs/order-with-payment-link.json[]
274+
----
275+
<1> You see the `LinkRelation.of("payments")` plugged in as this link's relation.
276+
<2> The URI was provided by the processor.
277+
====
278+
279+
This example is quite simple, but you can easily:
280+
281+
* Use `WebMvcLinkBuilder` or `WebFluxLinkBuilder` to construct a dynamic link to your `PaymentController`.
282+
* Inject any services needed to conditionally add other links (e.g. `cancel`, `amend`) that are driven by state.
283+
* Leverage cross cutting services like Spring Security to add, remove, or revise links based upon the current user's context.
284+
285+
Also, in this example, the `PaymentProcessor` alters the provided `EntityModel<Order>`. You also have the power to
286+
_replace_ it with another object. Just be advised the API requires the return type to equal the input type.
287+
226288
[[server.rel-provider]]
227289
== [[spis.rel-provider]] Using the `RelProvider` API
228290

src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import lombok.RequiredArgsConstructor;
1919

20+
import java.util.ArrayList;
2021
import java.util.Collection;
22+
import java.util.List;
2123

2224
import org.springframework.beans.BeansException;
2325
import org.springframework.beans.factory.ObjectProvider;
@@ -27,14 +29,20 @@
2729
import org.springframework.context.annotation.Lazy;
2830
import org.springframework.core.codec.CharSequenceEncoder;
2931
import org.springframework.core.codec.StringDecoder;
32+
import org.springframework.hateoas.server.RepresentationModelProcessor;
33+
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorHandlerMethodReturnValueHandler;
34+
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker;
3035
import org.springframework.hateoas.server.reactive.HypermediaWebFilter;
3136
import org.springframework.http.codec.CodecConfigurer;
3237
import org.springframework.http.codec.ServerCodecConfigurer;
3338
import org.springframework.http.codec.json.Jackson2JsonDecoder;
3439
import org.springframework.http.codec.json.Jackson2JsonEncoder;
3540
import org.springframework.util.MimeType;
41+
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
42+
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
3643
import org.springframework.web.reactive.config.WebFluxConfigurer;
3744
import org.springframework.web.reactive.function.client.WebClient;
45+
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
3846

3947
import com.fasterxml.jackson.databind.ObjectMapper;
4048

src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import lombok.RequiredArgsConstructor;
1919

2020
import java.util.Collection;
21+
import java.util.Collections;
2122
import java.util.List;
2223
import java.util.stream.Collectors;
2324

@@ -27,12 +28,17 @@
2728
import org.springframework.context.annotation.Bean;
2829
import org.springframework.context.annotation.Configuration;
2930
import org.springframework.hateoas.RepresentationModel;
31+
import org.springframework.hateoas.server.RepresentationModelProcessor;
32+
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorHandlerMethodReturnValueHandler;
33+
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker;
3034
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
3135
import org.springframework.hateoas.server.mvc.UriComponentsContributor;
3236
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory;
3337
import org.springframework.http.converter.HttpMessageConverter;
3438
import org.springframework.web.client.RestTemplate;
39+
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
3540
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
41+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
3642

3743
import com.fasterxml.jackson.databind.ObjectMapper;
3844

@@ -51,6 +57,13 @@ HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(ObjectProvider<ObjectMappe
5157
return new HypermediaWebMvcConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
5258
}
5359

60+
@Bean
61+
HypermediaRepresentationModelBeanProcessorPostProcessor hypermediaRepresentionModelProcessorConfigurator(
62+
List<RepresentationModelProcessor<?>> processors) {
63+
64+
return new HypermediaRepresentationModelBeanProcessorPostProcessor(processors);
65+
}
66+
5467
@Bean
5568
static HypermediaRestTemplateBeanPostProcessor restTemplateBeanPostProcessor(
5669
ObjectProvider<HypermediaWebMvcConfigurer> configurer) {
@@ -91,6 +104,34 @@ public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
91104
}
92105
}
93106

107+
/**
108+
* @author Greg Turnquist
109+
*/
110+
@RequiredArgsConstructor
111+
static class HypermediaRepresentationModelBeanProcessorPostProcessor implements BeanPostProcessor {
112+
113+
private final List<RepresentationModelProcessor<?>> processors;
114+
115+
@Override
116+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
117+
118+
if (RequestMappingHandlerAdapter.class.isInstance(bean)) {
119+
120+
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
121+
122+
HandlerMethodReturnValueHandlerComposite delegate = new HandlerMethodReturnValueHandlerComposite();
123+
delegate.addHandlers(adapter.getReturnValueHandlers());
124+
125+
RepresentationModelProcessorHandlerMethodReturnValueHandler handler = new RepresentationModelProcessorHandlerMethodReturnValueHandler(
126+
delegate, new RepresentationModelProcessorInvoker(processors));
127+
128+
adapter.setReturnValueHandlers(Collections.singletonList(handler));
129+
}
130+
131+
return bean;
132+
}
133+
}
134+
94135
/**
95136
* {@link BeanPostProcessor} to register hypermedia support with {@link RestTemplate} instances found in the
96137
* application context.

0 commit comments

Comments
 (0)