Refine 'Allow operations to produce different output'
Refine the new `Producible` support so that it can also be used with `@ReadOperation`, `@WriteOperation` and `@DeleteOperation` annotations. This update allows the same enum to be used both as an argument and as an indicator of the media-types that an operation may produce. Closes gh-25738
This commit is contained in:
@@ -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<String, Object> 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<String, Object> arguments,
|
||||
List<OperationArgumentResolver> 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<T> implements OperationArgumentResolver {
|
||||
|
||||
private final Class<T> argumentType;
|
||||
|
||||
private final T value;
|
||||
|
||||
private FixedValueArgumentResolver(Class<T> argumentType, T value) {
|
||||
this.argumentType = argumentType;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <U> U resolve(Class<U> type) {
|
||||
return (U) this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canResolve(Class<?> type) {
|
||||
return this.argumentType.equals(type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SuppliedValueArgumentResolver<T> implements OperationArgumentResolver {
|
||||
|
||||
private final Class<T> argumentType;
|
||||
|
||||
private final Supplier<T> value;
|
||||
|
||||
private SuppliedValueArgumentResolver(Class<T> argumentType, Supplier<T> value) {
|
||||
this.argumentType = argumentType;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <U> U resolve(Class<U> type) {
|
||||
return (U) this.value.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canResolve(Class<?> type) {
|
||||
return this.argumentType.equals(type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T> required type of the argument
|
||||
@@ -33,11 +45,30 @@ public interface OperationArgumentResolver {
|
||||
<T> T resolve(Class<T> 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 <T> the resolvable type
|
||||
* @param type the resolvable type
|
||||
* @param supplier the value supplier
|
||||
* @return an {@link OperationArgumentResolver} instance
|
||||
*/
|
||||
boolean canResolve(Class<?> type);
|
||||
static <T> OperationArgumentResolver of(Class<T> type, Supplier<? extends T> 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> R resolve(Class<R> argumentType) {
|
||||
return (R) supplier.get();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<? extends Producible> producesFrom() default Producible.class;
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> producesMediaTypes = new ArrayList<>();
|
||||
producesMediaTypes.addAll(Arrays.asList(annotationAttributes.getStringArray("produces")));
|
||||
producesMediaTypes.addAll(getProducesFromProducable(annotationAttributes));
|
||||
this.producesMediaTypes = Collections.unmodifiableList(producesMediaTypes);
|
||||
}
|
||||
|
||||
private <E extends Enum<E> & Producible<E>> List<String> getProducesFromProducable(
|
||||
AnnotationAttributes annotationAttributes) {
|
||||
Class<?> type = getProducesFrom(annotationAttributes);
|
||||
if (type == Producible.class) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> 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<String> getProducesMediaTypes() {
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* Can be used with {@link ReadOperation @ReadOperation},
|
||||
* {@link WriteOperation @ReadOperation} and {@link DeleteOperation @ReadOperation}
|
||||
* annotations to quickly define a list of {@code produces} values.
|
||||
* <p>
|
||||
* {@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 <E> enum type that implements this interface
|
||||
* @author Andy Wilkinson
|
||||
@@ -34,6 +42,6 @@ public interface Producible<E extends Enum<E> & Producible<E>> {
|
||||
* Mime type that can be produced.
|
||||
* @return the producible mime type
|
||||
*/
|
||||
MimeType getMimeType();
|
||||
MimeType getProducedMimeType();
|
||||
|
||||
}
|
||||
@@ -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<? extends Producible> producesFrom() default Producible.class;
|
||||
|
||||
}
|
||||
|
||||
@@ -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<? extends Producible> producesFrom() default Producible.class;
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ApiVersion> {
|
||||
*/
|
||||
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<String, List<String>> headers) {
|
||||
ApiVersion version = null;
|
||||
List<String> accepts = headers.get("Accept");
|
||||
@@ -88,15 +103,4 @@ public enum ApiVersion implements Producible<ApiVersion> {
|
||||
return (candidateOrdinal > existingOrdinal) ? candidate : existing;
|
||||
}
|
||||
|
||||
private final MimeType mimeType;
|
||||
|
||||
ApiVersion(String mimeType) {
|
||||
this.mimeType = MimeTypeUtils.parseMimeType(mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MimeType getMimeType() {
|
||||
return this.mimeType;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, List<String>> httpHeaders;
|
||||
private final Map<String, List<String>> headers;
|
||||
|
||||
public ProducibleOperationArgumentResolver(Map<String, List<String>> httpHeaders) {
|
||||
this.httpHeaders = httpHeaders;
|
||||
public ProducibleOperationArgumentResolver(Map<String, List<String>> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,7 +53,7 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes
|
||||
}
|
||||
|
||||
private Enum<? extends Producible<?>> resolveProducible(Class<Enum<? extends Producible<?>>> type) {
|
||||
List<String> accepts = this.httpHeaders.get("Accept");
|
||||
List<String> accepts = this.headers.get("Accept");
|
||||
List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants());
|
||||
Collections.reverse(values);
|
||||
if (CollectionUtils.isEmpty(accepts)) {
|
||||
@@ -69,15 +70,15 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes
|
||||
|
||||
private static Enum<? extends Producible<?>> mostRecent(Enum<? extends Producible<?>> existing,
|
||||
Enum<? extends Producible<?>> 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<? extends Producible<?>> forType(List<Enum<? extends Producible<?>>> candidates,
|
||||
MimeType mimeType) {
|
||||
for (Enum<? extends Producible<?>> candidate : candidates) {
|
||||
if (mimeType.isCompatibleWith(((Producible<?>) candidate).getMimeType())) {
|
||||
if (mimeType.isCompatibleWith(((Producible<?>) candidate).getProducedMimeType())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> {
|
||||
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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ExampleProducible> {
|
||||
|
||||
ONE, TWO, THREE;
|
||||
|
||||
@Override
|
||||
public MimeType getProducedMimeType() {
|
||||
return new MimeType(toString().toLowerCase());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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> T getFirst(Iterable<T> 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<TestOperation> {
|
||||
|
||||
TestDiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper,
|
||||
@@ -229,4 +247,15 @@ class DiscoveredOperationsFactoryTests {
|
||||
|
||||
}
|
||||
|
||||
enum ExampleProducible implements Producible<ExampleProducible> {
|
||||
|
||||
ONE, TWO, THREE;
|
||||
|
||||
@Override
|
||||
public MimeType getProducedMimeType() {
|
||||
return new MimeType(toString().toLowerCase());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user