diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index fffaee3a60..fd786e55f7 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -302,5 +302,10 @@ spring-data-elasticsearch test + + org.springframework.data + spring-data-rest-webmvc + test + diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java index 7e44e0dc2a..fc7c7a90d1 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java @@ -41,6 +41,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.autoconfigure.hateoas.HypermediaHttpMessageConverterConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorAttributes; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -53,6 +54,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.servlet.DispatcherServlet; @@ -69,6 +72,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; * * @author Dave Syer * @author Stephane Nicoll + * @author Andy Wilkinson * @see EndpointWebMvcAutoConfiguration */ @Configuration @@ -193,6 +197,14 @@ public class EndpointWebMvcChildContextConfiguration { } + @Configuration + @ConditionalOnClass({ LinkDiscoverer.class }) + @Import(HypermediaHttpMessageConverterConfiguration.class) + @EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) + static class HypermediaConfiguration { + + } + static class ServerCustomization implements EmbeddedServletContainerCustomizer, Ordered { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java index e0a9107aaf..18f694cece 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure; import java.io.IOException; import java.lang.reflect.Type; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -61,6 +62,7 @@ import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.TypeUtils; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import com.fasterxml.jackson.annotation.JsonAnyGetter; @@ -219,7 +221,7 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { public static class MvcEndpointAdvice implements ResponseBodyAdvice { @Autowired - private HttpMessageConverters converters; + private List handlerAdapters; private Map> converterCache = new ConcurrentHashMap>(); @@ -275,11 +277,14 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { if (this.converterCache.containsKey(mediaType)) { return (HttpMessageConverter) this.converterCache.get(mediaType); } - for (HttpMessageConverter converter : this.converters) { - if (selectedConverterType.isAssignableFrom(converter.getClass()) - && converter.canWrite(EndpointResource.class, mediaType)) { - this.converterCache.put(mediaType, converter); - return (HttpMessageConverter) converter; + for (RequestMappingHandlerAdapter handlerAdapter : this.handlerAdapters) { + for (HttpMessageConverter converter : handlerAdapter + .getMessageConverters()) { + if (selectedConverterType.isAssignableFrom(converter.getClass()) + && converter.canWrite(EndpointResource.class, mediaType)) { + this.converterCache.put(mediaType, converter); + return (HttpMessageConverter) converter; + } } } return null; diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java index ec46c38a6d..79da5696b5 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java @@ -25,10 +25,13 @@ import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.Status; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockServletContext; -import org.springframework.stereotype.Component; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import static org.junit.Assert.assertEquals; @@ -37,6 +40,7 @@ import static org.junit.Assert.assertEquals; * Tests for {@link EndpointWebMvcAutoConfiguration} of the {@link HealthMvcEndpoint}. * * @author Dave Syer + * @author Andy Wilkinson */ public class HealthMvcEndpointAutoConfigurationTests { @@ -53,12 +57,7 @@ public class HealthMvcEndpointAutoConfigurationTests { public void testSecureByDefault() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); - this.context.register(SecurityAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, - TestHealthIndicator.class); + this.context.register(TestConfiguration.class); this.context.refresh(); Health health = (Health) this.context.getBean(HealthMvcEndpoint.class).invoke( null); @@ -70,25 +69,33 @@ public class HealthMvcEndpointAutoConfigurationTests { public void testNotSecured() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); - this.context.register(SecurityAutoConfiguration.class, - JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, - TestHealthIndicator.class); + this.context.register(TestConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "management.security.enabled=false"); this.context.refresh(); Health health = (Health) this.context.getBean(HealthMvcEndpoint.class).invoke( null); assertEquals(Status.UP, health.getStatus()); - Health map = (Health) health.getDetails().get( - "healthMvcEndpointAutoConfigurationTests.Test"); + Health map = (Health) health.getDetails().get("test"); assertEquals("bar", map.getDetails().get("foo")); } - @Component - protected static class TestHealthIndicator extends AbstractHealthIndicator { + @Configuration + @ImportAutoConfiguration({ SecurityAutoConfiguration.class, + JacksonAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class }) + static class TestConfiguration { + + @Bean + public TestHealthIndicator testHealthIndicator() { + return new TestHealthIndicator(); + } + + } + + static class TestHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck(Builder builder) throws Exception { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java index 2f692e9876..6ac8362d40 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfigurationTests.java @@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfigurati import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; @@ -82,6 +83,7 @@ public class ManagementWebSecurityAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, + WebMvcAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, @@ -110,13 +112,7 @@ public class ManagementWebSecurityAutoConfigurationTests { public void testWebConfigurationWithExtraRole() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); - this.context.register(EndpointAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); + this.context.register(WebConfiguration.class); this.context.refresh(); UserDetails user = getUser(); assertTrue(user.getAuthorities().containsAll( @@ -155,14 +151,7 @@ public class ManagementWebSecurityAutoConfigurationTests { public void testDisableBasicAuthOnApplicationPaths() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); - this.context.register(HttpMessageConvertersAutoConfiguration.class, - JacksonAutoConfiguration.class, EndpointAutoConfiguration.class, - EndpointWebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - SecurityAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - FallbackWebSecurityAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); + this.context.register(WebConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "security.basic.enabled:false"); this.context.refresh(); // Just the management endpoints (one filter) and ignores now plus the backup @@ -248,6 +237,18 @@ public class ManagementWebSecurityAutoConfigurationTests { Matchers.containsString("realm=\"Spring\"")); } + @Configuration + @ImportAutoConfiguration({ SecurityAutoConfiguration.class, + WebMvcAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + FallbackWebSecurityAutoConfiguration.class }) + static class WebConfiguration { + + } + @EnableGlobalAuthentication @Configuration static class AuthenticationConfig { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java index b286e04b04..dc91902a00 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java @@ -25,6 +25,7 @@ import java.lang.annotation.Target; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; @@ -32,16 +33,18 @@ import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfi import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; /** + * Minimal configuration required to run the Actuator with hypermedia. + * * @author Dave Syer + * @author Andy Wilkinson */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration -@Import({ ServerPropertiesAutoConfiguration.class, +@ImportAutoConfiguration({ ServerPropertiesAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, EmbeddedServletContainerAutoConfiguration.class, DispatcherServletAutoConfiguration.class, JacksonAutoConfiguration.class, diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java new file mode 100644 index 0000000000..d5d1beed16 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.mvc; + +import org.junit.Test; +import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; +import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; + +/** + * Integration tests for the Actuator's MVC endpoints. + * + * @author Andy Wilkinson + */ +public class MvcEndpointIntegrationTests { + + private AnnotationConfigWebApplicationContext context; + + @Test + public void defaultJsonResponseIsNotIndented() throws Exception { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(DefaultConfiguration.class); + MockMvc mockMvc = createMockMvc(); + mockMvc.perform(get("/beans")).andExpect(content().string(startsWith("{\""))); + } + + @Test + public void jsonResponsesCanBeIndented() throws Exception { + assertIndentedJsonResponse(DefaultConfiguration.class); + } + + @Test + public void jsonResponsesCanBeIndentedWhenSpringHateoasIsAutoConfigured() + throws Exception { + assertIndentedJsonResponse(SpringHateoasConfiguration.class); + } + + @Test + public void jsonResponsesCanBeIndentedWhenSpringDataRestIsAutoConfigured() + throws Exception { + assertIndentedJsonResponse(SpringDataRestConfiguration.class); + } + + private void assertIndentedJsonResponse(Class configuration) throws Exception { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(configuration); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.serialization.indent-output:true"); + MockMvc mockMvc = createMockMvc(); + mockMvc.perform(get("/beans")).andExpect(content().string(startsWith("{\n"))); + } + + private MockMvc createMockMvc() { + this.context.setServletContext(new MockServletContext()); + this.context.refresh(); + return MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + @ImportAutoConfiguration({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) + static class DefaultConfiguration { + + } + + @ImportAutoConfiguration({ HypermediaAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) + static class SpringHateoasConfiguration { + + } + + @ImportAutoConfiguration({ HypermediaAutoConfiguration.class, + RepositoryRestMvcAutoConfiguration.class, JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) + static class SpringDataRestConfiguration { + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java index 349a199005..637d807e1e 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -40,13 +41,15 @@ import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguratio * * @author Rob Winch * @author Stephane Nicoll + * @author Andy Wilkinson * @since 1.1.0 */ @Configuration @ConditionalOnWebApplication @ConditionalOnMissingBean(RepositoryRestMvcConfiguration.class) @ConditionalOnClass(RepositoryRestMvcConfiguration.class) -@AutoConfigureAfter(HttpMessageConvertersAutoConfiguration.class) +@AutoConfigureAfter({ HttpMessageConvertersAutoConfiguration.class, + JacksonAutoConfiguration.class }) @Import(SpringBootRepositoryRestMvcConfiguration.class) public class RepositoryRestMvcAutoConfiguration { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java index ebbb94bc38..5d534a92dd 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java @@ -21,23 +21,25 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * {@link ConfigurationProperties properties} for Spring HATEOAS. * - * @author Phillip webb + * @author Phillip Webb + * @author Andy Wilkinson * @since 1.2.1 */ @ConfigurationProperties(prefix = "spring.hateoas") public class HateoasProperties { /** - * Specify if HATEOAS support should be applied to the primary ObjectMapper. + * Specify if application/hal+json responses should be sent to requests that accept + * application/json. */ - private boolean applyToPrimaryObjectMapper = true; + private boolean useHalAsDefaultJsonMediaType = true; - public boolean isApplyToPrimaryObjectMapper() { - return this.applyToPrimaryObjectMapper; + public boolean getUseHalAsDefaultJsonMediaType() { + return this.useHalAsDefaultJsonMediaType; } - public void setApplyToPrimaryObjectMapper(boolean applyToPrimaryObjectMapper) { - this.applyToPrimaryObjectMapper = applyToPrimaryObjectMapper; + public void setUseHalAsDefaultJsonMediaType(boolean useHalAsDefaultJsonMediaType) { + this.useHalAsDefaultJsonMediaType = useHalAsDefaultJsonMediaType; } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java index d9e3665667..afb8db3b1f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java @@ -16,36 +16,30 @@ package org.springframework.boot.autoconfigure.hateoas; -import javax.annotation.PostConstruct; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.context.annotation.Import; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverers; -import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.Resource; import org.springframework.hateoas.config.EnableEntityLinks; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; -import org.springframework.hateoas.hal.CurieProvider; -import org.springframework.hateoas.hal.Jackson2HalModule; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.plugin.core.Plugin; import org.springframework.web.bind.annotation.RequestMapping; @@ -65,8 +59,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; @ConditionalOnClass({ Resource.class, RequestMapping.class, Plugin.class }) @ConditionalOnWebApplication @AutoConfigureAfter({ WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class }) + HttpMessageConvertersAutoConfiguration.class, + RepositoryRestMvcAutoConfiguration.class }) @EnableConfigurationProperties(HateoasProperties.class) +@Import(HypermediaHttpMessageConverterConfiguration.class) public class HypermediaAutoConfiguration { @Configuration @@ -74,48 +70,9 @@ public class HypermediaAutoConfiguration { @EnableHypermediaSupport(type = HypermediaType.HAL) protected static class HypermediaConfiguration { - @ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, ObjectMapper.class }) - protected static class HalObjectMapperConfiguration { - - @Autowired - private HateoasProperties hateoasProperties; - - @Autowired(required = false) - private CurieProvider curieProvider; - - @Autowired - @Qualifier("_relProvider") - private RelProvider relProvider; - - @Autowired(required = false) - private ObjectMapper primaryObjectMapper; - - @Autowired - @Qualifier("linkRelationMessageSource") - private MessageSourceAccessor linkRelationMessageSource; - - @PostConstruct - public void configurePrimaryObjectMapper() { - if (this.primaryObjectMapper != null - && this.hateoasProperties.isApplyToPrimaryObjectMapper()) { - registerHalModule(this.primaryObjectMapper); - } - } - - private void registerHalModule(ObjectMapper objectMapper) { - objectMapper.registerModule(new Jackson2HalModule()); - Jackson2HalModule.HalHandlerInstantiator instantiator = new Jackson2HalModule.HalHandlerInstantiator( - HalObjectMapperConfiguration.this.relProvider, - HalObjectMapperConfiguration.this.curieProvider, - this.linkRelationMessageSource); - objectMapper.setHandlerInstantiator(instantiator); - } - - @Bean - public static HalObjectMapperConfigurer halObjectMapperConfigurer() { - return new HalObjectMapperConfigurer(); - } - + @Bean + public static HalObjectMapperConfigurer halObjectMapperConfigurer() { + return new HalObjectMapperConfigurer(); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java new file mode 100644 index 0000000000..a2673c0619 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.hateoas; + +import java.util.Arrays; +import java.util.Map; +import java.util.Map.Entry; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +/** + * Configuration for {@link HttpMessageConverter HttpMessageConverters} when hypermedia is + * enabled. + * + * @author Andy Wilkinson + */ +public class HypermediaHttpMessageConverterConfiguration { + + @Bean + @ConditionalOnProperty(prefix = "spring.hateoas", name = "use-hal-as-default-json-media-type", matchIfMissing = true) + public static HalMessageConverterSupportedMediaTypesCustomizer halMessageConverterSupportedMediaTypeCustomizer() { + return new HalMessageConverterSupportedMediaTypesCustomizer(); + } + + /** + * Updates any {@link TypeConstrainedMappingJackson2HttpMessageConverter}s to support + * {@code application/json} in addition to {@code application/hal+json}. Cannot be a + * {@link BeanPostProcessor} as processing must be performed after + * {@code Jackson2ModuleRegisteringBeanPostProcessor} has registered the converter and + * it is unordered. + */ + private static class HalMessageConverterSupportedMediaTypesCustomizer implements + BeanFactoryAware { + + private volatile BeanFactory beanFactory; + + @PostConstruct + public void customizedSupportedMediaTypes() { + if (this.beanFactory instanceof ListableBeanFactory) { + Map handlerAdapters = ((ListableBeanFactory) this.beanFactory) + .getBeansOfType(RequestMappingHandlerAdapter.class); + for (Entry entry : handlerAdapters + .entrySet()) { + RequestMappingHandlerAdapter handlerAdapter = entry.getValue(); + for (HttpMessageConverter converter : handlerAdapter + .getMessageConverters()) { + if (converter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { + ((TypeConstrainedMappingJackson2HttpMessageConverter) converter) + .setSupportedMediaTypes(Arrays.asList( + MediaTypes.HAL_JSON, + MediaType.APPLICATION_JSON)); + } + } + + } + } + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java index 3709b4afb8..73f2ceacd3 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.data.rest; import java.net.URI; import java.util.Date; +import java.util.Map; import org.junit.After; import org.junit.Test; @@ -25,8 +26,10 @@ import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfigurati import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.data.jpa.city.City; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,8 +45,11 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; /** * Tests for {@link RepositoryRestMvcAutoConfiguration}. @@ -100,6 +106,15 @@ public class RepositoryRestMvcAutoConfigurationTests { assertThatDateIsFormattedCorrectly("objectMapper"); } + @Test + public void primaryObjectMapperIsAvailable() { + load(TestConfiguration.class); + Map objectMappers = this.context + .getBeansOfType(ObjectMapper.class); + assertThat(objectMappers.size(), is(greaterThan(1))); + this.context.getBean(ObjectMapper.class); + } + public void assertThatDateIsFormattedCorrectly(String beanName) throws JsonProcessingException { ObjectMapper objectMapper = this.context.getBean(beanName, ObjectMapper.class); @@ -111,16 +126,22 @@ public class RepositoryRestMvcAutoConfigurationTests { private void load(Class config, String... environment) { AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); applicationContext.setServletContext(new MockServletContext()); - applicationContext.register(config, EmbeddedDataSourceConfiguration.class, - HibernateJpaAutoConfiguration.class, - JpaRepositoriesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, - RepositoryRestMvcAutoConfiguration.class); + applicationContext.register(config, BaseConfiguration.class); EnvironmentTestUtils.addEnvironment(applicationContext, environment); applicationContext.refresh(); this.context = applicationContext; } + @Configuration + @Import(EmbeddedDataSourceConfiguration.class) + @ImportAutoConfiguration({ HibernateJpaAutoConfiguration.class, + JpaRepositoriesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + RepositoryRestMvcAutoConfiguration.class, JacksonAutoConfiguration.class }) + protected static class BaseConfiguration { + + } + @Configuration @TestAutoConfigurationPackage(City.class) @EnableWebMvc diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java index 8c5655c9ef..f285d30048 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java @@ -19,6 +19,9 @@ package org.springframework.boot.autoconfigure.hateoas; import org.junit.After; import org.junit.Test; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.EntityLinks; @@ -28,12 +31,21 @@ import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.hal.HalLinkDiscoverer; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** @@ -41,6 +53,7 @@ import static org.junit.Assert.assertTrue; * * @author Roy Clarkson * @author Oliver Gierke + * @author Andy Wilkinson */ public class HypermediaAutoConfigurationTests { @@ -56,7 +69,8 @@ public class HypermediaAutoConfigurationTests { @Test public void linkDiscoverersCreated() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(HypermediaAutoConfiguration.class); + this.context.setServletContext(new MockServletContext()); + this.context.register(BaseConfig.class); this.context.refresh(); LinkDiscoverers discoverers = this.context.getBean(LinkDiscoverers.class); assertNotNull(discoverers); @@ -67,7 +81,8 @@ public class HypermediaAutoConfigurationTests { @Test public void entityLinksCreated() throws Exception { this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(HypermediaAutoConfiguration.class); + this.context.setServletContext(new MockServletContext()); + this.context.register(BaseConfig.class); this.context.refresh(); EntityLinks discoverers = this.context.getBean(EntityLinks.class); assertNotNull(discoverers); @@ -76,16 +91,23 @@ public class HypermediaAutoConfigurationTests { @Test public void doesBackOffIfEnableHypermediaSupportIsDeclaredManually() { this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(SampleConfig.class, HypermediaAutoConfiguration.class); + this.context.setServletContext(new MockServletContext()); + this.context.register(EnableHypermediaSupportConfig.class, BaseConfig.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.serialization.INDENT_OUTPUT:true"); this.context.refresh(); - this.context.getBean(LinkDiscoverers.class); + ObjectMapper objectMapper = this.context.getBean("_halObjectMapper", + ObjectMapper.class); + assertThat( + objectMapper.getSerializationConfig().isEnabled( + SerializationFeature.INDENT_OUTPUT), is(false)); } @Test public void jacksonConfigurationIsAppliedToTheHalObjectMapper() { this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(JacksonAutoConfiguration.class, - HypermediaAutoConfiguration.class); + this.context.setServletContext(new MockServletContext()); + this.context.register(BaseConfig.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.serialization.INDENT_OUTPUT:true"); this.context.refresh(); @@ -95,9 +117,52 @@ public class HypermediaAutoConfigurationTests { SerializationFeature.INDENT_OUTPUT)); } + @Test + public void supportedMediaTypesOfTypeConstrainedConvertersIsCustomized() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(BaseConfig.class); + this.context.refresh(); + RequestMappingHandlerAdapter handlerAdapter = this.context + .getBean(RequestMappingHandlerAdapter.class); + for (HttpMessageConverter converter : handlerAdapter.getMessageConverters()) { + if (converter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { + assertThat( + converter.getSupportedMediaTypes(), + containsInAnyOrder(MediaType.APPLICATION_JSON, + MediaTypes.HAL_JSON)); + } + } + } + + @Test + public void customizationOfSupportedMediaTypesCanBeDisabled() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(BaseConfig.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.hateoas.use-hal-as-default-json-media-type:false"); + this.context.refresh(); + RequestMappingHandlerAdapter handlerAdapter = this.context + .getBean(RequestMappingHandlerAdapter.class); + for (HttpMessageConverter converter : handlerAdapter.getMessageConverters()) { + if (converter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { + assertThat(converter.getSupportedMediaTypes(), + contains(MediaTypes.HAL_JSON)); + } + } + } + + @ImportAutoConfiguration({ HttpMessageConvertersAutoConfiguration.class, + WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, + HypermediaAutoConfiguration.class }) + static class BaseConfig { + + } + @Configuration @EnableHypermediaSupport(type = HypermediaType.HAL) - static class SampleConfig { + static class EnableHypermediaSupportConfig { }