diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java index 916ccb2eb9..861793eb73 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java @@ -26,17 +26,21 @@ import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -204,6 +208,11 @@ public abstract class AbstractEndpointHandlerMapping return addSecurityInterceptor(chain); } + @Override + protected void extendInterceptors(List interceptors) { + interceptors.add(new SkipPathExtensionContentNegotiation()); + } + private HandlerExecutionChain addSecurityInterceptor(HandlerExecutionChain chain) { List interceptors = new ArrayList<>(); if (chain.getInterceptors() != null) { @@ -279,4 +288,22 @@ public abstract class AbstractEndpointHandlerMapping return this.corsConfiguration; } + /** + * {@link HandlerInterceptorAdapter} to ensure that + * {@link PathExtensionContentNegotiationStrategy} is skipped for actuator endpoints. + */ + private static final class SkipPathExtensionContentNegotiation + extends HandlerInterceptorAdapter { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + request.setAttribute( + WebMvcAutoConfiguration.SKIP_PATH_EXTENSION_CONTENT_NEGOTIATION_ATTRIBUTE, + Boolean.TRUE); + return true; + } + + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index 1d259047f1..df17a03424 100755 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -181,7 +181,7 @@ public class EndpointWebMvcAutoConfigurationTests { .getBean(ManagementContextResolver.class).getApplicationContext(); List interceptors = (List) ReflectionTestUtils.getField( managementContext.getBean(EndpointHandlerMapping.class), "interceptors"); - assertThat(interceptors).hasSize(1); + assertThat(interceptors).hasSize(2); } @Test @@ -201,7 +201,7 @@ public class EndpointWebMvcAutoConfigurationTests { .getBean(ManagementContextResolver.class).getApplicationContext(); List interceptors = (List) ReflectionTestUtils.getField( managementContext.getBean(EndpointHandlerMapping.class), "interceptors"); - assertThat(interceptors).hasSize(1); + assertThat(interceptors).hasSize(2); ServletWebServerFactory parentFactory = this.applicationContext .getBean(ServletWebServerFactory.class); ServletWebServerFactory managementFactory = managementContext @@ -535,7 +535,7 @@ public class EndpointWebMvcAutoConfigurationTests { .getBean(ManagementContextResolver.class).getApplicationContext(); List interceptors = (List) ReflectionTestUtils.getField( managementContext.getBean(EndpointHandlerMapping.class), "interceptors"); - assertThat(interceptors).hasSize(1); + assertThat(interceptors).hasSize(2); ManagementServerProperties managementServerProperties = this.applicationContext .getBean(ManagementServerProperties.class); assertThat(managementServerProperties.getSsl()).isNotNull(); @@ -577,7 +577,7 @@ public class EndpointWebMvcAutoConfigurationTests { .getBean(ManagementContextResolver.class).getApplicationContext(); List interceptors = (List) ReflectionTestUtils.getField( managementContext.getBean(EndpointHandlerMapping.class), "interceptors"); - assertThat(interceptors).hasSize(1); + assertThat(interceptors).hasSize(2); ManagementServerProperties managementServerProperties = this.applicationContext .getBean(ManagementServerProperties.class); assertThat(managementServerProperties.getSsl()).isNotNull(); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java index a34aa19268..4f7a6e205c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMappingTests.java @@ -60,7 +60,8 @@ public abstract class AbstractEndpointHandlerMappingTests { Collections.singletonList(endpoint)); mapping.setApplicationContext(this.context); mapping.afterPropertiesSet(); - assertThat(mapping.getHandler(request("POST", "/a")).getInterceptors()).isNull(); + assertThat(mapping.getHandler(request("POST", "/a")).getInterceptors()) + .hasSize(1); } @Test @@ -75,8 +76,8 @@ public abstract class AbstractEndpointHandlerMappingTests { mapping.afterPropertiesSet(); MockHttpServletRequest request = request("POST", "/a"); request.addHeader("Origin", "http://example.com"); - assertThat(mapping.getHandler(request).getInterceptors().length).isEqualTo(2); - assertThat(mapping.getHandler(request).getInterceptors()[1]) + assertThat(mapping.getHandler(request).getInterceptors().length).isEqualTo(3); + assertThat(mapping.getHandler(request).getInterceptors()[2]) .isEqualTo(securityInterceptor); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java index 5f2309f2f5..2bb4b18b4b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/LoggersMvcEndpointTests.java @@ -177,6 +177,16 @@ public class LoggersMvcEndpointTests { verifyZeroInteractions(this.loggingSystem); } + @Test + public void logLevelForLoggerWithNameThatCouldBeMistakenForAPathExtension() + throws Exception { + given(this.loggingSystem.getLoggerConfiguration("com.png")) + .willReturn(new LoggerConfiguration("com.png", null, LogLevel.DEBUG)); + this.mvc.perform(get("/loggers/com.png")).andExpect(status().isOk()) + .andExpect(content().string(equalTo( + "{\"configuredLevel\":null," + "\"effectiveLevel\":\"DEBUG\"}"))); + } + @Configuration @Import({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java index a9cce63b76..5290124e4f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java @@ -124,20 +124,30 @@ public class MetricsMvcEndpointTests { .andExpect(content().string(equalTo("{\"foo\":1}"))); } + @Test + public void specificMetricWithNameThatCouldBeMistakenForAPathExtension() + throws Exception { + this.mvc.perform(get("/metrics/bar.png")).andExpect(status().isOk()) + .andExpect(content().string(equalTo("{\"bar.png\":1}"))); + } + @Test public void specificMetricWhenDisabled() throws Exception { this.context.getBean(MetricsEndpoint.class).setEnabled(false); - this.mvc.perform(get("/application/metrics/foo")).andExpect(status().isNotFound()); + this.mvc.perform(get("/application/metrics/foo")) + .andExpect(status().isNotFound()); } @Test public void specificMetricThatDoesNotExist() throws Exception { - this.mvc.perform(get("/application/metrics/bar")).andExpect(status().isNotFound()); + this.mvc.perform(get("/application/metrics/bar")) + .andExpect(status().isNotFound()); } @Test public void regexAll() throws Exception { - String expected = "\"foo\":1,\"group1.a\":1,\"group1.b\":1,\"group2.a\":1,\"group2_a\":1"; + String expected = "\"foo\":1,\"bar.png\":1,\"group1.a\":1,\"group1.b\":1," + + "\"group2.a\":1,\"group2_a\":1"; this.mvc.perform(get("/application/metrics/.*")).andExpect(status().isOk()) .andExpect(content().string(containsString(expected))); } @@ -145,14 +155,16 @@ public class MetricsMvcEndpointTests { @Test public void regexGroupDot() throws Exception { String expected = "\"group1.a\":1,\"group1.b\":1,\"group2.a\":1"; - this.mvc.perform(get("/application/metrics/group[0-9]+\\..*")).andExpect(status().isOk()) + this.mvc.perform(get("/application/metrics/group[0-9]+\\..*")) + .andExpect(status().isOk()) .andExpect(content().string(containsString(expected))); } @Test public void regexGroup1() throws Exception { String expected = "\"group1.a\":1,\"group1.b\":1"; - this.mvc.perform(get("/application/metrics/group1\\..*")).andExpect(status().isOk()) + this.mvc.perform(get("/application/metrics/group1\\..*")) + .andExpect(status().isOk()) .andExpect(content().string(containsString(expected))); } @@ -176,6 +188,7 @@ public class MetricsMvcEndpointTests { public Collection> metrics() { ArrayList> metrics = new ArrayList<>(); metrics.add(new Metric<>("foo", 1)); + metrics.add(new Metric<>("bar.png", 1)); metrics.add(new Metric<>("group1.a", 1)); metrics.add(new Metric<>("group1.b", 1)); metrics.add(new Metric<>("group2.a", 1)); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriterTests.java index ad609de2b3..92ac5e265b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriterTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriterTests.java @@ -102,7 +102,8 @@ public class StatsdMetricWriterTests { public void incrementMetricWithInvalidCharsInName() throws Exception { this.writer.increment(new Delta("counter.fo:o", 3L)); this.server.waitForMessage(); - assertThat(this.server.messagesReceived().get(0)).isEqualTo("me.counter.fo-o:3|c"); + assertThat(this.server.messagesReceived().get(0)) + .isEqualTo("me.counter.fo-o:3|c"); } @Test diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java index e4da42c3df..056a8b7775 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java @@ -21,9 +21,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; @@ -68,7 +65,6 @@ import org.springframework.social.connect.support.OAuth2ConnectionFactory; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; /** @@ -83,9 +79,6 @@ import org.springframework.web.client.RestTemplate; @ConditionalOnMissingBean(AuthorizationServerEndpointsConfiguration.class) public class ResourceServerTokenServicesConfiguration { - private static final Log logger = LogFactory - .getLog(ResourceServerTokenServicesConfiguration.class); - @Bean @ConditionalOnMissingBean public UserInfoRestTemplateFactory userInfoRestTemplateFactory( @@ -278,13 +271,7 @@ public class ResourceServerTokenServicesConfiguration { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); String keyValue = this.resource.getJwt().getKeyValue(); if (!StringUtils.hasText(keyValue)) { - try { - keyValue = getKeyFromServer(); - } - catch (ResourceAccessException ex) { - logger.warn("Failed to fetch token key (you may need to refresh " - + "when the auth server is back)"); - } + keyValue = getKeyFromServer(); } if (StringUtils.hasText(keyValue) && !keyValue.startsWith("-----BEGIN")) { converter.setSigningKey(keyValue); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index 7e5e2f6414..6399bdccce 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; @@ -72,8 +73,13 @@ import org.springframework.util.StringUtils; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextListener; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.filter.HttpPutFormContentFilter; @@ -130,9 +136,16 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver; ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { - public static String DEFAULT_PREFIX = ""; + public static final String DEFAULT_PREFIX = ""; - public static String DEFAULT_SUFFIX = ""; + public static final String DEFAULT_SUFFIX = ""; + + /** + * Attribute that can be added to the web request when the + * {@link PathExtensionContentNegotiationStrategy} should be be skipped. + */ + public static final String SKIP_PATH_EXTENSION_CONTENT_NEGOTIATION_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class + .getName() + ".SKIP"; @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @@ -457,6 +470,22 @@ public class WebMvcAutoConfiguration { } } + @Bean + @Override + public ContentNegotiationManager mvcContentNegotiationManager() { + ContentNegotiationManager manager = super.mvcContentNegotiationManager(); + List strategies = manager.getStrategies(); + ListIterator iterator = strategies.listIterator(); + while (iterator.hasNext()) { + ContentNegotiationStrategy strategy = iterator.next(); + if (strategy instanceof PathExtensionContentNegotiationStrategy) { + iterator.set(new OptionalPathExtensionContentNegotiationStrategy( + strategy)); + } + } + return manager; + } + } @Configuration @@ -554,4 +583,32 @@ public class WebMvcAutoConfiguration { } + /** + * Decorator to make {@link PathExtensionContentNegotiationStrategy} optional + * depending on a request attribute. + */ + static class OptionalPathExtensionContentNegotiationStrategy + implements ContentNegotiationStrategy { + + private final ContentNegotiationStrategy delegate; + + OptionalPathExtensionContentNegotiationStrategy( + ContentNegotiationStrategy delegate) { + this.delegate = delegate; + } + + @Override + public List resolveMediaTypes(NativeWebRequest webRequest) + throws HttpMediaTypeNotAcceptableException { + Object skip = webRequest.getAttribute( + SKIP_PATH_EXTENSION_CONTENT_NEGOTIATION_ATTRIBUTE, + RequestAttributes.SCOPE_REQUEST); + if (skip != null && Boolean.parseBoolean(skip.toString())) { + return Collections.emptyList(); + } + return this.delegate.resolveMediaTypes(webRequest); + } + + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java index 7eb1fabc2d..8d7648b1c6 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java @@ -46,9 +46,12 @@ import org.springframework.context.annotation.Import; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.client.OAuth2RestTemplate; @@ -61,9 +64,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link ResourceServerTokenServicesConfiguration}. @@ -211,7 +212,7 @@ public class ResourceServerTokenServicesConfigurationTests { EnvironmentTestUtils.addEnvironment(this.environment, "security.oauth2.resource.jwk.key-set-uri=http://my-auth-server/token_keys"); this.context = new SpringApplicationBuilder(ResourceConfiguration.class) - .environment(this.environment).web(false).run(); + .environment(this.environment).web(WebApplicationType.NONE).run(); DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class); assertThat(services).isNotNull(); this.thrown.expect(NoSuchBeanDefinitionException.class); @@ -249,23 +250,12 @@ public class ResourceServerTokenServicesConfigurationTests { @Test public void jwtAccessTokenConverterIsConfiguredWhenKeyUriIsProvided() { - EnvironmentTestUtils.addEnvironment(this.environment, - "security.oauth2.resource.jwt.key-uri=http://localhost:12345/banana"); - this.context = new SpringApplicationBuilder(ResourceConfiguration.class) - .environment(this.environment).web(WebApplicationType.NONE).run(); - assertThat(this.context.getBeansOfType(JwtAccessTokenConverter.class)).hasSize(1); - } - - @Test - public void jwtAccessTokenConverterRestTemplateCanBeCustomized() { EnvironmentTestUtils.addEnvironment(this.environment, "security.oauth2.resource.jwt.key-uri=http://localhost:12345/banana"); this.context = new SpringApplicationBuilder(ResourceConfiguration.class, JwtAccessTokenConverterRestTemplateCustomizerConfiguration.class) .environment(this.environment).web(WebApplicationType.NONE).run(); - JwtAccessTokenConverterRestTemplateCustomizer customizer = this.context - .getBean(JwtAccessTokenConverterRestTemplateCustomizer.class); - verify(customizer).customize(any(RestTemplate.class)); + assertThat(this.context.getBeansOfType(JwtAccessTokenConverter.class)).hasSize(1); } @Configuration @@ -387,7 +377,29 @@ public class ResourceServerTokenServicesConfigurationTests { @Bean public JwtAccessTokenConverterRestTemplateCustomizer restTemplateCustomizer() { - return mock(JwtAccessTokenConverterRestTemplateCustomizer.class); + return new MockRestCallCustomizer(); + } + + } + + private static class MockRestCallCustomizer + implements JwtAccessTokenConverterRestTemplateCustomizer { + + @Override + public void customize(RestTemplate template) { + template.getInterceptors().add(new ClientHttpRequestInterceptor() { + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + String payload = "{\"value\":\"FOO\"}"; + MockClientHttpResponse response = new MockClientHttpResponse( + payload.getBytes(), HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + return response; + } + + }); } } diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/JsonTestIntegrationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/JsonTestIntegrationTests.java index 35aeb19aa0..cf1fb09915 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/JsonTestIntegrationTests.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/JsonTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java index 66036972c4..edea2fdbd8 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java @@ -19,6 +19,7 @@ package org.springframework.boot.test.context; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -31,13 +32,16 @@ import org.springframework.boot.WebApplicationType; import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySources; import org.springframework.core.env.PropertySourcesPropertyResolver; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContext; @@ -153,7 +157,7 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr propertySourceProperties .toArray(new String[propertySourceProperties.size()])); WebEnvironment webEnvironment = getWebEnvironment(mergedConfig.getTestClass()); - if (webEnvironment != null) { + if (webEnvironment != null && isWebEnvironmentSupported(mergedConfig)) { WebApplicationType webApplicationType = getWebApplicationType(mergedConfig); if (webApplicationType == WebApplicationType.SERVLET && (webEnvironment.isEmbedded() @@ -198,6 +202,32 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr return WebApplicationType.SERVLET; } + private boolean isWebEnvironmentSupported(MergedContextConfiguration mergedConfig) { + Class testClass = mergedConfig.getTestClass(); + ContextHierarchy hierarchy = AnnotationUtils.getAnnotation(testClass, + ContextHierarchy.class); + if (hierarchy == null || hierarchy.value().length == 0) { + return true; + } + ContextConfiguration[] configurations = hierarchy.value(); + return isFromConfiguration(mergedConfig, + configurations[configurations.length - 1]); + } + + private boolean isFromConfiguration(MergedContextConfiguration candidateConfig, + ContextConfiguration configuration) { + ContextConfigurationAttributes attributes = new ContextConfigurationAttributes( + candidateConfig.getTestClass(), configuration); + Set> configurationClasses = new HashSet>( + Arrays.asList(attributes.getClasses())); + for (Class candidate : candidateConfig.getClasses()) { + if (configurationClasses.contains(candidate)) { + return true; + } + } + return false; + } + private WebApplicationType getConfiguredWebApplicationType( MergedContextConfiguration configuration) { PropertySources sources = convertToPropertySources( diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWebEnvironmentContextHierarchyTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWebEnvironmentContextHierarchyTests.java new file mode 100644 index 0000000000..e506577a66 --- /dev/null +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWebEnvironmentContextHierarchyTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2017 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.test.context; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.AbstractSpringBootTestEmbeddedWebEnvironmentTests.AbstractConfig; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.SpringBootTestWebEnvironmentContextHierarchyTests.ChildConfiguration; +import org.springframework.boot.test.context.SpringBootTestWebEnvironmentContextHierarchyTests.ParentConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringBootTest} configured with {@link WebEnvironment#DEFINED_PORT}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +@RunWith(SpringRunner.class) +@DirtiesContext +@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT, properties = { + "server.port=0", "value=123" }) +@ContextHierarchy({ @ContextConfiguration(classes = ParentConfiguration.class), + @ContextConfiguration(classes = ChildConfiguration.class) }) +public class SpringBootTestWebEnvironmentContextHierarchyTests { + + @Autowired + private ApplicationContext context; + + @Test + public void testShouldOnlyStartSingleServer() throws Exception { + ApplicationContext parent = this.context.getParent(); + assertThat(this.context).isInstanceOf(WebApplicationContext.class); + assertThat(parent).isNotInstanceOf(WebApplicationContext.class); + } + + @Configuration + protected static class ParentConfiguration extends AbstractConfig { + + } + + @Configuration + @EnableWebMvc + @RestController + protected static class ChildConfiguration extends AbstractConfig { + + } + +} diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/json/ExampleObject.java b/spring-boot-test/src/test/java/org/springframework/boot/test/json/ExampleObject.java index 9ad5a00d49..3db0036a58 100644 --- a/spring-boot-test/src/test/java/org/springframework/boot/test/json/ExampleObject.java +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/json/ExampleObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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.