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()); } }