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 a5674c0536..8050419613 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 @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.function.Supplier; import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; @@ -58,11 +57,11 @@ public class InvocationContext { * @param arguments the arguments available to the operation. Never {@code null} * @since 2.2.0 * @deprecated since 2.5.0 in favor of - * {@link #InvocationContext(SecurityContext, Map, List)} + * {@link #InvocationContext(SecurityContext, Map, OperationArgumentResolver[])} */ @Deprecated public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map arguments) { - this(securityContext, arguments, Arrays.asList(new FixedValueArgumentResolver<>(ApiVersion.class, apiVersion))); + this(securityContext, arguments, OperationArgumentResolver.of(ApiVersion.class, () -> apiVersion)); } /** @@ -74,17 +73,17 @@ public class InvocationContext { * the operation. */ public InvocationContext(SecurityContext securityContext, Map arguments, - List argumentResolvers) { + OperationArgumentResolver... argumentResolvers) { Assert.notNull(securityContext, "SecurityContext must not be null"); Assert.notNull(arguments, "Arguments must not be null"); this.arguments = arguments; this.argumentResolvers = new ArrayList<>(); if (argumentResolvers != null) { - this.argumentResolvers.addAll(argumentResolvers); + this.argumentResolvers.addAll(Arrays.asList(argumentResolvers)); } - this.argumentResolvers.add(new FixedValueArgumentResolver<>(SecurityContext.class, securityContext)); - this.argumentResolvers.add(new SuppliedValueArgumentResolver<>(Principal.class, securityContext::getPrincipal)); - this.argumentResolvers.add(new FixedValueArgumentResolver<>(ApiVersion.class, ApiVersion.LATEST)); + this.argumentResolvers.add(OperationArgumentResolver.of(SecurityContext.class, () -> securityContext)); + this.argumentResolvers.add(OperationArgumentResolver.of(Principal.class, securityContext::getPrincipal)); + this.argumentResolvers.add(OperationArgumentResolver.of(ApiVersion.class, () -> ApiVersion.LATEST)); } /** @@ -154,52 +153,4 @@ public class InvocationContext { return false; } - private static final class FixedValueArgumentResolver implements OperationArgumentResolver { - - private final Class argumentType; - - private final T value; - - private FixedValueArgumentResolver(Class argumentType, T value) { - this.argumentType = argumentType; - this.value = value; - } - - @SuppressWarnings("unchecked") - @Override - public U resolve(Class type) { - return (U) this.value; - } - - @Override - public boolean canResolve(Class type) { - return this.argumentType.equals(type); - } - - } - - private static final class SuppliedValueArgumentResolver implements OperationArgumentResolver { - - private final Class argumentType; - - private final Supplier value; - - private SuppliedValueArgumentResolver(Class argumentType, Supplier value) { - this.argumentType = argumentType; - this.value = value; - } - - @SuppressWarnings("unchecked") - @Override - public U resolve(Class type) { - return (U) this.value.get(); - } - - @Override - public boolean canResolve(Class type) { - return this.argumentType.equals(type); - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java index 1e7f0ae6a1..9ade2ec523 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java @@ -16,6 +16,10 @@ package org.springframework.boot.actuate.endpoint; +import java.util.function.Supplier; + +import org.springframework.util.Assert; + /** * Resolver for an argument of an {@link Operation}. * @@ -24,6 +28,14 @@ package org.springframework.boot.actuate.endpoint; */ public interface OperationArgumentResolver { + /** + * Return whether an argument of the given {@code type} can be resolved. + * @param type argument type + * @return {@code true} if an argument of the required type can be resolved, otherwise + * {@code false} + */ + boolean canResolve(Class type); + /** * Resolves an argument of the given {@code type}. * @param required type of the argument @@ -33,11 +45,30 @@ public interface OperationArgumentResolver { T resolve(Class type); /** - * Return whether an argument of the given {@code type} can be resolved. - * @param type argument type - * @return {@code true} if an argument of the required type can be resolved, otherwise - * {@code false} + * Factory method that creates an {@link OperationArgumentResolver} for a specific + * type using a {@link Supplier}. + * @param the resolvable type + * @param type the resolvable type + * @param supplier the value supplier + * @return an {@link OperationArgumentResolver} instance */ - boolean canResolve(Class type); + static OperationArgumentResolver of(Class type, Supplier supplier) { + Assert.notNull(type, "Type must not be null"); + Assert.notNull(supplier, "Supplier must not be null"); + return new OperationArgumentResolver() { + + @Override + public boolean canResolve(Class actualType) { + return actualType.equals(type); + } + + @Override + @SuppressWarnings("unchecked") + public R resolve(Class argumentType) { + return (R) supplier.get(); + } + + }; + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java index e30afe7912..ddef5bc00a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -40,4 +40,11 @@ public @interface DeleteOperation { */ String[] produces() default {}; + /** + * The media types of the result of the operation. + * @return the media types + */ + @SuppressWarnings("rawtypes") + Class producesFrom() default Producible.class; + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java index 89d216035f..8f7b81ff40 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.endpoint.annotation; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -40,8 +41,32 @@ public class DiscoveredOperationMethod extends OperationMethod { AnnotationAttributes annotationAttributes) { super(method, operationType); Assert.notNull(annotationAttributes, "AnnotationAttributes must not be null"); - String[] produces = annotationAttributes.getStringArray("produces"); - this.producesMediaTypes = Collections.unmodifiableList(Arrays.asList(produces)); + List producesMediaTypes = new ArrayList<>(); + producesMediaTypes.addAll(Arrays.asList(annotationAttributes.getStringArray("produces"))); + producesMediaTypes.addAll(getProducesFromProducable(annotationAttributes)); + this.producesMediaTypes = Collections.unmodifiableList(producesMediaTypes); + } + + private & Producible> List getProducesFromProducable( + AnnotationAttributes annotationAttributes) { + Class type = getProducesFrom(annotationAttributes); + if (type == Producible.class) { + return Collections.emptyList(); + } + List produces = new ArrayList<>(); + for (Object value : type.getEnumConstants()) { + produces.add(((Producible) value).getProducedMimeType().toString()); + } + return produces; + } + + private Class getProducesFrom(AnnotationAttributes annotationAttributes) { + try { + return annotationAttributes.getClass("producesFrom"); + } + catch (IllegalArgumentException ex) { + return Producible.class; + } } public List getProducesMediaTypes() { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/Producible.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/Producible.java similarity index 52% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/Producible.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/Producible.java index 3cfbc84f63..b699bdcadc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/Producible.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/Producible.java @@ -14,15 +14,23 @@ * limitations under the License. */ -package org.springframework.boot.actuate.endpoint.http; +package org.springframework.boot.actuate.endpoint.annotation; import org.springframework.util.MimeType; /** - * Interface to be implemented by an {@link Enum} that can be injected into an operation - * on a web endpoint. The value of the {@code Producible} enum is resolved using the - * {@code Accept} header of the request. When multiple values are equally acceptable, the - * value with the highest {@link Enum#ordinal() ordinal} is used. + * Interface that can be implemented by any {@link Enum} that represents a finite set of + * producible mime-types. + *

+ * Can be used with {@link ReadOperation @ReadOperation}, + * {@link WriteOperation @ReadOperation} and {@link DeleteOperation @ReadOperation} + * annotations to quickly define a list of {@code produces} values. + *

+ * {@link Producible} types can also be injected into operations when the underlying + * technology supports content negotiation. For example, with web based endpoints, the + * value of the {@code Producible} enum is resolved using the {@code Accept} header of the + * request. When multiple values are equally acceptable, the value with the highest + * {@link Enum#ordinal() ordinal} is used. * * @param enum type that implements this interface * @author Andy Wilkinson @@ -34,6 +42,6 @@ public interface Producible & Producible> { * Mime type that can be produced. * @return the producible mime type */ - MimeType getMimeType(); + MimeType getProducedMimeType(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java index 4fa2c47301..0ba9e846ea 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -39,4 +39,11 @@ public @interface ReadOperation { */ String[] produces() default {}; + /** + * The media types of the result of the operation. + * @return the media types + */ + @SuppressWarnings("rawtypes") + Class producesFrom() default Producible.class; + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java index a12e4d9cfe..ab2ca7fc6a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -39,4 +39,11 @@ public @interface WriteOperation { */ String[] produces() default {}; + /** + * The media types of the result of the operation. + * @return the media types + */ + @SuppressWarnings("rawtypes") + Class producesFrom() default Producible.class; + } 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 index 5a5a7bcdeb..052746ab83 100644 --- 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 @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.http; import java.util.List; import java.util.Map; +import org.springframework.boot.actuate.endpoint.annotation.Producible; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -49,12 +50,26 @@ public enum ApiVersion implements Producible { */ public static final ApiVersion LATEST = ApiVersion.V3; + private final MimeType mimeType; + + ApiVersion(String mimeType) { + this.mimeType = MimeTypeUtils.parseMimeType(mimeType); + } + + @Override + public MimeType getProducedMimeType() { + return this.mimeType; + } + /** * 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 + * @deprecated since 2.5.0 in favor of direct injection with resolution via the + * {@link ProducibleOperationArgumentResolver}. */ + @Deprecated public static ApiVersion fromHttpHeaders(Map> headers) { ApiVersion version = null; List accepts = headers.get("Accept"); @@ -88,15 +103,4 @@ public enum ApiVersion implements Producible { return (candidateOrdinal > existingOrdinal) ? candidate : existing; } - private final MimeType mimeType; - - ApiVersion(String mimeType) { - this.mimeType = MimeTypeUtils.parseMimeType(mimeType); - } - - @Override - public MimeType getMimeType() { - return this.mimeType; - } - } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ProducibleOperationArgumentResolver.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ProducibleOperationArgumentResolver.java index 7383197d7d..6f2ce67c56 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ProducibleOperationArgumentResolver.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ProducibleOperationArgumentResolver.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import org.springframework.boot.actuate.endpoint.OperationArgumentResolver; +import org.springframework.boot.actuate.endpoint.annotation.Producible; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -34,10 +35,10 @@ import org.springframework.util.MimeTypeUtils; */ public class ProducibleOperationArgumentResolver implements OperationArgumentResolver { - private final Map> httpHeaders; + private final Map> headers; - public ProducibleOperationArgumentResolver(Map> httpHeaders) { - this.httpHeaders = httpHeaders; + public ProducibleOperationArgumentResolver(Map> headers) { + this.headers = headers; } @Override @@ -52,7 +53,7 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes } private Enum> resolveProducible(Class>> type) { - List accepts = this.httpHeaders.get("Accept"); + List accepts = this.headers.get("Accept"); List>> values = Arrays.asList(type.getEnumConstants()); Collections.reverse(values); if (CollectionUtils.isEmpty(accepts)) { @@ -69,15 +70,15 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes private static Enum> mostRecent(Enum> existing, Enum> candidate) { - int existingOrdinal = (existing != null) ? ((Enum) existing).ordinal() : -1; - int candidateOrdinal = (candidate != null) ? ((Enum) candidate).ordinal() : -1; + int existingOrdinal = (existing != null) ? existing.ordinal() : -1; + int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1; return (candidateOrdinal > existingOrdinal) ? candidate : existing; } private static Enum> forType(List>> candidates, MimeType mimeType) { for (Enum> candidate : candidates) { - if (mimeType.isCompatibleWith(((Producible) candidate).getMimeType())) { + if (mimeType.isCompatibleWith(((Producible) candidate).getProducedMimeType())) { return candidate; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java index 385df34cd9..6e673b8bcf 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.endpoint.web; +import org.springframework.boot.actuate.endpoint.annotation.Producible; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; import org.springframework.util.MimeType; @@ -98,6 +99,17 @@ public final class WebEndpointResponse { this(body, STATUS_OK); } + /** + * Creates a new {@code WebEndpointResponse} with the given body and content type and + * a 200 (OK) status. + * @param body the body + * @param producible the producible providing the content type + * @since 2.5.0 + */ + public WebEndpointResponse(T body, Producible producible) { + this(body, STATUS_OK, producible.getProducedMimeType()); + } + /** * Creates a new {@code WebEndpointResponse} with the given body and content type and * a 200 (OK) status. 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 f38a435739..abaa523ed2 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 @@ -21,7 +21,6 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -154,7 +153,7 @@ public class JerseyEndpointResourceFactory { try { JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext()); InvocationContext invocationContext = new InvocationContext(securityContext, arguments, - Arrays.asList(new ProducibleOperationArgumentResolver(data.getHeaders()))); + new ProducibleOperationArgumentResolver(data.getHeaders())); Object response = this.operation.invoke(invocationContext); return convertToJaxRsResponse(response, data.getRequest().getMethod()); } 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 8bf3c3b907..3f9bd608a1 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 @@ -19,7 +19,6 @@ package org.springframework.boot.actuate.endpoint.web.reactive; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.Principal; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -308,7 +307,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi } return this.securityContextSupplier.get() .map((securityContext) -> new InvocationContext(securityContext, arguments, - Arrays.asList(new ProducibleOperationArgumentResolver(exchange.getRequest().getHeaders())))) + new ProducibleOperationArgumentResolver(exchange.getRequest().getHeaders()))) .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 9b330fc928..ca76684778 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 @@ -287,7 +287,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin try { ServletSecurityContext securityContext = new ServletSecurityContext(request); InvocationContext invocationContext = new InvocationContext(securityContext, arguments, - Arrays.asList(new ProducibleOperationArgumentResolver(headers))); + new ProducibleOperationArgumentResolver(headers)); return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod())); } catch (InvalidEndpointRequestException ex) { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java index 0cbcd19b71..3d09ea6460 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.MimeType; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -48,12 +49,35 @@ class DiscoveredOperationMethodTests { AnnotationAttributes annotationAttributes = new AnnotationAttributes(); String[] produces = new String[] { "application/json" }; annotationAttributes.put("produces", produces); + annotationAttributes.put("producesFrom", Producible.class); DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ, annotationAttributes); assertThat(discovered.getProducesMediaTypes()).containsExactly("application/json"); } + @Test + void getProducesMediaTypesWhenProducesFromShouldReturnMediaTypes() { + Method method = ReflectionUtils.findMethod(getClass(), "example"); + AnnotationAttributes annotationAttributes = new AnnotationAttributes(); + annotationAttributes.put("produces", new String[0]); + annotationAttributes.put("producesFrom", ExampleProducible.class); + DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ, + annotationAttributes); + assertThat(discovered.getProducesMediaTypes()).containsExactly("one/*", "two/*", "three/*"); + } + void example() { } + enum ExampleProducible implements Producible { + + ONE, TWO, THREE; + + @Override + public MimeType getProducedMimeType() { + return new MimeType(toString().toLowerCase()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java index 033c40d346..b6552f3111 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -34,6 +34,7 @@ import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; +import org.springframework.util.MimeType; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -121,6 +122,14 @@ class DiscoveredOperationsFactoryTests { assertThat(advisor.getParameters()).isEmpty(); } + @Test + void createOperationShouldApplyProducesFrom() { + TestOperation operation = getFirst( + this.factory.createOperations(EndpointId.of("test"), new ExampleWithProducesFrom())); + DiscoveredOperationMethod method = (DiscoveredOperationMethod) operation.getOperationMethod(); + assertThat(method.getProducesMediaTypes()).containsExactly("one/*", "two/*", "three/*"); + } + private T getFirst(Iterable iterable) { return iterable.iterator().next(); } @@ -175,6 +184,15 @@ class DiscoveredOperationsFactoryTests { } + static class ExampleWithProducesFrom { + + @ReadOperation(producesFrom = ExampleProducible.class) + String read() { + return "read"; + } + + } + static class TestDiscoveredOperationsFactory extends DiscoveredOperationsFactory { TestDiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper, @@ -229,4 +247,15 @@ class DiscoveredOperationsFactoryTests { } + enum ExampleProducible implements Producible { + + ONE, TWO, THREE; + + @Override + public MimeType getProducedMimeType() { + return new MimeType(toString().toLowerCase()); + } + + } + } 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 index 34f30643cf..933b7a4ab5 100644 --- 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 @@ -39,24 +39,28 @@ class ApiVersionTests { } @Test + @Deprecated void fromHttpHeadersWhenEmptyReturnsLatest() { ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap()); assertThat(version).isEqualTo(ApiVersion.V3); } @Test + @Deprecated void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON)); assertThat(version).isEqualTo(ApiVersion.V2); } @Test + @Deprecated void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON)); assertThat(version).isEqualTo(ApiVersion.V3); } @Test + @Deprecated void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() { ApiVersion version = ApiVersion .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON)); @@ -64,6 +68,7 @@ class ApiVersionTests { } @Test + @Deprecated void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() { ApiVersion version = ApiVersion .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON)); @@ -71,18 +76,21 @@ class ApiVersionTests { } @Test + @Deprecated void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2")); assertThat(version).isEqualTo(ApiVersion.V2); } @Test + @Deprecated void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200")); assertThat(version).isEqualTo(ApiVersion.V3); } @Test + @Deprecated void fromHttpHeadersWhenAcceptsEverythingReturnsLatest() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("*/*")); assertThat(version).isEqualTo(ApiVersion.V3); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java index 9583036db9..36a951e807 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java @@ -18,7 +18,6 @@ package org.springframework.boot.actuate.endpoint.invoker.cache; import java.security.Principal; import java.time.Duration; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -203,9 +202,9 @@ class CachingOperationInvokerTests { Object expectedV2 = new Object(); Object expectedV3 = new Object(); InvocationContext contextV2 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(), - Arrays.asList(new ApiVersionArgumentResolver(ApiVersion.V2))); + new ApiVersionArgumentResolver(ApiVersion.V2)); InvocationContext contextV3 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(), - Arrays.asList(new ApiVersionArgumentResolver(ApiVersion.V3))); + new ApiVersionArgumentResolver(ApiVersion.V3)); given(target.invoke(contextV2)).willReturn(expectedV2); given(target.invoke(contextV3)).willReturn(expectedV3); CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL);