From 0af4536316e1a4666ea27907669b44d8fbd574a6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2017 12:09:19 +0100 Subject: [PATCH] Ensure media types are used consistently across endpoint mappings Previously, the media types that are consumed and produced by endpoints were configured in the web stack-specific configuration. Furthermore, these configured media types were not used for the discovery "endpoint" that links to all the available endpoints. This commit introduces EndpointMediaTypes that is configred in a single, central location and then used to configure the consumed and produced media types for endpoints exposed via WebFlux, Web MVC, and Jersey as well as the discovery "endpoint" provided by each. Closes gh-10659 --- ...CloudFoundryActuatorAutoConfiguration.java | 6 +- ...undryWebEndpointServletHandlerMapping.java | 5 +- .../endpoint/EndpointAutoConfiguration.java | 8 +- ...ndpointManagementContextConfiguration.java | 4 +- ...ndpointManagementContextConfiguration.java | 4 +- ...ndpointManagementContextConfiguration.java | 4 +- ...FoundryActuatorAutoConfigurationTests.java | 17 ++ ...FoundryMvcWebEndpointIntegrationTests.java | 18 ++- .../EndpointAutoConfigurationTests.java | 47 ++++++ .../servlet/RequestMappingEndpointTests.java | 6 +- .../endpoint/web/EndpointMediaTypes.java | 68 ++++++++ .../WebAnnotationEndpointDiscoverer.java | 24 ++- .../jersey/JerseyEndpointResourceFactory.java | 20 ++- .../WebFluxEndpointHandlerMapping.java | 30 +++- .../AbstractWebMvcEndpointHandlerMapping.java | 18 ++- .../servlet/WebMvcEndpointHandlerMapping.java | 12 +- .../AbstractWebEndpointIntegrationTests.java | 150 ++++++++++-------- .../WebAnnotationEndpointDiscovererTests.java | 14 +- .../JerseyWebEndpointIntegrationTests.java | 6 +- .../WebFluxEndpointIntegrationTests.java | 7 +- .../MvcWebEndpointIntegrationTests.java | 7 +- .../web/test/JerseyEndpointsRunner.java | 12 +- .../web/test/WebFluxEndpointsRunner.java | 8 +- .../web/test/WebMvcEndpointRunner.java | 8 +- 24 files changed, 370 insertions(+), 133 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java index 068698bdcd..d0ba071454 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java @@ -20,6 +20,7 @@ import java.util.Arrays; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointProvider; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -67,11 +68,12 @@ public class CloudFoundryActuatorAutoConfiguration { @Bean public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping( - EndpointProvider provider, Environment environment, + EndpointProvider provider, + EndpointMediaTypes endpointMediaTypes, Environment environment, RestTemplateBuilder builder) { return new CloudFoundryWebEndpointServletHandlerMapping( new EndpointMapping("/cloudfoundryapplication"), - provider.getEndpoints(), getCorsConfiguration(), + provider.getEndpoints(), endpointMediaTypes, getCorsConfiguration(), getSecurityInterceptor(builder, environment)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java index c94e8a3f14..5b673c32ee 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java @@ -36,6 +36,7 @@ import org.springframework.boot.actuate.endpoint.OperationInvoker; import org.springframework.boot.actuate.endpoint.ParameterMappingException; import org.springframework.boot.actuate.endpoint.ParametersMissingException; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.Link; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; @@ -76,9 +77,9 @@ class CloudFoundryWebEndpointServletHandlerMapping CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, - CorsConfiguration corsConfiguration, + EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, CloudFoundrySecurityInterceptor securityInterceptor) { - super(endpointMapping, webEndpoints, corsConfiguration); + super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration); this.securityInterceptor = securityInterceptor; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java index 3ace88daee..35863b6a10 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFactory; import org.springframework.boot.actuate.endpoint.convert.ConversionServiceOperationParameterMapper; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -71,6 +72,11 @@ public class EndpointAutoConfiguration { this.applicationContext = applicationContext; } + @Bean + public EndpointMediaTypes endpointMediaTypes() { + return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES); + } + @Bean public EndpointProvider webEndpointProvider( OperationParameterMapper parameterMapper, @@ -78,7 +84,7 @@ public class EndpointAutoConfiguration { Environment environment = this.applicationContext.getEnvironment(); WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer( this.applicationContext, parameterMapper, cachingConfigurationFactory, - MEDIA_TYPES, MEDIA_TYPES); + endpointMediaTypes()); return new EndpointProvider<>(environment, endpointDiscoverer, EndpointExposure.WEB); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index db3e85073e..fab0195f34 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathP import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -55,11 +56,12 @@ class JerseyWebEndpointManagementContextConfiguration { @Bean public ResourceConfigCustomizer webEndpointRegistrar( EndpointProvider provider, + EndpointMediaTypes endpointMediaTypes, WebEndpointProperties webEndpointProperties) { return (resourceConfig) -> resourceConfig.registerResources( new HashSet<>(new JerseyEndpointResourceFactory().createEndpointResources( new EndpointMapping(webEndpointProperties.getBasePath()), - provider.getEndpoints()))); + provider.getEndpoints(), endpointMediaTypes))); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 5520e78558..d8034449b9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathP import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -45,10 +46,11 @@ public class WebFluxEndpointManagementContextConfiguration { @ConditionalOnMissingBean public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping( EndpointProvider provider, + EndpointMediaTypes endpointMediaTypes, WebEndpointProperties webEndpointProperties) { return new WebFluxEndpointHandlerMapping( new EndpointMapping(webEndpointProperties.getBasePath()), - provider.getEndpoints()); + provider.getEndpoints(), endpointMediaTypes); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 152cd624a8..797a7bf905 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointPr import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -56,11 +57,12 @@ public class WebMvcEndpointManagementContextConfiguration { @ConditionalOnMissingBean public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping( EndpointProvider provider, + EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties) { WebMvcEndpointHandlerMapping handlerMapping = new WebMvcEndpointHandlerMapping( new EndpointMapping(webEndpointProperties.getBasePath()), - provider.getEndpoints(), getCorsConfiguration(corsProperties)); + provider.getEndpoints(), endpointMediaTypes, getCorsConfiguration(corsProperties)); return handlerMapping; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java index 4f815d8cea..4028390aa5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -40,11 +41,15 @@ import org.springframework.mock.web.MockServletContext; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; /** * Tests for {@link CloudFoundryActuatorAutoConfiguration}. @@ -92,6 +97,18 @@ public class CloudFoundryActuatorAutoConfigurationTests { Arrays.asList("Authorization", "X-Cf-App-Instance", "Content-Type")); } + @Test + public void cloudfoundryapplicationProducesActuatorMediaType() throws Exception { + TestPropertyValues + .of("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id", + "vcap.application.cf_api:http://my-cloud-controller.com") + .applyTo(this.context); + this.context.refresh(); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + mockMvc.perform(get("/cloudfoundryapplication")).andExpect(header() + .string("Content-Type", ActuatorMediaType.V2_JSON + ";charset=UTF-8")); + } + @Test public void cloudFoundryPlatformActiveSetsApplicationId() throws Exception { CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java index fdf12d908c..6500dcb418 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.actuate.endpoint.cache.CachingConfiguration; import org.springframework.boot.actuate.endpoint.convert.ConversionServiceOperationParameterMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -190,28 +191,35 @@ public class CloudFoundryMvcWebEndpointIntegrationTests { "app-id"); } + @Bean + public EndpointMediaTypes EndpointMediaTypes() { + return new EndpointMediaTypes(Collections.singletonList("application/json"), + Collections.singletonList("application/json")); + } + @Bean public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping( WebAnnotationEndpointDiscoverer webEndpointDiscoverer, + EndpointMediaTypes endpointMediaTypes, CloudFoundrySecurityInterceptor interceptor) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); return new CloudFoundryWebEndpointServletHandlerMapping( new EndpointMapping("/cfApplication"), - webEndpointDiscoverer.discoverEndpoints(), corsConfiguration, - interceptor); + webEndpointDiscoverer.discoverEndpoints(), endpointMediaTypes, + corsConfiguration, interceptor); } @Bean public WebAnnotationEndpointDiscoverer webEndpointDiscoverer( - ApplicationContext applicationContext) { + ApplicationContext applicationContext, + EndpointMediaTypes endpointMediaTypes) { OperationParameterMapper parameterMapper = new ConversionServiceOperationParameterMapper( DefaultConversionService.getSharedInstance()); return new WebAnnotationEndpointDiscoverer(applicationContext, parameterMapper, (id) -> new CachingConfiguration(0), - Collections.singletonList("application/json"), - Collections.singletonList("application/json")); + endpointMediaTypes); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java new file mode 100644 index 0000000000..dcfb3a7377 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java @@ -0,0 +1,47 @@ +/* + * 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.actuate.autoconfigure.endpoint; + +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EndpointAutoConfiguration}. + * + * @author Andy Wilkinson + */ +public class EndpointAutoConfigurationTests { + + @Test + public void webApplicationConfiguresEndpointMediaTypes() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class)) + .run((context) -> { + EndpointMediaTypes endpointMediaTypes = context + .getBean(EndpointMediaTypes.class); + assertThat(endpointMediaTypes.getConsumed()).containsExactly( + ActuatorMediaType.V2_JSON, "application/json"); + }); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/RequestMappingEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/RequestMappingEndpointTests.java index ab537695ec..2ce195dce3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/RequestMappingEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/RequestMappingEndpointTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet; +import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -24,6 +25,7 @@ import org.junit.Test; import org.springframework.boot.actuate.endpoint.DefaultEnablement; import org.springframework.boot.actuate.endpoint.EndpointInfo; import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; @@ -142,7 +144,9 @@ public class RequestMappingEndpointTests { WebMvcEndpointHandlerMapping mapping = new WebMvcEndpointHandlerMapping( new EndpointMapping("application"), Collections.singleton(new EndpointInfo<>("test", - DefaultEnablement.ENABLED, Collections.singleton(operation)))); + DefaultEnablement.ENABLED, Collections.singleton(operation))), + new EndpointMediaTypes(Arrays.asList("application/vnd.test+json"), + Arrays.asList("application/vnd.test+json"))); mapping.setApplicationContext(new StaticApplicationContext()); mapping.afterPropertiesSet(); return mapping; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java new file mode 100644 index 0000000000..4e7bf1623e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java @@ -0,0 +1,68 @@ +/* + * 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.actuate.endpoint.web; + +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Media types that are, by default, produced and consumed by an endpoint. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class EndpointMediaTypes { + + private final List produced; + + private final List consumed; + + /** + * Creates a new {@link EndpointMediaTypes} with the given {@code produced} and + * {@code consumed} media types. + * @param produced the default media types that are produced by an endpoint. Must not + * be {@code null}. + * @param consumed the default media types that are consumed by an endpoint. Must not + */ + public EndpointMediaTypes(List produced, List consumed) { + Assert.notNull(produced, () -> "Produced must not be null"); + Assert.notNull(consumed, () -> "Consumed must not be null"); + this.produced = Collections.unmodifiableList(produced); + this.consumed = Collections.unmodifiableList(consumed); + } + + /** + * Returns the media types produced by an endpoint. + * + * @return the produced media types + */ + public List getProduced() { + return this.produced; + } + + /** + * Returns the media types consumed by an endpoint. + * + * @return the consumed media types + */ + public List getConsumed() { + return this.consumed; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebAnnotationEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebAnnotationEndpointDiscoverer.java index b8d4fc5594..d83199ffb5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebAnnotationEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebAnnotationEndpointDiscoverer.java @@ -39,6 +39,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.cache.CachingConfiguration; import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFactory; import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvoker; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; @@ -68,17 +69,16 @@ public class WebAnnotationEndpointDiscoverer extends * @param operationParameterMapper the {@link OperationParameterMapper} used to * convert arguments when an operation is invoked * @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use - * @param consumedMediaTypes the media types consumed by web endpoint operations - * @param producedMediaTypes the media types produced by web endpoint operations + * @param endpointMediaTypes the media types produced and consumed by web endpoint + * operations */ public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext, OperationParameterMapper operationParameterMapper, CachingConfigurationFactory cachingConfigurationFactory, - Collection consumedMediaTypes, - Collection producedMediaTypes) { + EndpointMediaTypes endpointMediaTypes) { super(applicationContext, new WebEndpointOperationFactory(operationParameterMapper, - consumedMediaTypes, producedMediaTypes), + endpointMediaTypes), WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory); } @@ -119,16 +119,12 @@ public class WebAnnotationEndpointDiscoverer extends private final OperationParameterMapper parameterMapper; - private final Collection consumedMediaTypes; - - private final Collection producedMediaTypes; + private final EndpointMediaTypes endpointMediaTypes; private WebEndpointOperationFactory(OperationParameterMapper parameterMapper, - Collection consumedMediaTypes, - Collection producedMediaTypes) { + EndpointMediaTypes endpointMediaTypes) { this.parameterMapper = parameterMapper; - this.consumedMediaTypes = consumedMediaTypes; - this.producedMediaTypes = producedMediaTypes; + this.endpointMediaTypes = endpointMediaTypes; } @Override @@ -172,7 +168,7 @@ public class WebAnnotationEndpointDiscoverer extends private Collection determineConsumedMediaTypes( WebEndpointHttpMethod httpMethod, Method method) { if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) { - return this.consumedMediaTypes; + return this.endpointMediaTypes.getConsumed(); } return Collections.emptyList(); } @@ -189,7 +185,7 @@ public class WebAnnotationEndpointDiscoverer extends if (producesResourceResponseBody(method)) { return Collections.singletonList("application/octet-stream"); } - return this.producedMediaTypes; + return this.endpointMediaTypes.getProduced(); } private boolean producesResourceResponseBody(Method method) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java index e4bd6cb3e5..73c7e519be 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -27,7 +27,6 @@ import java.util.function.Function; import javax.ws.rs.HttpMethod; import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -43,6 +42,7 @@ import org.springframework.boot.actuate.endpoint.OperationInvoker; import org.springframework.boot.actuate.endpoint.ParameterMappingException; import org.springframework.boot.actuate.endpoint.ParametersMissingException; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.Link; import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; @@ -68,18 +68,20 @@ public class JerseyEndpointResourceFactory { * {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints * @param webEndpoints the web endpoints + * @param endpointMediaTypes media types consumed and produced by the endpoints * @return the resources for the operations */ public Collection createEndpointResources(EndpointMapping endpointMapping, - Collection> webEndpoints) { + Collection> webEndpoints, + EndpointMediaTypes endpointMediaTypes) { List resources = new ArrayList<>(); webEndpoints.stream() .flatMap((endpointInfo) -> endpointInfo.getOperations().stream()) .map((operation) -> createResource(endpointMapping, operation)) .forEach(resources::add); if (StringUtils.hasText(endpointMapping.getPath())) { - resources.add( - createEndpointLinksResource(endpointMapping.getPath(), webEndpoints)); + resources.add(createEndpointLinksResource(endpointMapping.getPath(), + webEndpoints, endpointMediaTypes)); } return resources; } @@ -102,10 +104,14 @@ public class JerseyEndpointResourceFactory { } private Resource createEndpointLinksResource(String endpointPath, - Collection> webEndpoints) { + Collection> webEndpoints, + EndpointMediaTypes endpointMediaTypes) { Builder resourceBuilder = Resource.builder().path(endpointPath); - resourceBuilder.addMethod("GET").produces(MediaType.APPLICATION_JSON).handledBy( - new EndpointLinksInflector(webEndpoints, this.endpointLinksResolver)); + resourceBuilder.addMethod("GET") + .produces(endpointMediaTypes.getProduced() + .toArray(new String[endpointMediaTypes.getProduced().size()])) + .handledBy(new EndpointLinksInflector(webEndpoints, + this.endpointLinksResolver)); return resourceBuilder.build(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java index 3e616a4838..3e2324d626 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java @@ -34,6 +34,7 @@ import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.ParameterMappingException; import org.springframework.boot.actuate.endpoint.ParametersMissingException; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.Link; import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; @@ -87,6 +88,8 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp private final Collection> webEndpoints; + private final EndpointMediaTypes endpointMediaTypes; + private final CorsConfiguration corsConfiguration; /** @@ -94,10 +97,12 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp * operations of the given {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints * @param collection the web endpoints + * @param endpointMediaTypes media types consumed and produced by the endpoints */ public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> collection) { - this(endpointMapping, collection, null); + Collection> collection, + EndpointMediaTypes endpointMediaTypes) { + this(endpointMapping, collection, endpointMediaTypes, null); } /** @@ -105,13 +110,15 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp * operations of the given {@code webEndpoints}. * @param endpointMapping the path beneath which all endpoints should be mapped * @param webEndpoints the web endpoints + * @param endpointMediaTypes media types consumed and produced by the endpoints * @param corsConfiguration the CORS configuration for the endpoints */ public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, - CorsConfiguration corsConfiguration) { + EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { this.endpointMapping = endpointMapping; this.webEndpoints = webEndpoints; + this.endpointMediaTypes = endpointMediaTypes; this.corsConfiguration = corsConfiguration; setOrder(-100); } @@ -127,11 +134,18 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp } private void registerLinksMapping() { - registerMapping(new RequestMappingInfo( - new PatternsRequestCondition( - pathPatternParser.parse(this.endpointMapping.getPath())), - new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null, - null, null), this, this.links); + registerMapping( + new RequestMappingInfo( + new PatternsRequestCondition( + pathPatternParser.parse(this.endpointMapping.getPath())), + new RequestMethodsRequestCondition(RequestMethod.GET), null, null, + null, + new ProducesRequestCondition( + this.endpointMediaTypes.getProduced() + .toArray(new String[this.endpointMediaTypes + .getProduced().size()])), + null), + this, this.links); } @Override diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java index 6a7b831778..745cd3357b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.actuate.endpoint.EndpointInfo; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.endpoint.web.EndpointMapping; @@ -56,6 +57,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping private final Collection> webEndpoints; + private final EndpointMediaTypes endpointMediaTypes; + private final CorsConfiguration corsConfiguration; /** @@ -63,10 +66,12 @@ public abstract class AbstractWebMvcEndpointHandlerMapping * operations of the given {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints * @param collection the web endpoints operations + * @param endpointMediaTypes media types consumed and produced by the endpoints */ public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> collection) { - this(endpointMapping, collection, null); + Collection> collection, + EndpointMediaTypes endpointMediaTypes) { + this(endpointMapping, collection, endpointMediaTypes, null); } /** @@ -74,13 +79,15 @@ public abstract class AbstractWebMvcEndpointHandlerMapping * operations of the given {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints * @param webEndpoints the web endpoints + * @param endpointMediaTypes media types consumed and produced by the endpoints * @param corsConfiguration the CORS configuration for the endpoints */ public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, - CorsConfiguration corsConfiguration) { + EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { this.endpointMapping = endpointMapping; this.webEndpoints = webEndpoints; + this.endpointMediaTypes = endpointMediaTypes; this.corsConfiguration = corsConfiguration; setOrder(-100); } @@ -107,8 +114,11 @@ public abstract class AbstractWebMvcEndpointHandlerMapping PatternsRequestCondition patterns = patternsRequestConditionForPattern(""); RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( RequestMethod.GET); + ProducesRequestCondition produces = new ProducesRequestCondition( + this.endpointMediaTypes.getProduced().toArray( + new String[this.endpointMediaTypes.getProduced().size()])); RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, - null, null, null); + null, produces, null); registerMapping(mapping, this, getLinks()); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java index 0de4648a7d..1d8bdca6fa 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java @@ -30,6 +30,7 @@ import org.springframework.boot.actuate.endpoint.OperationInvoker; import org.springframework.boot.actuate.endpoint.ParameterMappingException; import org.springframework.boot.actuate.endpoint.ParametersMissingException; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.Link; import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; @@ -65,10 +66,12 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM * operations of the given {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints * @param collection the web endpoints operations + * @param endpointMediaTypes media types consumed and produced by the endpoints */ public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, - Collection> collection) { - this(endpointMapping, collection, null); + Collection> collection, + EndpointMediaTypes endpointMediaTypes) { + this(endpointMapping, collection, endpointMediaTypes, null); } /** @@ -76,12 +79,13 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM * operations of the given {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints * @param webEndpoints the web endpoints + * @param endpointMediaTypes media types consumed and produced by the endpoints * @param corsConfiguration the CORS configuration for the endpoints */ public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection> webEndpoints, - CorsConfiguration corsConfiguration) { - super(endpointMapping, webEndpoints, corsConfiguration); + EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { + super(endpointMapping, webEndpoints, endpointMediaTypes, corsConfiguration); setOrder(-100); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/AbstractWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/AbstractWebEndpointIntegrationTests.java index e1c02d6d6f..218ce4acca 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/AbstractWebEndpointIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.endpoint.web; import java.time.Duration; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -65,6 +66,10 @@ public abstract class AbstractWebEndpointIntegrationTests exporterConfiguration; protected AbstractWebEndpointIntegrationTests(Class exporterConfiguration) { @@ -74,41 +79,36 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("/test").accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk().expectBody().jsonPath("All") - .isEqualTo(true)); + (client) -> client.get().uri("/test").exchange().expectStatus().isOk() + .expectBody().jsonPath("All").isEqualTo(true)); } @Test public void readOperationWithEndpointsMappedToTheRoot() { load(TestEndpointConfiguration.class, "", - (client) -> client.get().uri("/test").accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk().expectBody().jsonPath("All") - .isEqualTo(true)); + (client) -> client.get().uri("/test").exchange().expectStatus().isOk() + .expectBody().jsonPath("All").isEqualTo(true)); } @Test public void readOperationWithSelector() { load(TestEndpointConfiguration.class, - (client) -> client.get().uri("/test/one") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isOk().expectBody().jsonPath("part").isEqualTo("one")); + (client) -> client.get().uri("/test/one").exchange().expectStatus().isOk() + .expectBody().jsonPath("part").isEqualTo("one")); } @Test public void readOperationWithSelectorContainingADot() { load(TestEndpointConfiguration.class, - (client) -> client.get().uri("/test/foo.bar") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + (client) -> client.get().uri("/test/foo.bar").exchange().expectStatus() .isOk().expectBody().jsonPath("part").isEqualTo("foo.bar")); } @Test public void linksToOtherEndpointsAreProvided() { load(TestEndpointConfiguration.class, - (client) -> client.get().uri("").accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk().expectBody() - .jsonPath("_links.length()").isEqualTo(3) + (client) -> client.get().uri("").exchange().expectStatus().isOk() + .expectBody().jsonPath("_links.length()").isEqualTo(3) .jsonPath("_links.self.href").isNotEmpty() .jsonPath("_links.self.templated").isEqualTo(false) .jsonPath("_links.test.href").isNotEmpty() @@ -120,48 +120,45 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("").accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isNotFound()); + (client) -> client.get().uri("").exchange().expectStatus().isNotFound()); } @Test public void readOperationWithSingleQueryParameters() { load(QueryEndpointConfiguration.class, - (client) -> client.get().uri("/query?one=1&two=2") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isOk().expectBody().jsonPath("query").isEqualTo("1 2")); + (client) -> client.get().uri("/query?one=1&two=2").exchange() + .expectStatus().isOk().expectBody().jsonPath("query") + .isEqualTo("1 2")); } @Test public void readOperationWithSingleQueryParametersAndMultipleValues() { load(QueryEndpointConfiguration.class, - (client) -> client.get().uri("/query?one=1&one=1&two=2") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isOk().expectBody().jsonPath("query").isEqualTo("1,1 2")); + (client) -> client.get().uri("/query?one=1&one=1&two=2").exchange() + .expectStatus().isOk().expectBody().jsonPath("query") + .isEqualTo("1,1 2")); } @Test public void readOperationWithListQueryParameterAndSingleValue() { load(QueryWithListEndpointConfiguration.class, - (client) -> client.get().uri("/query?one=1&two=2") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isOk().expectBody().jsonPath("query").isEqualTo("1 [2]")); + (client) -> client.get().uri("/query?one=1&two=2").exchange() + .expectStatus().isOk().expectBody().jsonPath("query") + .isEqualTo("1 [2]")); } @Test public void readOperationWithListQueryParameterAndMultipleValues() { load(QueryWithListEndpointConfiguration.class, - (client) -> client.get().uri("/query?one=1&two=2&two=2") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isOk().expectBody().jsonPath("query").isEqualTo("1 [2, 2]")); + (client) -> client.get().uri("/query?one=1&two=2&two=2").exchange() + .expectStatus().isOk().expectBody().jsonPath("query") + .isEqualTo("1 [2, 2]")); } @Test public void readOperationWithMappingFailureProducesBadRequestResponse() { - load(QueryEndpointConfiguration.class, - (client) -> client.get().uri("/query?two=two") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isBadRequest()); + load(QueryEndpointConfiguration.class, (client) -> client.get() + .uri("/query?two=two").exchange().expectStatus().isBadRequest()); } @Test @@ -170,16 +167,16 @@ public abstract class AbstractWebEndpointIntegrationTests body = new HashMap<>(); body.put("foo", "one"); body.put("bar", "two"); - client.post().uri("/test").syncBody(body).accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isNoContent().expectBody().isEmpty(); + client.post().uri("/test").syncBody(body).exchange().expectStatus() + .isNoContent().expectBody().isEmpty(); }); } @Test public void writeOperationWithVoidResponse() { load(VoidWriteResponseEndpointConfiguration.class, (context, client) -> { - client.post().uri("/voidwrite").accept(MediaType.APPLICATION_JSON).exchange() - .expectStatus().isNoContent().expectBody().isEmpty(); + client.post().uri("/voidwrite").exchange().expectStatus().isNoContent() + .expectBody().isEmpty(); verify(context.getBean(EndpointDelegate.class)).write(); }); } @@ -187,16 +184,15 @@ public abstract class AbstractWebEndpointIntegrationTests client.delete().uri("/test/one") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + (client) -> client.delete().uri("/test/one").exchange().expectStatus() .isOk().expectBody().jsonPath("part").isEqualTo("one")); } @Test public void deleteOperationWithVoidResponse() { load(VoidDeleteResponseEndpointConfiguration.class, (context, client) -> { - client.delete().uri("/voiddelete").accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isNoContent().expectBody().isEmpty(); + client.delete().uri("/voiddelete").exchange().expectStatus().isNoContent() + .expectBody().isEmpty(); verify(context.getBean(EndpointDelegate.class)).delete(); }); } @@ -206,8 +202,8 @@ public abstract class AbstractWebEndpointIntegrationTests { Map body = new HashMap<>(); body.put("foo", "one"); - client.post().uri("/test").syncBody(body).accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isNoContent().expectBody().isEmpty(); + client.post().uri("/test").syncBody(body).exchange().expectStatus() + .isNoContent().expectBody().isEmpty(); verify(context.getBean(EndpointDelegate.class)).write("one", null); }); } @@ -215,35 +211,28 @@ public abstract class AbstractWebEndpointIntegrationTests { - client.post().uri("/test").contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isNoContent().expectBody().isEmpty(); + client.post().uri("/test").contentType(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isNoContent().expectBody().isEmpty(); verify(context.getBean(EndpointDelegate.class)).write(null, null); }); } @Test public void nullResponseFromReadOperationResultsInNotFoundResponseStatus() { - load(NullReadResponseEndpointConfiguration.class, - (context, client) -> client.get().uri("/nullread") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isNotFound()); + load(NullReadResponseEndpointConfiguration.class, (context, client) -> client + .get().uri("/nullread").exchange().expectStatus().isNotFound()); } @Test public void nullResponseFromDeleteOperationResultsInNoContentResponseStatus() { - load(NullDeleteResponseEndpointConfiguration.class, - (context, client) -> client.delete().uri("/nulldelete") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isNoContent()); + load(NullDeleteResponseEndpointConfiguration.class, (context, client) -> client + .delete().uri("/nulldelete").exchange().expectStatus().isNoContent()); } @Test public void nullResponseFromWriteOperationResultsInNoContentResponseStatus() { - load(NullWriteResponseEndpointConfiguration.class, - (context, client) -> client.post().uri("/nullwrite") - .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() - .isNoContent()); + load(NullWriteResponseEndpointConfiguration.class, (context, client) -> client + .post().uri("/nullwrite").exchange().expectStatus().isNoContent()); } @Test @@ -272,9 +261,8 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("/mono").accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk().expectBody().jsonPath("a") - .isEqualTo("alpha")); + (client) -> client.get().uri("/mono").exchange().expectStatus().isOk() + .expectBody().jsonPath("a").isEqualTo("alpha")); } @Test @@ -298,6 +286,38 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("/test").exchange().expectStatus().isOk() + .expectHeader() + .valueMatches("Content-Type", ACTUATOR_MEDIA_TYPE_PATTERN)); + } + + @Test + public void endpointsProduceSecondaryMediaTypeWhenRequested() { + load(TestEndpointConfiguration.class, + (client) -> client.get().uri("/test").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectHeader() + .valueMatches("Content-Type", JSON_MEDIA_TYPE_PATTERN)); + } + + @Test + public void linksProducesPrimaryMediaTypeByDefault() { + load(TestEndpointConfiguration.class, + (client) -> client.get().uri("").exchange().expectStatus().isOk() + .expectHeader() + .valueMatches("Content-Type", ACTUATOR_MEDIA_TYPE_PATTERN)); + } + + @Test + public void linksProducesSecondaryMediaTypeWhenRequested() { + load(TestEndpointConfiguration.class, + (client) -> client.get().uri("").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectHeader() + .valueMatches("Content-Type", JSON_MEDIA_TYPE_PATTERN)); + } + protected abstract T createApplicationContext(Class... config); protected abstract int getPort(T context); @@ -351,6 +371,13 @@ public abstract class AbstractWebEndpointIntegrationTests mediaTypes = Arrays.asList("application/vnd.test+json", + "application/json"); + return new EndpointMediaTypes(mediaTypes, mediaTypes); + } + @Bean public WebAnnotationEndpointDiscoverer webEndpointDiscoverer( ApplicationContext applicationContext) { @@ -358,8 +385,7 @@ public abstract class AbstractWebEndpointIntegrationTests new CachingConfiguration(0), - Collections.singletonList("application/json"), - Collections.singletonList("application/json")); + endpointMediaTypes()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebAnnotationEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebAnnotationEndpointDiscovererTests.java index c1e2eaaaf0..928df7d99f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebAnnotationEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebAnnotationEndpointDiscovererTests.java @@ -247,12 +247,14 @@ public class WebAnnotationEndpointDiscovererTests { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( configuration); try { - consumer.accept(new WebAnnotationEndpointDiscoverer(context, - new ConversionServiceOperationParameterMapper( - DefaultConversionService.getSharedInstance()), - cachingConfigurationFactory, - Collections.singletonList("application/json"), - Collections.singletonList("application/json"))); + consumer.accept( + new WebAnnotationEndpointDiscoverer(context, + new ConversionServiceOperationParameterMapper( + DefaultConversionService.getSharedInstance()), + cachingConfigurationFactory, + new EndpointMediaTypes( + Collections.singletonList("application/json"), + Collections.singletonList("application/json")))); } finally { context.close(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java index 3c10b699cc..70dcace1ea 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java @@ -28,6 +28,7 @@ import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.servlet.ServletContainer; import org.springframework.boot.actuate.endpoint.web.AbstractWebEndpointIntegrationTests; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -80,12 +81,13 @@ public class JerseyWebEndpointIntegrationTests extends @Bean public ResourceConfig resourceConfig(Environment environment, - WebAnnotationEndpointDiscoverer endpointDiscoverer) { + WebAnnotationEndpointDiscoverer endpointDiscoverer, + EndpointMediaTypes endpointMediaTypes) { ResourceConfig resourceConfig = new ResourceConfig(); Collection resources = new JerseyEndpointResourceFactory() .createEndpointResources( new EndpointMapping(environment.getProperty("endpointPath")), - endpointDiscoverer.discoverEndpoints()); + endpointDiscoverer.discoverEndpoints(), endpointMediaTypes); resourceConfig.registerResources(new HashSet<>(resources)); resourceConfig.register(JacksonFeature.class); resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()), diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java index d4e9e4924f..d454cb8773 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java @@ -21,6 +21,7 @@ import java.util.Arrays; import org.junit.Test; import org.springframework.boot.actuate.endpoint.web.AbstractWebEndpointIntegrationTests; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; @@ -110,13 +111,15 @@ public class WebFluxEndpointIntegrationTests @Bean public WebFluxEndpointHandlerMapping webEndpointHandlerMapping( Environment environment, - WebAnnotationEndpointDiscoverer endpointDiscoverer) { + WebAnnotationEndpointDiscoverer endpointDiscoverer, + EndpointMediaTypes endpointMediaTypes) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); return new WebFluxEndpointHandlerMapping( new EndpointMapping(environment.getProperty("endpointPath")), - endpointDiscoverer.discoverEndpoints(), corsConfiguration); + endpointDiscoverer.discoverEndpoints(), endpointMediaTypes, + corsConfiguration); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java index 01589e8c7f..39e904bce9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java @@ -21,6 +21,7 @@ import java.util.Arrays; import org.junit.Test; import org.springframework.boot.actuate.endpoint.web.AbstractWebEndpointIntegrationTests; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.endpoint.web.EndpointMapping; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -104,13 +105,15 @@ public class MvcWebEndpointIntegrationTests extends @Bean public WebMvcEndpointHandlerMapping webEndpointHandlerMapping( Environment environment, - WebAnnotationEndpointDiscoverer webEndpointDiscoverer) { + WebAnnotationEndpointDiscoverer webEndpointDiscoverer, + EndpointMediaTypes endpointMediaTypes) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com")); corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST")); return new WebMvcEndpointHandlerMapping( new EndpointMapping(environment.getProperty("endpointPath")), - webEndpointDiscoverer.discoverEndpoints(), corsConfiguration); + webEndpointDiscoverer.discoverEndpoints(), endpointMediaTypes, + corsConfiguration); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java index aeb1144412..0d9df61e7b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java @@ -21,6 +21,8 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import javax.ws.rs.core.MediaType; + import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.model.Resource; import org.junit.runners.BlockJUnit4ClassRunner; @@ -28,6 +30,7 @@ import org.junit.runners.model.InitializationError; import org.springframework.boot.actuate.endpoint.convert.ConversionServiceOperationParameterMapper; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -41,7 +44,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; /** * {@link BlockJUnit4ClassRunner} for Jersey. @@ -90,15 +92,17 @@ class JerseyEndpointsRunner extends AbstractWebEndpointRunner { } private void customize(ResourceConfig config) { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, + List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, ActuatorMediaType.V2_JSON); + EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, + mediaTypes); WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer( this.applicationContext, new ConversionServiceOperationParameterMapper(), (id) -> null, - mediaTypes, mediaTypes); + endpointMediaTypes); Collection resources = new JerseyEndpointResourceFactory() .createEndpointResources(new EndpointMapping("/application"), - discoverer.discoverEndpoints()); + discoverer.discoverEndpoints(), endpointMediaTypes); config.registerResources(new HashSet<>(resources)); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java index 76643d70c4..e6bbf51ce0 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java @@ -24,6 +24,7 @@ import org.junit.runners.model.InitializationError; import org.springframework.boot.actuate.endpoint.convert.ConversionServiceOperationParameterMapper; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -99,12 +100,15 @@ class WebFluxEndpointsRunner extends AbstractWebEndpointRunner { public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() { List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON); + EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, + mediaTypes); WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer( this.applicationContext, new ConversionServiceOperationParameterMapper(), (id) -> null, - mediaTypes, mediaTypes); + endpointMediaTypes); return new WebFluxEndpointHandlerMapping(new EndpointMapping("/application"), - discoverer.discoverEndpoints(), new CorsConfiguration()); + discoverer.discoverEndpoints(), endpointMediaTypes, + new CorsConfiguration()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java index d14d03cf2b..3d5c6bf75c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java @@ -24,6 +24,7 @@ import org.junit.runners.model.InitializationError; import org.springframework.boot.actuate.endpoint.convert.ConversionServiceOperationParameterMapper; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -82,12 +83,15 @@ class WebMvcEndpointRunner extends AbstractWebEndpointRunner { public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() { List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON); + EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, + mediaTypes); WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer( this.applicationContext, new ConversionServiceOperationParameterMapper(), (id) -> null, - mediaTypes, mediaTypes); + endpointMediaTypes); return new WebMvcEndpointHandlerMapping(new EndpointMapping("/application"), - discoverer.discoverEndpoints(), new CorsConfiguration()); + discoverer.discoverEndpoints(), endpointMediaTypes, + new CorsConfiguration()); } }