Skip to content

Commit fe78959

Browse files
committed
Add hypermedia autoconfiguration for RestTemplate.
* Provide RestTemplateCustomizer. * Verify RestTemplateCustomizer is also picked up for TestRestTemplate. Related: spring-projects#16020.
1 parent 533a20a commit fe78959

File tree

5 files changed

+236
-5
lines changed

5 files changed

+236
-5
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
package org.springframework.boot.autoconfigure.hateoas;
1818

19-
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import java.util.List;
2020

21+
import org.springframework.beans.factory.ObjectProvider;
2122
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2223
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2324
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -28,23 +29,36 @@
2829
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
2930
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
3031
import org.springframework.boot.context.properties.EnableConfigurationProperties;
32+
import org.springframework.boot.web.client.RestTemplateCustomizer;
33+
import org.springframework.boot.web.codec.CodecCustomizer;
34+
import org.springframework.context.annotation.Bean;
3135
import org.springframework.context.annotation.Configuration;
3236
import org.springframework.context.annotation.Import;
3337
import org.springframework.hateoas.EntityModel;
3438
import org.springframework.hateoas.client.LinkDiscoverers;
3539
import org.springframework.hateoas.config.EnableHypermediaSupport;
3640
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
41+
import org.springframework.hateoas.config.HypermediaMappingInformation;
42+
import org.springframework.http.codec.json.Jackson2JsonDecoder;
43+
import org.springframework.http.codec.json.Jackson2JsonEncoder;
3744
import org.springframework.plugin.core.Plugin;
45+
import org.springframework.util.Assert;
46+
import org.springframework.util.MimeType;
3847
import org.springframework.web.bind.annotation.RequestMapping;
48+
import org.springframework.web.client.RestTemplate;
49+
import org.springframework.web.reactive.function.client.WebClient;
3950
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
4051

52+
import com.fasterxml.jackson.databind.ObjectMapper;
53+
4154
/**
4255
* {@link EnableAutoConfiguration Auto-configuration} for Spring HATEOAS's
4356
* {@link EnableHypermediaSupport @EnableHypermediaSupport}.
4457
*
4558
* @author Roy Clarkson
4659
* @author Oliver Gierke
4760
* @author Andy Wilkinson
61+
* @author Greg Turnquist
4862
* @since 1.1.0
4963
*/
5064
@Configuration(proxyBeanMethods = false)
@@ -64,4 +78,23 @@ protected static class HypermediaConfiguration {
6478

6579
}
6680

81+
@Configuration(proxyBeanMethods = false)
82+
@ConditionalOnClass(RestTemplate.class)
83+
protected static class HypermediaRestTemplateConfiguration {
84+
85+
@Bean
86+
WebConverters springBootWebConverters(ObjectProvider<ObjectMapper> mapper,
87+
List<HypermediaMappingInformation> information) {
88+
return WebConverters.of(mapper.getIfUnique(ObjectMapper::new), information);
89+
}
90+
91+
@Bean
92+
RestTemplateCustomizer hypermediaRestTemplatCustomizer(ObjectProvider<WebConverters> converters) {
93+
return restTemplate -> {
94+
restTemplate.setMessageConverters(converters.getObject().and(restTemplate.getMessageConverters()));
95+
};
96+
}
97+
98+
}
99+
67100
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2020 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+
* https://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.boot.autoconfigure.hateoas;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
import org.springframework.hateoas.config.HypermediaMappingInformation;
24+
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
25+
import org.springframework.http.converter.HttpMessageConverter;
26+
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
27+
import org.springframework.util.Assert;
28+
29+
import com.fasterxml.jackson.databind.ObjectMapper;
30+
31+
/**
32+
* @author Greg Turnquist
33+
*/
34+
class WebConverters {
35+
36+
private final List<HttpMessageConverter<?>> converters;
37+
38+
/**
39+
* Creates a new {@link WebConverters} from the given {@link ObjectMapper} and
40+
* {@link HypermediaMappingInformation}s.
41+
* @param mapper must not be {@literal null}.
42+
* @param mappingInformation must not be {@literal null}.
43+
*/
44+
private WebConverters(ObjectMapper mapper, List<HypermediaMappingInformation> mappingInformation) {
45+
46+
this.converters = mappingInformation.stream() //
47+
.map(it -> createMessageConverter(it, it.configureObjectMapper(mapper.copy()))) //
48+
.collect(Collectors.toList());
49+
}
50+
51+
/**
52+
* Creates a new {@link WebConverters} from the given {@link ObjectMapper} and
53+
* {@link HypermediaMappingInformation}s.
54+
* @param mapper must not be {@literal null}.
55+
* @param mappingInformations must not be {@literal null}.
56+
* @return
57+
*/
58+
public static WebConverters of(ObjectMapper mapper, List<HypermediaMappingInformation> mappingInformations) {
59+
60+
Assert.notNull(mapper, "ObjectMapper must not be null!");
61+
Assert.notNull(mappingInformations, "Mapping information must not be null!");
62+
63+
return new WebConverters(mapper, mappingInformations);
64+
}
65+
66+
/**
67+
* Augments the given {@link List} of {@link HttpMessageConverter}s with the
68+
* hypermedia enabled ones.
69+
* @param converters must not be {@literal null}.
70+
*/
71+
public void augment(List<HttpMessageConverter<?>> converters) {
72+
73+
Assert.notNull(converters, "HttpMessageConverters must not be null!");
74+
75+
this.converters.forEach(it -> converters.add(0, it));
76+
}
77+
78+
/**
79+
* Returns a new {@link List} of {@link HttpMessageConverter}s consisting of both the
80+
* hypermedia based ones as well as the given ones.
81+
* @param converters must not be {@literal null}.
82+
*/
83+
public List<HttpMessageConverter<?>> and(Collection<HttpMessageConverter<?>> converters) {
84+
85+
Assert.notNull(converters, "HttpMessageConverters must not be null!");
86+
87+
List<HttpMessageConverter<?>> result = new ArrayList<>(this.converters);
88+
result.addAll(converters);
89+
90+
return result;
91+
}
92+
93+
/**
94+
* Creates a new {@link TypeConstrainedMappingJackson2HttpMessageConverter} to handle
95+
* {@link org.springframework.hateoas.RepresentationModel} for the given
96+
* {@link HypermediaMappingInformation} using a copy of the given
97+
* {@link ObjectMapper}.
98+
* @param type must not be {@literal null}.
99+
* @param mapper must not be {@literal null}.
100+
* @return
101+
*/
102+
private static AbstractJackson2HttpMessageConverter createMessageConverter(HypermediaMappingInformation type,
103+
ObjectMapper mapper) {
104+
105+
return new TypeConstrainedMappingJackson2HttpMessageConverter(type.getRootType(), type.getMediaTypes(),
106+
type.configureObjectMapper(mapper));
107+
}
108+
109+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
3030
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
3131
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
32+
import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration;
3233
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
3334
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
3435
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration.NotReactiveWebApplicationCondition;
@@ -49,7 +50,7 @@
4950
* @since 1.4.0
5051
*/
5152
@Configuration(proxyBeanMethods = false)
52-
@AutoConfigureAfter(HttpMessageConvertersAutoConfiguration.class)
53+
@AutoConfigureAfter({ HttpMessageConvertersAutoConfiguration.class, HypermediaAutoConfiguration.class })
5354
@ConditionalOnClass(RestTemplate.class)
5455
@Conditional(NotReactiveWebApplicationCondition.class)
5556
public class RestTemplateAutoConfiguration {

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@
1616

1717
package org.springframework.boot.autoconfigure.hateoas;
1818

19+
import static org.assertj.core.api.Assertions.*;
20+
1921
import java.util.Optional;
2022

2123
import org.junit.jupiter.api.Test;
22-
2324
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
2425
import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration.HypermediaConfiguration;
2526
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
2627
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
28+
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
2729
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
2830
import org.springframework.boot.test.context.FilteredClassLoader;
2931
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
32+
import org.springframework.boot.web.client.RestTemplateBuilder;
3033
import org.springframework.context.annotation.Configuration;
3134
import org.springframework.hateoas.MediaTypes;
3235
import org.springframework.hateoas.client.LinkDiscoverer;
@@ -38,17 +41,17 @@
3841
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
3942
import org.springframework.http.MediaType;
4043
import org.springframework.http.converter.HttpMessageConverter;
44+
import org.springframework.web.client.RestTemplate;
4145
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
4246

43-
import static org.assertj.core.api.Assertions.assertThat;
44-
4547
/**
4648
* Tests for {@link HypermediaAutoConfiguration}.
4749
*
4850
* @author Roy Clarkson
4951
* @author Oliver Gierke
5052
* @author Andy Wilkinson
5153
* @author Madhura Bhave
54+
* @author Greg Turnquist
5255
*/
5356
class HypermediaAutoConfigurationTests {
5457

@@ -112,6 +115,17 @@ void customizationOfSupportedMediaTypesCanBeDisabled() {
112115
});
113116
}
114117

118+
@Test
119+
void restTemplateCustomizerShouldRegisterHypermediaTypes() {
120+
this.contextRunner.withUserConfiguration(EnableHypermediaSupportConfig.class,
121+
EnableHypermediaRestTemplateSupportConfig.class).run((context) -> {
122+
RestTemplate restTemplate = context.getBean(RestTemplateBuilder.class).build();
123+
assertThat(restTemplate.getMessageConverters())
124+
.flatExtracting(HttpMessageConverter::getSupportedMediaTypes).contains(MediaTypes.HAL_JSON)
125+
.doesNotContainSequence(MediaTypes.HAL_FORMS_JSON);
126+
});
127+
}
128+
115129
@ImportAutoConfiguration({ HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class,
116130
JacksonAutoConfiguration.class, HypermediaAutoConfiguration.class })
117131
static class BaseConfig {
@@ -124,4 +138,10 @@ static class EnableHypermediaSupportConfig {
124138

125139
}
126140

141+
@Configuration(proxyBeanMethods = false)
142+
@ImportAutoConfiguration(RestTemplateAutoConfiguration.class)
143+
static class EnableHypermediaRestTemplateSupportConfig {
144+
145+
}
146+
127147
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2020 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+
* https://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.boot.test.autoconfigure.hateoas;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
23+
import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration;
24+
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
25+
import org.springframework.boot.test.context.SpringBootTest;
26+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
27+
import org.springframework.boot.test.web.client.TestRestTemplate;
28+
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.hateoas.MediaTypes;
32+
import org.springframework.hateoas.config.EnableHypermediaSupport;
33+
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
34+
import org.springframework.http.converter.HttpMessageConverter;
35+
import org.springframework.test.annotation.DirtiesContext;
36+
37+
/**
38+
* @author Greg Turnquist
39+
*/
40+
@SpringBootTest(classes = HypermediaTestRestTemplateTests.TestConfig.class, webEnvironment = WebEnvironment.RANDOM_PORT)
41+
@DirtiesContext
42+
class HypermediaTestRestTemplateTests {
43+
44+
@Autowired
45+
private TestRestTemplate testRestTemplate;
46+
47+
@Test
48+
void testRestTemplateShouldHaveHypermediaSupportWiredIn() {
49+
50+
assertThat(this.testRestTemplate.getRestTemplate().getMessageConverters())
51+
.flatExtracting(HttpMessageConverter::getSupportedMediaTypes)
52+
.contains(MediaTypes.HAL_JSON, MediaTypes.COLLECTION_JSON)
53+
.doesNotContain(MediaTypes.HAL_FORMS_JSON, MediaTypes.UBER_JSON);
54+
}
55+
56+
@ImportAutoConfiguration({ HypermediaAutoConfiguration.class, RestTemplateAutoConfiguration.class })
57+
@Configuration(proxyBeanMethods = false)
58+
@EnableHypermediaSupport(type = { HypermediaType.HAL, HypermediaType.COLLECTION_JSON })
59+
static class TestConfig {
60+
61+
@Bean
62+
TomcatServletWebServerFactory webServerFactory() {
63+
return new TomcatServletWebServerFactory(0);
64+
}
65+
66+
}
67+
68+
}

0 commit comments

Comments
 (0)