Commit c81a0223 authored by Phillip Webb's avatar Phillip Webb

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
parent 1ec49cee
...@@ -21,7 +21,6 @@ import java.util.ArrayList; ...@@ -21,7 +21,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier;
import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.http.ApiVersion;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
...@@ -58,11 +57,11 @@ public class InvocationContext { ...@@ -58,11 +57,11 @@ public class InvocationContext {
* @param arguments the arguments available to the operation. Never {@code null} * @param arguments the arguments available to the operation. Never {@code null}
* @since 2.2.0 * @since 2.2.0
* @deprecated since 2.5.0 in favor of * @deprecated since 2.5.0 in favor of
* {@link #InvocationContext(SecurityContext, Map, List)} * {@link #InvocationContext(SecurityContext, Map, OperationArgumentResolver[])}
*/ */
@Deprecated @Deprecated
public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map<String, Object> arguments) { 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 { ...@@ -74,17 +73,17 @@ public class InvocationContext {
* the operation. * the operation.
*/ */
public InvocationContext(SecurityContext securityContext, Map<String, Object> arguments, public InvocationContext(SecurityContext securityContext, Map<String, Object> arguments,
List<OperationArgumentResolver> argumentResolvers) { OperationArgumentResolver... argumentResolvers) {
Assert.notNull(securityContext, "SecurityContext must not be null"); Assert.notNull(securityContext, "SecurityContext must not be null");
Assert.notNull(arguments, "Arguments must not be null"); Assert.notNull(arguments, "Arguments must not be null");
this.arguments = arguments; this.arguments = arguments;
this.argumentResolvers = new ArrayList<>(); this.argumentResolvers = new ArrayList<>();
if (argumentResolvers != null) { if (argumentResolvers != null) {
this.argumentResolvers.addAll(argumentResolvers); this.argumentResolvers.addAll(Arrays.asList(argumentResolvers));
} }
this.argumentResolvers.add(new FixedValueArgumentResolver<>(SecurityContext.class, securityContext)); this.argumentResolvers.add(OperationArgumentResolver.of(SecurityContext.class, () -> securityContext));
this.argumentResolvers.add(new SuppliedValueArgumentResolver<>(Principal.class, securityContext::getPrincipal)); this.argumentResolvers.add(OperationArgumentResolver.of(Principal.class, securityContext::getPrincipal));
this.argumentResolvers.add(new FixedValueArgumentResolver<>(ApiVersion.class, ApiVersion.LATEST)); this.argumentResolvers.add(OperationArgumentResolver.of(ApiVersion.class, () -> ApiVersion.LATEST));
} }
/** /**
...@@ -154,52 +153,4 @@ public class InvocationContext { ...@@ -154,52 +153,4 @@ public class InvocationContext {
return false; 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 @@ ...@@ -16,6 +16,10 @@
package org.springframework.boot.actuate.endpoint; package org.springframework.boot.actuate.endpoint;
import java.util.function.Supplier;
import org.springframework.util.Assert;
/** /**
* Resolver for an argument of an {@link Operation}. * Resolver for an argument of an {@link Operation}.
* *
...@@ -24,6 +28,14 @@ package org.springframework.boot.actuate.endpoint; ...@@ -24,6 +28,14 @@ package org.springframework.boot.actuate.endpoint;
*/ */
public interface OperationArgumentResolver { 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}. * Resolves an argument of the given {@code type}.
* @param <T> required type of the argument * @param <T> required type of the argument
...@@ -33,11 +45,30 @@ public interface OperationArgumentResolver { ...@@ -33,11 +45,30 @@ public interface OperationArgumentResolver {
<T> T resolve(Class<T> type); <T> T resolve(Class<T> type);
/** /**
* Return whether an argument of the given {@code type} can be resolved. * Factory method that creates an {@link OperationArgumentResolver} for a specific
* @param type argument type * type using a {@link Supplier}.
* @return {@code true} if an argument of the required type can be resolved, otherwise * @param <T> the resolvable type
* {@code false} * @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();
}
};
}
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -40,4 +40,11 @@ public @interface DeleteOperation { ...@@ -40,4 +40,11 @@ public @interface DeleteOperation {
*/ */
String[] produces() default {}; 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;
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.boot.actuate.endpoint.annotation; package org.springframework.boot.actuate.endpoint.annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
...@@ -40,8 +41,32 @@ public class DiscoveredOperationMethod extends OperationMethod { ...@@ -40,8 +41,32 @@ public class DiscoveredOperationMethod extends OperationMethod {
AnnotationAttributes annotationAttributes) { AnnotationAttributes annotationAttributes) {
super(method, operationType); super(method, operationType);
Assert.notNull(annotationAttributes, "AnnotationAttributes must not be null"); Assert.notNull(annotationAttributes, "AnnotationAttributes must not be null");
String[] produces = annotationAttributes.getStringArray("produces"); List<String> producesMediaTypes = new ArrayList<>();
this.producesMediaTypes = Collections.unmodifiableList(Arrays.asList(produces)); 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() { public List<String> getProducesMediaTypes() {
......
...@@ -14,15 +14,23 @@ ...@@ -14,15 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.endpoint.http; package org.springframework.boot.actuate.endpoint.annotation;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
/** /**
* Interface to be implemented by an {@link Enum} that can be injected into an operation * Interface that can be implemented by any {@link Enum} that represents a finite set of
* on a web endpoint. The value of the {@code Producible} enum is resolved using the * producible mime-types.
* {@code Accept} header of the request. When multiple values are equally acceptable, the * <p>
* value with the highest {@link Enum#ordinal() ordinal} is used. * 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 * @param <E> enum type that implements this interface
* @author Andy Wilkinson * @author Andy Wilkinson
...@@ -34,6 +42,6 @@ public interface Producible<E extends Enum<E> & Producible<E>> { ...@@ -34,6 +42,6 @@ public interface Producible<E extends Enum<E> & Producible<E>> {
* Mime type that can be produced. * Mime type that can be produced.
* @return the producible mime type * @return the producible mime type
*/ */
MimeType getMimeType(); MimeType getProducedMimeType();
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -39,4 +39,11 @@ public @interface ReadOperation { ...@@ -39,4 +39,11 @@ public @interface ReadOperation {
*/ */
String[] produces() default {}; 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;
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -39,4 +39,11 @@ public @interface WriteOperation { ...@@ -39,4 +39,11 @@ public @interface WriteOperation {
*/ */
String[] produces() default {}; 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; ...@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.http;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.endpoint.annotation.Producible;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
...@@ -49,12 +50,26 @@ public enum ApiVersion implements Producible<ApiVersion> { ...@@ -49,12 +50,26 @@ public enum ApiVersion implements Producible<ApiVersion> {
*/ */
public static final ApiVersion LATEST = ApiVersion.V3; 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 * Return the {@link ApiVersion} to use based on the HTTP request headers. The version
* will be deduced based on the {@code Accept} header. * will be deduced based on the {@code Accept} header.
* @param headers the HTTP headers * @param headers the HTTP headers
* @return the API version to use * @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) { public static ApiVersion fromHttpHeaders(Map<String, List<String>> headers) {
ApiVersion version = null; ApiVersion version = null;
List<String> accepts = headers.get("Accept"); List<String> accepts = headers.get("Accept");
...@@ -88,15 +103,4 @@ public enum ApiVersion implements Producible<ApiVersion> { ...@@ -88,15 +103,4 @@ public enum ApiVersion implements Producible<ApiVersion> {
return (candidateOrdinal > existingOrdinal) ? candidate : existing; 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; ...@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver; import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.annotation.Producible;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
...@@ -34,10 +35,10 @@ import org.springframework.util.MimeTypeUtils; ...@@ -34,10 +35,10 @@ import org.springframework.util.MimeTypeUtils;
*/ */
public class ProducibleOperationArgumentResolver implements OperationArgumentResolver { 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) { public ProducibleOperationArgumentResolver(Map<String, List<String>> headers) {
this.httpHeaders = httpHeaders; this.headers = headers;
} }
@Override @Override
...@@ -52,7 +53,7 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes ...@@ -52,7 +53,7 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes
} }
private Enum<? extends Producible<?>> resolveProducible(Class<Enum<? extends Producible<?>>> type) { 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()); List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants());
Collections.reverse(values); Collections.reverse(values);
if (CollectionUtils.isEmpty(accepts)) { if (CollectionUtils.isEmpty(accepts)) {
...@@ -69,15 +70,15 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes ...@@ -69,15 +70,15 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes
private static Enum<? extends Producible<?>> mostRecent(Enum<? extends Producible<?>> existing, private static Enum<? extends Producible<?>> mostRecent(Enum<? extends Producible<?>> existing,
Enum<? extends Producible<?>> candidate) { Enum<? extends Producible<?>> candidate) {
int existingOrdinal = (existing != null) ? ((Enum<?>) existing).ordinal() : -1; int existingOrdinal = (existing != null) ? existing.ordinal() : -1;
int candidateOrdinal = (candidate != null) ? ((Enum<?>) candidate).ordinal() : -1; int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1;
return (candidateOrdinal > existingOrdinal) ? candidate : existing; return (candidateOrdinal > existingOrdinal) ? candidate : existing;
} }
private static Enum<? extends Producible<?>> forType(List<Enum<? extends Producible<?>>> candidates, private static Enum<? extends Producible<?>> forType(List<Enum<? extends Producible<?>>> candidates,
MimeType mimeType) { MimeType mimeType) {
for (Enum<? extends Producible<?>> candidate : candidates) { for (Enum<? extends Producible<?>> candidate : candidates) {
if (mimeType.isCompatibleWith(((Producible<?>) candidate).getMimeType())) { if (mimeType.isCompatibleWith(((Producible<?>) candidate).getProducedMimeType())) {
return candidate; return candidate;
} }
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.web; 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.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
...@@ -98,6 +99,17 @@ public final class WebEndpointResponse<T> { ...@@ -98,6 +99,17 @@ public final class WebEndpointResponse<T> {
this(body, STATUS_OK); 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 * Creates a new {@code WebEndpointResponse} with the given body and content type and
* a 200 (OK) status. * a 200 (OK) status.
......
...@@ -21,7 +21,6 @@ import java.io.InputStream; ...@@ -21,7 +21,6 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Principal; import java.security.Principal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
...@@ -154,7 +153,7 @@ public class JerseyEndpointResourceFactory { ...@@ -154,7 +153,7 @@ public class JerseyEndpointResourceFactory {
try { try {
JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext()); JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext());
InvocationContext invocationContext = new InvocationContext(securityContext, arguments, InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
Arrays.asList(new ProducibleOperationArgumentResolver(data.getHeaders()))); new ProducibleOperationArgumentResolver(data.getHeaders()));
Object response = this.operation.invoke(invocationContext); Object response = this.operation.invoke(invocationContext);
return convertToJaxRsResponse(response, data.getRequest().getMethod()); return convertToJaxRsResponse(response, data.getRequest().getMethod());
} }
......
...@@ -19,7 +19,6 @@ package org.springframework.boot.actuate.endpoint.web.reactive; ...@@ -19,7 +19,6 @@ package org.springframework.boot.actuate.endpoint.web.reactive;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Principal; import java.security.Principal;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
...@@ -308,7 +307,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi ...@@ -308,7 +307,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
} }
return this.securityContextSupplier.get() return this.securityContextSupplier.get()
.map((securityContext) -> new InvocationContext(securityContext, arguments, .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), .flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
exchange.getRequest().getMethod())); exchange.getRequest().getMethod()));
} }
......
...@@ -287,7 +287,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin ...@@ -287,7 +287,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
try { try {
ServletSecurityContext securityContext = new ServletSecurityContext(request); ServletSecurityContext securityContext = new ServletSecurityContext(request);
InvocationContext invocationContext = new InvocationContext(securityContext, arguments, InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
Arrays.asList(new ProducibleOperationArgumentResolver(headers))); new ProducibleOperationArgumentResolver(headers));
return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod())); return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod()));
} }
catch (InvalidEndpointRequestException ex) { catch (InvalidEndpointRequestException ex) {
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; ...@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.MimeType;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -48,12 +49,35 @@ class DiscoveredOperationMethodTests { ...@@ -48,12 +49,35 @@ class DiscoveredOperationMethodTests {
AnnotationAttributes annotationAttributes = new AnnotationAttributes(); AnnotationAttributes annotationAttributes = new AnnotationAttributes();
String[] produces = new String[] { "application/json" }; String[] produces = new String[] { "application/json" };
annotationAttributes.put("produces", produces); annotationAttributes.put("produces", produces);
annotationAttributes.put("producesFrom", Producible.class);
DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ, DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ,
annotationAttributes); annotationAttributes);
assertThat(discovered.getProducesMediaTypes()).containsExactly("application/json"); 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() { void example() {
} }
enum ExampleProducible implements Producible<ExampleProducible> {
ONE, TWO, THREE;
@Override
public MimeType getProducedMimeType() {
return new MimeType(toString().toLowerCase());
}
}
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; ...@@ -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.OperationParameters;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; 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.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
...@@ -121,6 +122,14 @@ class DiscoveredOperationsFactoryTests { ...@@ -121,6 +122,14 @@ class DiscoveredOperationsFactoryTests {
assertThat(advisor.getParameters()).isEmpty(); 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) { private <T> T getFirst(Iterable<T> iterable) {
return iterable.iterator().next(); return iterable.iterator().next();
} }
...@@ -175,6 +184,15 @@ class DiscoveredOperationsFactoryTests { ...@@ -175,6 +184,15 @@ class DiscoveredOperationsFactoryTests {
} }
static class ExampleWithProducesFrom {
@ReadOperation(producesFrom = ExampleProducible.class)
String read() {
return "read";
}
}
static class TestDiscoveredOperationsFactory extends DiscoveredOperationsFactory<TestOperation> { static class TestDiscoveredOperationsFactory extends DiscoveredOperationsFactory<TestOperation> {
TestDiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper, TestDiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper,
...@@ -229,4 +247,15 @@ class DiscoveredOperationsFactoryTests { ...@@ -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 { ...@@ -39,24 +39,28 @@ class ApiVersionTests {
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenEmptyReturnsLatest() { void fromHttpHeadersWhenEmptyReturnsLatest() {
ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap()); ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap());
assertThat(version).isEqualTo(ApiVersion.V3); assertThat(version).isEqualTo(ApiVersion.V3);
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() { void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON)); ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON));
assertThat(version).isEqualTo(ApiVersion.V2); assertThat(version).isEqualTo(ApiVersion.V2);
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() { void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON)); ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON));
assertThat(version).isEqualTo(ApiVersion.V3); assertThat(version).isEqualTo(ApiVersion.V3);
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() { void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() {
ApiVersion version = ApiVersion ApiVersion version = ApiVersion
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON)); .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON));
...@@ -64,6 +68,7 @@ class ApiVersionTests { ...@@ -64,6 +68,7 @@ class ApiVersionTests {
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() { void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() {
ApiVersion version = ApiVersion ApiVersion version = ApiVersion
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON)); .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON));
...@@ -71,18 +76,21 @@ class ApiVersionTests { ...@@ -71,18 +76,21 @@ class ApiVersionTests {
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() { void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2")); ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2"));
assertThat(version).isEqualTo(ApiVersion.V2); assertThat(version).isEqualTo(ApiVersion.V2);
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() { void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200")); ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200"));
assertThat(version).isEqualTo(ApiVersion.V3); assertThat(version).isEqualTo(ApiVersion.V3);
} }
@Test @Test
@Deprecated
void fromHttpHeadersWhenAcceptsEverythingReturnsLatest() { void fromHttpHeadersWhenAcceptsEverythingReturnsLatest() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("*/*")); ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("*/*"));
assertThat(version).isEqualTo(ApiVersion.V3); assertThat(version).isEqualTo(ApiVersion.V3);
......
...@@ -18,7 +18,6 @@ package org.springframework.boot.actuate.endpoint.invoker.cache; ...@@ -18,7 +18,6 @@ package org.springframework.boot.actuate.endpoint.invoker.cache;
import java.security.Principal; import java.security.Principal;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
...@@ -203,9 +202,9 @@ class CachingOperationInvokerTests { ...@@ -203,9 +202,9 @@ class CachingOperationInvokerTests {
Object expectedV2 = new Object(); Object expectedV2 = new Object();
Object expectedV3 = new Object(); Object expectedV3 = new Object();
InvocationContext contextV2 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(), 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(), 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(contextV2)).willReturn(expectedV2);
given(target.invoke(contextV3)).willReturn(expectedV3); given(target.invoke(contextV3)).willReturn(expectedV3);
CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment