diff --git a/src/docs/java/org/springframework/hateoas/client/HypermediaConfiguration.java b/src/docs/java/org/springframework/hateoas/client/HypermediaConfiguration.java new file mode 100644 index 00000000..c2184c3e --- /dev/null +++ b/src/docs/java/org/springframework/hateoas/client/HypermediaConfiguration.java @@ -0,0 +1,20 @@ +package org.springframework.hateoas.client; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.config.HypermediaRestTemplateConfigurer; +import org.springframework.web.client.RestTemplate; + +// tag::code[] +@Configuration +public class HypermediaConfiguration { + + /** + * Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}. + */ + @Bean + RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { // <1> + return configurer.registerHypermediaTypes(new RestTemplate()); // <2> + } +} +// end::code[] diff --git a/src/main/asciidoc/client.adoc b/src/main/asciidoc/client.adoc index dcc81366..fc03a847 100644 --- a/src/main/asciidoc/client.adoc +++ b/src/main/asciidoc/client.adoc @@ -146,4 +146,77 @@ Again, you can use similar assertions as the earlier example. There are many other ways to fashion test cases. `WebTestClient` can be bound to controllers, functions, and URLs. This section isn't meant to show all that. Instead, this gives you some examples to get started. The important thing is that by applying `HypermediaWebTestClientConfigurer`, any instance of `WebTestClient` can be altered to handle hypermedia. +[[client.rest-template]] +== Configuring RestTemplate instances +If you want to create your own copy of `RestTemplate`, configured to speak hypermedia, you can use the `HypermediaRestTemplateConfigurer`: + +.Configuring a `RestTemplate` yourself +==== +[source, java] +---- +include::{base-dir}/src/docs/java/org/springframework/hateoas/client/HypermediaConfiguration.java[tag=code] +---- +<1> Inside your `@Configuration` class, get a copy of the `HypermediaRestTemplateConfigurer` bean Spring HATEOAS registers. +<2> After creating a `RestTemplate`, use the configurer to apply hypermedia types. +==== + +You are free to apply this pattern to any instance of `RestTemplate` that you need, whether is to create a registered bean, or inside a service you define. + +If you're using Spring Boot, there is another approach. + +In general, Spring Boot has moved away from the concept of registering a `RestTemplate` bean in the application context. + +* When talking to different services, you often need different credentials. +* When `RestTemplate` uses an underlying connection pool, you run into additional issues. +* Users often need different instances rather than a single bean. + +To compensate for this, Spring Boot provides a `RestTemplateBuilder`. This autoconfigured bean lets you define various beans used to fashion +a `RestTemplate` instance. You ask for a `RestTemplateBuilder` bean, execute its `build()` method, and then apply final settings (like credentials, etc.). + +To register hypermedia-based message converters, add the following to your code: + +.Letting Spring Boot configure things +==== +[source,java] +---- +@Bean // <4> +RestTemplateCustomizer hypermediaRestTemplatCustomizer(HypermediaRestTemplateConfigurer configurer) { // <1> + return restTemplate -> { // <2> + configurer.registerHypermediaTypes(restTemplate); // <3> + }; +} +---- +<1> When creating a Spring bean, request a copy of Spring HATEOAS's `HypermediaRestTemplateConfigurer` bean. +<2> Use a Java 8 lambda expression to define a `RestTemplateCustomizer`. +<3> Inside the function call, apply the `registerHypermediaTypes` method. +<4> Return the whole thing as a Spring bean so Spring Boot can pick it up and apply it to its autoconfigured `RestTemplateBuilder`. +==== + +Assuming you added that `RestTemplateCustomizer` bean definition, this is all you must do to get a `RestTemplate` with hypermedia support: + +.Injecting `RestTemplateBuilder` into your service +==== +[source,java] +---- +@Service +public class SampleService { + + private RestTemplateBuilder restTemplateBuilder; + + public SampleService(RestTemplateBuilder restTemplateBuilder) { // <1> + this.restTemplateBuilder = restTemplateBuilder; + } + + void doSomething() { + RestTemplate template = restTemplateBuilder.build(); // <2> + + // Your template is now configured to speak hypermedia! + } +} +---- +<1> Use *constructor injection* to get a hold of Spring Boot's `RestTemplateBuilder`. +<2> When you need a `RestTemplate`, invoke `build()`. The instance will have tapped your `RestTemplateCustomizer` code and registered support. +==== + +That's all it takes to get `RestTemplate` hypermedia support. diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurer.java b/src/main/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurer.java new file mode 100644 index 00000000..f53a25a2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurer.java @@ -0,0 +1,30 @@ +package org.springframework.hateoas.config; + +import org.springframework.web.client.RestTemplate; + +/** + * Assembles hypermedia-based message converters and applies them to an existing {@link RestTemplate}. + * + * @author Greg Turnquist + * @since 1.1 + */ +public class HypermediaRestTemplateConfigurer { + + private final WebConverters converters; + + HypermediaRestTemplateConfigurer(WebConverters converters) { + this.converters = converters; + } + + /** + * Insert hypermedia-aware message converters in front of any other existing message converters. + * + * @param template + * @return + */ + public RestTemplate registerHypermediaTypes(RestTemplate template) { + + template.setMessageConverters(converters.and(template.getMessageConverters())); + return template; + } +} diff --git a/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java index d24a31bd..3699233c 100644 --- a/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java @@ -16,9 +16,7 @@ package org.springframework.hateoas.config; import lombok.RequiredArgsConstructor; - import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,8 +34,13 @@ class RestTemplateHateoasConfiguration { @Bean static HypermediaRestTemplateBeanPostProcessor hypermediaRestTemplateBeanPostProcessor( - ObjectProvider converters) { - return new HypermediaRestTemplateBeanPostProcessor(converters); + HypermediaRestTemplateConfigurer configurer) { + return new HypermediaRestTemplateBeanPostProcessor(configurer); + } + + @Bean + HypermediaRestTemplateConfigurer hypermediaRestTemplateConfigurer(WebConverters converters) { + return new HypermediaRestTemplateConfigurer(converters); } /** @@ -50,7 +53,7 @@ class RestTemplateHateoasConfiguration { @RequiredArgsConstructor static class HypermediaRestTemplateBeanPostProcessor implements BeanPostProcessor { - private final ObjectProvider converters; + private final HypermediaRestTemplateConfigurer configurer; /* * (non-Javadoc) @@ -64,10 +67,7 @@ class RestTemplateHateoasConfiguration { return bean; } - RestTemplate template = (RestTemplate) bean; - template.setMessageConverters(converters.getObject().and(template.getMessageConverters())); - - return template; + return this.configurer.registerHypermediaTypes((RestTemplate) bean); } } } diff --git a/src/main/java/org/springframework/hateoas/config/WebConverters.java b/src/main/java/org/springframework/hateoas/config/WebConverters.java index 9feb581e..8aa3d28f 100644 --- a/src/main/java/org/springframework/hateoas/config/WebConverters.java +++ b/src/main/java/org/springframework/hateoas/config/WebConverters.java @@ -15,18 +15,17 @@ */ package org.springframework.hateoas.config; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; import org.springframework.util.Assert; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; /** * Value type to handle registration of hypermedia related {@link HttpMessageConverter}s. diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurerTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurerTest.java new file mode 100644 index 00000000..ff521f49 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurerTest.java @@ -0,0 +1,81 @@ +package org.springframework.hateoas.config; + +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*; +import static org.springframework.hateoas.support.ContextTester.*; + +public class HypermediaRestTemplateConfigurerTest { + + private static MediaType FRODO_JSON = MediaType.parseMediaType("application/frodo+json"); + + @Test // #1223 + void webConvertersShouldAddHypermediaMessageConverters() { + + withContext(HalConfig.class, context -> { + + HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class); + + RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate()); + + assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes) + .contains(MediaTypes.HAL_JSON) // + .doesNotContain(MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON); + }); + } + + @Test // #1223 + void webConvertersShouldAddAllHypermediaMessageConverters() { + + withContext(AllHypermediaConfig.class, context -> { + + HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class); + + RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate()); + + assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes) + .contains(MediaTypes.HAL_JSON, MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON); + }); + } + + @Test // #1223 + void webConvertersShouldSupportCustomHypermediaTypes() { + + withContext(CustomHypermediaConfig.class, context -> { + + HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class); + + RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate()); + + assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes) + .contains(MediaTypes.HAL_JSON, FRODO_JSON) + .doesNotContain(MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON); + }); + } + + @EnableHypermediaSupport(type = HAL) + static class HalConfig { + + } + + @EnableHypermediaSupport(type = { HAL, HAL_FORMS, COLLECTION_JSON, UBER }) + static class AllHypermediaConfig { + + } + + static class CustomHypermediaConfig extends HalConfig { + + @Bean + HypermediaMappingInformation hypermediaMappingInformation() { + return () -> Collections.singletonList(FRODO_JSON); + } + } +}