diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java index e54837ad14..ea5eac25d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java @@ -16,10 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; @@ -28,7 +26,6 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludeProp import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointsSupplier; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -67,8 +64,6 @@ import org.springframework.context.annotation.Configuration; @EnableConfigurationProperties(WebEndpointProperties.class) public class WebEndpointAutoConfiguration { - private static final List MEDIA_TYPES = Arrays.asList(ActuatorMediaType.V2_JSON, "application/json"); - private final ApplicationContext applicationContext; private final WebEndpointProperties properties; @@ -86,7 +81,7 @@ public class WebEndpointAutoConfiguration { @Bean @ConditionalOnMissingBean public EndpointMediaTypes endpointMediaTypes() { - return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES); + return EndpointMediaTypes.DEFAULT; } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java index dea8001a8d..eaeed62e86 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java @@ -98,7 +98,7 @@ class CloudFoundryActuatorAutoConfigurationTests { "vcap.application.cf_api:https://my-cloud-controller.com").run((context) -> { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); mockMvc.perform(get("/cloudfoundryapplication")) - .andExpect(header().string("Content-Type", ActuatorMediaType.V2_JSON)); + .andExpect(header().string("Content-Type", ActuatorMediaType.V3_JSON)); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java index f5b564d50d..89fc5ae40c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java @@ -61,7 +61,8 @@ class WebEndpointAutoConfigurationTests { void webApplicationConfiguresEndpointMediaTypes() { this.contextRunner.run((context) -> { EndpointMediaTypes endpointMediaTypes = context.getBean(EndpointMediaTypes.class); - assertThat(endpointMediaTypes.getConsumed()).containsExactly(ActuatorMediaType.V2_JSON, "application/json"); + assertThat(endpointMediaTypes.getConsumed()).containsExactly(ActuatorMediaType.V3_JSON, + ActuatorMediaType.V2_JSON, "application/json"); }); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java index 1ea4430f6f..8c80225412 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint; import java.util.Map; +import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.util.Assert; @@ -25,6 +26,7 @@ import org.springframework.util.Assert; * The context for the {@link OperationInvoker invocation of an operation}. * * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ public class InvocationContext { @@ -33,23 +35,55 @@ public class InvocationContext { private final Map arguments; + private final ApiVersion apiVersion; + /** - * Creates a new context for an operation being invoked by the given {@code principal} - * with the given available {@code arguments}. + * Creates a new context for an operation being invoked by the given + * {@code securityContext} with the given available {@code arguments}. * @param securityContext the current security context. Never {@code null} * @param arguments the arguments available to the operation. Never {@code null} */ public InvocationContext(SecurityContext securityContext, Map arguments) { + this(null, securityContext, arguments); + } + + /** + * Creates a new context for an operation being invoked by the given + * {@code securityContext} with the given available {@code arguments}. + * @param apiVersion the API version or {@code null} to use the latest + * @param securityContext the current security context. Never {@code null} + * @param arguments the arguments available to the operation. Never {@code null} + * @since 2.2.0 + */ + public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map arguments) { Assert.notNull(securityContext, "SecurityContext must not be null"); Assert.notNull(arguments, "Arguments must not be null"); + this.apiVersion = (apiVersion != null) ? apiVersion : ApiVersion.LATEST; this.securityContext = securityContext; this.arguments = arguments; } + /** + * Return the API version in use. + * @return the apiVersion the API version + * @since 2.2.0 + */ + public ApiVersion getApiVersion() { + return this.apiVersion; + } + + /** + * Return the security context to use for the invocation. + * @return the security context + */ public SecurityContext getSecurityContext() { return this.securityContext; } + /** + * Return the invocation arguments. + * @return the arguments + */ public Map getArguments() { return this.arguments; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java index 596cb8dd09..831f4e0c4b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java @@ -27,14 +27,21 @@ public final class ActuatorMediaType { /** * Constant for the Actuator V1 media type. + * @deprecated since 2.2.0 as the v1 format is no longer supported */ + @Deprecated public static final String V1_JSON = "application/vnd.spring-boot.actuator.v1+json"; /** - * Constant for the Actuator V2 media type. + * Constant for the Actuator {@link ApiVersion#V2 v2} media type. */ public static final String V2_JSON = "application/vnd.spring-boot.actuator.v2+json"; + /** + * Constant for the Actuator {@link ApiVersion#V3 v3} media type. + */ + public static final String V3_JSON = "application/vnd.spring-boot.actuator.v3+json"; + private ActuatorMediaType() { } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java new file mode 100644 index 0000000000..6267ef8f85 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.http; + +import java.util.List; +import java.util.Map; + +import org.springframework.util.CollectionUtils; +import org.springframework.util.MimeTypeUtils; + +/** + * API versions supported for the actuator HTTP API. This enum may be injected into + * actuator endpoints in order to return a response compatible with the requested version. + * + * @author Phillip Webb + * @since 2.2.0 + */ +public enum ApiVersion { + + /** + * Version 2 (supported by Spring Boot 2.0+). + */ + V2, + + /** + * Version 3 (supported by Spring Boot 2.2+). + */ + V3; + + private static final String MEDIA_TYPE_PREFIX = "application/vnd.spring-boot.actuator."; + + /** + * The latest API version. + */ + public static final ApiVersion LATEST = ApiVersion.V3; + + /** + * Return the {@link ApiVersion} to use based on the HTTP request headers. The version + * will be deduced based on the {@code Accept} header. + * @param headers the HTTP headers + * @return the API version to use + */ + public static ApiVersion fromHttpHeaders(Map> headers) { + ApiVersion version = null; + List accepts = headers.get("Accept"); + if (!CollectionUtils.isEmpty(accepts)) { + for (String accept : accepts) { + for (String type : MimeTypeUtils.tokenize(accept)) { + version = mostRecent(version, forType(type)); + } + } + } + return (version != null) ? version : LATEST; + } + + private static ApiVersion forType(String type) { + if (type.startsWith(MEDIA_TYPE_PREFIX)) { + type = type.substring(MEDIA_TYPE_PREFIX.length()); + int suffixIndex = type.indexOf("+"); + type = (suffixIndex != -1) ? type.substring(0, suffixIndex) : type; + try { + return valueOf(type.toUpperCase()); + } + catch (IllegalArgumentException ex) { + } + } + return null; + } + + private static ApiVersion mostRecent(ApiVersion existing, ApiVersion candidate) { + int existingOrdinal = (existing != null) ? existing.ordinal() : -1; + int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1; + return (candidateOrdinal > existingOrdinal) ? candidate : existing; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java index dbb74b3ce3..38137d9ee1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; @@ -88,6 +89,9 @@ public class ReflectiveOperationInvoker implements OperationInvoker { if (!parameter.isMandatory()) { return false; } + if (ApiVersion.class.equals(parameter.getType())) { + return false; + } if (Principal.class.equals(parameter.getType())) { return context.getSecurityContext().getPrincipal() == null; } @@ -103,6 +107,9 @@ public class ReflectiveOperationInvoker implements OperationInvoker { } private Object resolveArgument(OperationParameter parameter, InvocationContext context) { + if (ApiVersion.class.equals(parameter.getType())) { + return context.getApiVersion(); + } if (Principal.class.equals(parameter.getType())) { return context.getSecurityContext().getPrincipal(); } 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 index 7141a3bddc..0a7e46d3a2 100644 --- 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 @@ -16,9 +16,11 @@ package org.springframework.boot.actuate.endpoint.web; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.util.Assert; /** @@ -29,10 +31,40 @@ import org.springframework.util.Assert; */ public class EndpointMediaTypes { + private static final String JSON_MEDIA_TYPE = "application/json"; + + /** + * Default {@link EndpointMediaTypes} for this version of Spring Boot. + */ + public static final EndpointMediaTypes DEFAULT = new EndpointMediaTypes(ActuatorMediaType.V3_JSON, + ActuatorMediaType.V2_JSON, JSON_MEDIA_TYPE); + private final List produced; private final List consumed; + /** + * Creates a new {@link EndpointMediaTypes} with the given {@code produced} and + * {@code consumed} media types. + * @param producedAndConsumed the default media types that are produced and consumed + * by an endpoint. Must not be {@code null}. + * @since 2.2.0 + */ + public EndpointMediaTypes(String... producedAndConsumed) { + this((producedAndConsumed != null) ? Arrays.asList(producedAndConsumed) : (List) null); + } + + /** + * Creates a new {@link EndpointMediaTypes} with the given {@code produced} and + * {@code consumed} media types. + * @param producedAndConsumed the default media types that are produced and consumed + * by an endpoint. Must not be {@code null}. + * @since 2.2.0 + */ + public EndpointMediaTypes(List producedAndConsumed) { + this(producedAndConsumed, producedAndConsumed); + } + /** * Creates a new {@link EndpointMediaTypes} with the given {@code produced} and * {@code consumed} media types. 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 bb6009e125..6c86907581 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 @@ -43,6 +43,7 @@ import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -150,8 +151,10 @@ public class JerseyEndpointResourceFactory { arguments.putAll(extractPathParameters(data)); arguments.putAll(extractQueryParameters(data)); try { - Object response = this.operation - .invoke(new InvocationContext(new JerseySecurityContext(data.getSecurityContext()), arguments)); + ApiVersion apiVersion = ApiVersion.fromHttpHeaders(data.getHeaders()); + JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext()); + InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments); + Object response = this.operation.invoke(invocationContext); return convertToJaxRsResponse(response, data.getRequest().getMethod()); } catch (InvalidEndpointRequestException ex) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java index 0744608d2c..a9aaff3664 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java @@ -33,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -309,6 +310,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi @Override public Mono> handle(ServerWebExchange exchange, Map body) { + ApiVersion apiVersion = ApiVersion.fromHttpHeaders(exchange.getRequest().getHeaders()); Map arguments = getArguments(exchange, body); String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate() .getMatchAllRemainingPathSegmentsVariable(); @@ -317,7 +319,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi tokenizePathSegments((String) arguments.get(matchAllRemainingPathSegmentsVariable))); } return this.securityContextSupplier.get() - .map((securityContext) -> new InvocationContext(securityContext, arguments)) + .map((securityContext) -> new InvocationContext(apiVersion, securityContext, arguments)) .flatMap((invocationContext) -> handleResult((Publisher) this.invoker.invoke(invocationContext), exchange.getRequest().getMethod())); } 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 abbba127ec..0b7f997051 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 @@ -33,6 +33,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -40,9 +41,11 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebOperation; import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -293,11 +296,13 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin @Override public Object handle(HttpServletRequest request, @RequestBody(required = false) Map body) { + HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders(); Map arguments = getArguments(request, body); try { - return handleResult( - this.operation.invoke(new InvocationContext(new ServletSecurityContext(request), arguments)), - HttpMethod.valueOf(request.getMethod())); + ApiVersion apiVersion = ApiVersion.fromHttpHeaders(headers); + ServletSecurityContext securityContext = new ServletSecurityContext(request); + InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments); + return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod())); } catch (InvalidEndpointRequestException ex) { throw new BadOperationRequestException(ex.getReason()); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/InvocationContextTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/InvocationContextTests.java new file mode 100644 index 0000000000..b71184c109 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/InvocationContextTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2019 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 + * + * https://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; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.http.ApiVersion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link InvocationContext}. + * + * @author Phillip Webb + */ +class InvocationContextTests { + + private final SecurityContext securityContext = mock(SecurityContext.class); + + private final Map arguments = Collections.singletonMap("test", "value"); + + @Test + void createWhenApiVersionIsNullUsesLatestVersion() { + InvocationContext context = new InvocationContext(null, this.securityContext, this.arguments); + assertThat(context.getApiVersion()).isEqualTo(ApiVersion.LATEST); + } + + @Test + void createWhenSecurityContextIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new InvocationContext(null, this.arguments)) + .withMessage("SecurityContext must not be null"); + } + + @Test + void createWhenArgumentsIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new InvocationContext(this.securityContext, null)) + .withMessage("Arguments must not be null"); + } + + @Test + void getApiVersionReturnsApiVersion() { + InvocationContext context = new InvocationContext(ApiVersion.V2, this.securityContext, this.arguments); + assertThat(context.getApiVersion()).isEqualTo(ApiVersion.V2); + } + + @Test + void getSecurityContextReturnsSecurityContext() { + InvocationContext context = new InvocationContext(this.securityContext, this.arguments); + assertThat(context.getSecurityContext()).isEqualTo(this.securityContext); + } + + @Test + void getArgumentsReturnsArguments() { + InvocationContext context = new InvocationContext(this.securityContext, this.arguments); + assertThat(context.getArguments()).isEqualTo(this.arguments); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/http/ApiVersionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/http/ApiVersionTests.java new file mode 100644 index 0000000000..45408d2158 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/http/ApiVersionTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.http; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link ApiVersion}. + * + * @author Phillip Webb + */ +class ApiVersionTests { + + @Test + void latestIsLatestVersion() { + ApiVersion[] values = ApiVersion.values(); + assertThat(ApiVersion.LATEST).isEqualTo(values[values.length - 1]); + } + + @Test + void fromHttpHeadersWhenEmptyReturnsLatest() { + ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap()); + assertThat(version).isEqualTo(ApiVersion.V3); + } + + @Test + void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() { + ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON)); + assertThat(version).isEqualTo(ApiVersion.V2); + } + + @Test + void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() { + ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON)); + assertThat(version).isEqualTo(ApiVersion.V3); + } + + @Test + void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() { + ApiVersion version = ApiVersion + .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON)); + assertThat(version).isEqualTo(ApiVersion.V3); + } + + @Test + void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() { + ApiVersion version = ApiVersion + .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON)); + assertThat(version).isEqualTo(ApiVersion.V3); + } + + @Test + void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() { + ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2")); + assertThat(version).isEqualTo(ApiVersion.V2); + } + + @Test + void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() { + ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200")); + assertThat(version).isEqualTo(ApiVersion.V3); + } + + private Map> acceptHeader(String... types) { + List value = Arrays.asList(types); + return value.isEmpty() ? Collections.emptyMap() : Collections.singletonMap("Accept", value); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java index 8fe3adc03e..11eb5bae88 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.lang.Nullable; @@ -50,8 +51,8 @@ class ReflectiveOperationInvokerTests { @BeforeEach void setup() { this.target = new Example(); - this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse", String.class), - OperationType.READ); + this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse", + ApiVersion.class, SecurityContext.class, String.class), OperationType.READ); this.parameterValueMapper = (parameter, value) -> (value != null) ? value.toString() : null; } @@ -95,8 +96,8 @@ class ReflectiveOperationInvokerTests { @Test void invokeWhenMissingNullableArgumentShouldInvoke() { - OperationMethod operationMethod = new OperationMethod( - ReflectionUtils.findMethod(Example.class, "reverseNullable", String.class), OperationType.READ); + OperationMethod operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, + "reverseNullable", ApiVersion.class, SecurityContext.class, String.class), OperationType.READ); ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, operationMethod, this.parameterValueMapper); Object result = invoker @@ -115,11 +116,15 @@ class ReflectiveOperationInvokerTests { static class Example { - String reverse(String name) { + String reverse(ApiVersion apiVersion, SecurityContext securityContext, String name) { + assertThat(apiVersion).isEqualTo(ApiVersion.LATEST); + assertThat(securityContext).isNotNull(); return new StringBuilder(name).reverse().toString(); } - String reverseNullable(@Nullable String name) { + String reverseNullable(ApiVersion apiVersion, SecurityContext securityContext, @Nullable String name) { + assertThat(apiVersion).isEqualTo(ApiVersion.LATEST); + assertThat(securityContext).isNotNull(); return new StringBuilder(String.valueOf(name)).reverse().toString(); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java index 2874f85014..cee1d83bd9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java @@ -22,6 +22,8 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -32,6 +34,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException */ class EndpointMediaTypesTests { + @Test + void defaultReturnsExpectedProducedAndConsumedTypes() { + assertThat(EndpointMediaTypes.DEFAULT.getProduced()).containsExactly(ActuatorMediaType.V3_JSON, + ActuatorMediaType.V2_JSON, "application/json"); + assertThat(EndpointMediaTypes.DEFAULT.getConsumed()).containsExactly(ActuatorMediaType.V3_JSON, + ActuatorMediaType.V2_JSON, "application/json"); + } + @Test void createWhenProducedIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new EndpointMediaTypes(null, Collections.emptyList())) @@ -44,6 +54,13 @@ class EndpointMediaTypesTests { .withMessageContaining("Consumed must not be null"); } + @Test + void createFromProducedAndConsumedUsesSameListForBoth() { + EndpointMediaTypes types = new EndpointMediaTypes("spring/framework", "spring/boot"); + assertThat(types.getProduced()).containsExactly("spring/framework", "spring/boot"); + assertThat(types.getConsumed()).containsExactly("spring/framework", "spring/boot"); + } + @Test void getProducedShouldReturnProduced() { List produced = Arrays.asList("a", "b", "c"); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java index a409302b36..41bf39c07f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java @@ -17,7 +17,6 @@ package org.springframework.boot.actuate.endpoint.web.test; import java.time.Duration; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -38,7 +37,6 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; @@ -68,7 +66,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.http.MediaType; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.ClassUtils; @@ -233,9 +230,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation } private void customize(ResourceConfig config) { - List mediaTypes = Arrays.asList(javax.ws.rs.core.MediaType.APPLICATION_JSON, - ActuatorMediaType.V2_JSON); - EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); + EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT; WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList()); @@ -281,8 +276,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation @Bean WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON); - EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); + EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT; WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList()); @@ -311,8 +305,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation @Bean WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON); - EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes); + EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT; WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), Collections.emptyList());