Allow operations to produce different output
Update the actuator @Enpoint` infrastructure code so that operations may inject enums that indicate the type of output to produce. A new `Producible` interface can be implemented by any enum that indicates the mime-type that an enum value produces. The new `OperationArgumentResolver` provides a general strategy for resolving operation arguments with `ProducibleOperationArgumentResolver` providing support for `Producible` enums. Existing injection support has been refactored to use the new resolver. See gh-25738
This commit is contained in:
parent
663fd8ce5e
commit
1ec49cee8b
|
@ -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.
|
||||
|
@ -16,7 +16,12 @@
|
|||
|
||||
package org.springframework.boot.actuate.endpoint;
|
||||
|
||||
import java.security.Principal;
|
||||
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;
|
||||
|
@ -31,11 +36,9 @@ import org.springframework.util.Assert;
|
|||
*/
|
||||
public class InvocationContext {
|
||||
|
||||
private final SecurityContext securityContext;
|
||||
|
||||
private final Map<String, Object> arguments;
|
||||
|
||||
private final ApiVersion apiVersion;
|
||||
private final List<OperationArgumentResolver> argumentResolvers;
|
||||
|
||||
/**
|
||||
* Creates a new context for an operation being invoked by the given
|
||||
|
@ -54,13 +57,34 @@ public class InvocationContext {
|
|||
* @param securityContext the current security context. Never {@code null}
|
||||
* @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)}
|
||||
*/
|
||||
@Deprecated
|
||||
public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map<String, Object> arguments) {
|
||||
this(securityContext, arguments, Arrays.asList(new FixedValueArgumentResolver<>(ApiVersion.class, apiVersion)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new context for an operation being invoked by the given
|
||||
* {@code securityContext} with the given available {@code arguments}.
|
||||
* @param securityContext the current security context. Never {@code null}
|
||||
* @param arguments the arguments available to the operation. Never {@code null}
|
||||
* @param argumentResolvers resolvers for additional arguments should be available to
|
||||
* the operation.
|
||||
*/
|
||||
public InvocationContext(SecurityContext securityContext, Map<String, Object> arguments,
|
||||
List<OperationArgumentResolver> argumentResolvers) {
|
||||
Assert.notNull(securityContext, "SecurityContext must not be null");
|
||||
Assert.notNull(arguments, "Arguments must not be null");
|
||||
this.apiVersion = (apiVersion != null) ? apiVersion : ApiVersion.LATEST;
|
||||
this.securityContext = securityContext;
|
||||
this.arguments = arguments;
|
||||
this.argumentResolvers = new ArrayList<>();
|
||||
if (argumentResolvers != null) {
|
||||
this.argumentResolvers.addAll(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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,15 +93,17 @@ public class InvocationContext {
|
|||
* @since 2.2.0
|
||||
*/
|
||||
public ApiVersion getApiVersion() {
|
||||
return this.apiVersion;
|
||||
return resolveArgument(ApiVersion.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the security context to use for the invocation.
|
||||
* @return the security context
|
||||
* @deprecated since 2.5.0 in favor of {@link #resolveArgument(Class)}
|
||||
*/
|
||||
@Deprecated
|
||||
public SecurityContext getSecurityContext() {
|
||||
return this.securityContext;
|
||||
return resolveArgument(SecurityContext.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,4 +114,92 @@ public class InvocationContext {
|
|||
return this.arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an argument with the given {@code argumentType}.
|
||||
* @param <T> type of the argument
|
||||
* @param argumentType type of the argument
|
||||
* @return resolved argument of the required type or {@code null}
|
||||
* @since 2.5.0
|
||||
* @see #canResolve(Class)
|
||||
*/
|
||||
public <T> T resolveArgument(Class<T> argumentType) {
|
||||
for (OperationArgumentResolver argumentResolver : this.argumentResolvers) {
|
||||
if (argumentResolver.canResolve(argumentType)) {
|
||||
T result = argumentResolver.resolve(argumentType);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the context is capable of resolving an argument of the given
|
||||
* {@code type}. Note that, even when {@code true} is returned,
|
||||
* {@link #resolveArgument argument resolution} will return {@code null} if no
|
||||
* argument of the required type is available.
|
||||
* @param type argument type
|
||||
* @return {@code true} if resolution of arguments of the given type is possible,
|
||||
* otherwise {@code false}.
|
||||
* @since 2.5.0
|
||||
* @see #resolveArgument(Class)
|
||||
*/
|
||||
public boolean canResolve(Class<?> type) {
|
||||
for (OperationArgumentResolver argumentResolver : this.argumentResolvers) {
|
||||
if (argumentResolver.canResolve(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.endpoint;
|
||||
|
||||
/**
|
||||
* Resolver for an argument of an {@link Operation}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.5.0
|
||||
*/
|
||||
public interface OperationArgumentResolver {
|
||||
|
||||
/**
|
||||
* Resolves an argument of the given {@code type}.
|
||||
* @param <T> required type of the argument
|
||||
* @param type argument type
|
||||
* @return an argument of the required type, or {@code null}
|
||||
*/
|
||||
<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}
|
||||
*/
|
||||
boolean canResolve(Class<?> type);
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 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.
|
||||
|
@ -20,6 +20,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
|
@ -29,17 +30,17 @@ import org.springframework.util.MimeTypeUtils;
|
|||
* @author Phillip Webb
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public enum ApiVersion {
|
||||
public enum ApiVersion implements Producible<ApiVersion> {
|
||||
|
||||
/**
|
||||
* Version 2 (supported by Spring Boot 2.0+).
|
||||
*/
|
||||
V2,
|
||||
V2(ActuatorMediaType.V2_JSON),
|
||||
|
||||
/**
|
||||
* Version 3 (supported by Spring Boot 2.2+).
|
||||
*/
|
||||
V3;
|
||||
V3(ActuatorMediaType.V3_JSON);
|
||||
|
||||
private static final String MEDIA_TYPE_PREFIX = "application/vnd.spring-boot.actuator.";
|
||||
|
||||
|
@ -87,4 +88,15 @@ public enum 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.http;
|
||||
|
||||
import 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.
|
||||
*
|
||||
* @param <E> enum type that implements this interface
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.5.0
|
||||
*/
|
||||
public interface Producible<E extends Enum<E> & Producible<E>> {
|
||||
|
||||
/**
|
||||
* Mime type that can be produced.
|
||||
* @return the producible mime type
|
||||
*/
|
||||
MimeType getMimeType();
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.http;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* An {@link OperationArgumentResolver} for {@link Producible producible enums}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.5.0
|
||||
*/
|
||||
public class ProducibleOperationArgumentResolver implements OperationArgumentResolver {
|
||||
|
||||
private final Map<String, List<String>> httpHeaders;
|
||||
|
||||
public ProducibleOperationArgumentResolver(Map<String, List<String>> httpHeaders) {
|
||||
this.httpHeaders = httpHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canResolve(Class<?> type) {
|
||||
return Producible.class.isAssignableFrom(type) && Enum.class.isAssignableFrom(type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T resolve(Class<T> type) {
|
||||
return (T) resolveProducible((Class<Enum<? extends Producible<?>>>) type);
|
||||
}
|
||||
|
||||
private Enum<? extends Producible<?>> resolveProducible(Class<Enum<? extends Producible<?>>> type) {
|
||||
List<String> accepts = this.httpHeaders.get("Accept");
|
||||
List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants());
|
||||
Collections.reverse(values);
|
||||
if (CollectionUtils.isEmpty(accepts)) {
|
||||
return values.get(0);
|
||||
}
|
||||
Enum<? extends Producible<?>> result = null;
|
||||
for (String accept : accepts) {
|
||||
for (String mimeType : MimeTypeUtils.tokenize(accept)) {
|
||||
result = mostRecent(result, forType(values, MimeTypeUtils.parseMimeType(mimeType)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
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())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,13 +17,10 @@
|
|||
package org.springframework.boot.actuate.endpoint.invoke.reflect;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.Principal;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
|
||||
|
@ -89,13 +86,7 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
|
|||
if (!parameter.isMandatory()) {
|
||||
return false;
|
||||
}
|
||||
if (ApiVersion.class.equals(parameter.getType())) {
|
||||
return false;
|
||||
}
|
||||
if (Principal.class.equals(parameter.getType())) {
|
||||
return context.getSecurityContext().getPrincipal() == null;
|
||||
}
|
||||
if (SecurityContext.class.equals(parameter.getType())) {
|
||||
if (context.canResolve(parameter.getType())) {
|
||||
return false;
|
||||
}
|
||||
return context.getArguments().get(parameter.getName()) == null;
|
||||
|
@ -107,14 +98,9 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
|
|||
}
|
||||
|
||||
private Object resolveArgument(OperationParameter parameter, InvocationContext context) {
|
||||
if (ApiVersion.class.equals(parameter.getType())) {
|
||||
return context.getApiVersion();
|
||||
}
|
||||
if (Principal.class.equals(parameter.getType())) {
|
||||
return context.getSecurityContext().getPrincipal();
|
||||
}
|
||||
if (SecurityContext.class.equals(parameter.getType())) {
|
||||
return context.getSecurityContext();
|
||||
Object resolvedByType = context.resolveArgument(parameter.getType());
|
||||
if (resolvedByType != null) {
|
||||
return resolvedByType;
|
||||
}
|
||||
Object value = context.getArguments().get(parameter.getName());
|
||||
return this.parameterValueMapper.mapParameterValue(parameter, value);
|
||||
|
|
|
@ -79,7 +79,8 @@ public class CachingOperationInvoker implements OperationInvoker {
|
|||
}
|
||||
long accessTime = System.currentTimeMillis();
|
||||
ApiVersion contextApiVersion = context.getApiVersion();
|
||||
CacheKey cacheKey = new CacheKey(contextApiVersion, context.getSecurityContext().getPrincipal());
|
||||
Principal principal = context.resolveArgument(Principal.class);
|
||||
CacheKey cacheKey = new CacheKey(contextApiVersion, principal);
|
||||
CachedResponse cached = this.cachedResponses.get(cacheKey);
|
||||
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
|
||||
Object response = this.invoker.invoke(context);
|
||||
|
|
|
@ -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.web;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* A {@code WebEndpointResponse} can be returned by an operation on a
|
||||
|
@ -70,6 +71,8 @@ public final class WebEndpointResponse<T> {
|
|||
|
||||
private final int status;
|
||||
|
||||
private final MimeType contentType;
|
||||
|
||||
/**
|
||||
* Creates a new {@code WebEndpointResponse} with no body and a 200 (OK) status.
|
||||
*/
|
||||
|
@ -87,7 +90,7 @@ public final class WebEndpointResponse<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code WebEndpointResponse} with then given body and a 200 (OK)
|
||||
* Creates a new {@code WebEndpointResponse} with the given body and a 200 (OK)
|
||||
* status.
|
||||
* @param body the body
|
||||
*/
|
||||
|
@ -96,13 +99,44 @@ public final class WebEndpointResponse<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code WebEndpointResponse} with then given body and status.
|
||||
* Creates a new {@code WebEndpointResponse} with the given body and content type and
|
||||
* a 200 (OK) status.
|
||||
* @param body the body
|
||||
* @param contentType the content type of the response
|
||||
* @since 2.5.0
|
||||
*/
|
||||
public WebEndpointResponse(T body, MimeType contentType) {
|
||||
this(body, STATUS_OK, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code WebEndpointResponse} with the given body and status.
|
||||
* @param body the body
|
||||
* @param status the HTTP status
|
||||
*/
|
||||
public WebEndpointResponse(T body, int status) {
|
||||
this(body, status, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code WebEndpointResponse} with the given body and status.
|
||||
* @param body the body
|
||||
* @param status the HTTP status
|
||||
* @param contentType the content type of the response
|
||||
* @since 2.5.0
|
||||
*/
|
||||
public WebEndpointResponse(T body, int status, MimeType contentType) {
|
||||
this.body = body;
|
||||
this.status = status;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content type of the response.
|
||||
* @return the content type;
|
||||
*/
|
||||
public MimeType getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 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.
|
||||
|
@ -21,6 +21,7 @@ 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;
|
||||
|
@ -43,7 +44,7 @@ import reactor.core.publisher.Mono;
|
|||
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.http.ProducibleOperationArgumentResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
|
@ -151,9 +152,9 @@ public class JerseyEndpointResourceFactory {
|
|||
arguments.putAll(extractPathParameters(data));
|
||||
arguments.putAll(extractQueryParameters(data));
|
||||
try {
|
||||
ApiVersion apiVersion = ApiVersion.fromHttpHeaders(data.getHeaders());
|
||||
JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext());
|
||||
InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments);
|
||||
InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
|
||||
Arrays.asList(new ProducibleOperationArgumentResolver(data.getHeaders())));
|
||||
Object response = this.operation.invoke(invocationContext);
|
||||
return convertToJaxRsResponse(response, data.getRequest().getMethod());
|
||||
}
|
||||
|
@ -215,6 +216,7 @@ public class JerseyEndpointResourceFactory {
|
|||
}
|
||||
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
|
||||
return Response.status(webEndpointResponse.getStatus())
|
||||
.header("Content-Type", webEndpointResponse.getContentType())
|
||||
.entity(convertIfNecessary(webEndpointResponse.getBody())).build();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
|
|
|
@ -19,6 +19,7 @@ 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;
|
||||
|
@ -33,7 +34,7 @@ import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException
|
|||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.http.ProducibleOperationArgumentResolver;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
|
@ -43,6 +44,7 @@ import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
|||
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDecisionVoter;
|
||||
import org.springframework.security.access.SecurityConfig;
|
||||
|
@ -297,7 +299,6 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
|||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body) {
|
||||
ApiVersion apiVersion = ApiVersion.fromHttpHeaders(exchange.getRequest().getHeaders());
|
||||
Map<String, Object> arguments = getArguments(exchange, body);
|
||||
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
|
||||
.getMatchAllRemainingPathSegmentsVariable();
|
||||
|
@ -306,7 +307,8 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
|||
tokenizePathSegments((String) arguments.get(matchAllRemainingPathSegmentsVariable)));
|
||||
}
|
||||
return this.securityContextSupplier.get()
|
||||
.map((securityContext) -> new InvocationContext(apiVersion, securityContext, arguments))
|
||||
.map((securityContext) -> new InvocationContext(securityContext, arguments,
|
||||
Arrays.asList(new ProducibleOperationArgumentResolver(exchange.getRequest().getHeaders()))))
|
||||
.flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
|
||||
exchange.getRequest().getMethod()));
|
||||
}
|
||||
|
@ -348,7 +350,10 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
|||
return new ResponseEntity<>(response, HttpStatus.OK);
|
||||
}
|
||||
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
|
||||
return ResponseEntity.status(webEndpointResponse.getStatus()).body(webEndpointResponse.getBody());
|
||||
MediaType contentType = (webEndpointResponse.getContentType() != null)
|
||||
? new MediaType(webEndpointResponse.getContentType()) : null;
|
||||
return ResponseEntity.status(webEndpointResponse.getStatus()).contentType(contentType)
|
||||
.body(webEndpointResponse.getBody());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 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.
|
||||
|
@ -33,7 +33,7 @@ import org.springframework.beans.factory.InitializingBean;
|
|||
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.http.ProducibleOperationArgumentResolver;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
|
@ -44,6 +44,7 @@ import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicat
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
@ -284,9 +285,9 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
|
||||
Map<String, Object> arguments = getArguments(request, body);
|
||||
try {
|
||||
ApiVersion apiVersion = ApiVersion.fromHttpHeaders(headers);
|
||||
ServletSecurityContext securityContext = new ServletSecurityContext(request);
|
||||
InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments);
|
||||
InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
|
||||
Arrays.asList(new ProducibleOperationArgumentResolver(headers)));
|
||||
return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod()));
|
||||
}
|
||||
catch (InvalidEndpointRequestException ex) {
|
||||
|
@ -352,7 +353,9 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
|||
return result;
|
||||
}
|
||||
WebEndpointResponse<?> response = (WebEndpointResponse<?>) result;
|
||||
return ResponseEntity.status(response.getStatus()).body(response.getBody());
|
||||
MediaType contentType = (response.getContentType() != null) ? new MediaType(response.getContentType())
|
||||
: null;
|
||||
return ResponseEntity.status(response.getStatus()).contentType(contentType).body(response.getBody());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,11 +39,24 @@ class InvocationContextTests {
|
|||
private final Map<String, Object> arguments = Collections.singletonMap("test", "value");
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void createWhenApiVersionIsNullUsesLatestVersion() {
|
||||
InvocationContext context = new InvocationContext(null, this.securityContext, this.arguments);
|
||||
assertThat(context.getApiVersion()).isEqualTo(ApiVersion.LATEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCreatedWithoutApiVersionThenGetApiVersionReturnsLatestVersion() {
|
||||
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||
assertThat(context.getApiVersion()).isEqualTo(ApiVersion.LATEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCreatedWithoutApiVersionThenResolveApiVersionReturnsLatestVersion() {
|
||||
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||
assertThat(context.resolveArgument(ApiVersion.class)).isEqualTo(ApiVersion.LATEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenSecurityContextIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new InvocationContext(null, this.arguments))
|
||||
|
@ -57,17 +70,25 @@ class InvocationContextTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void getApiVersionReturnsApiVersion() {
|
||||
InvocationContext context = new InvocationContext(ApiVersion.V2, this.securityContext, this.arguments);
|
||||
assertThat(context.getApiVersion()).isEqualTo(ApiVersion.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void getSecurityContextReturnsSecurityContext() {
|
||||
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||
assertThat(context.getSecurityContext()).isEqualTo(this.securityContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveSecurityContextReturnsSecurityContext() {
|
||||
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||
assertThat(context.resolveArgument(SecurityContext.class)).isEqualTo(this.securityContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArgumentsReturnsArguments() {
|
||||
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||
|
|
|
@ -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.
|
||||
|
@ -82,6 +82,12 @@ class ApiVersionTests {
|
|||
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenAcceptsEverythingReturnsLatest() {
|
||||
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("*/*"));
|
||||
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
private Map<String, List<String>> acceptHeader(String... types) {
|
||||
List<String> value = Arrays.asList(types);
|
||||
return value.isEmpty() ? Collections.emptyMap() : Collections.singletonMap("Accept", value);
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.http;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link ProducibleOperationArgumentResolver}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ProducibleOperationArgumentResolverTests {
|
||||
|
||||
@Test
|
||||
void whenAcceptHeaderIsEmptyThenHighestOrdinalIsReturned() {
|
||||
assertThat(resolve(acceptHeader())).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEverythingIsAcceptableThenHighestOrdinalIsReturned() {
|
||||
assertThat(resolve(acceptHeader("*/*"))).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNothingIsAcceptableThenNullIsReturned() {
|
||||
assertThat(resolve(acceptHeader("image/png"))).isEqualTo(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenSingleValueIsAcceptableThenMatchingEnumValueIsReturned() {
|
||||
assertThat(new ProducibleOperationArgumentResolver(acceptHeader(ActuatorMediaType.V2_JSON))
|
||||
.resolve(ApiVersion.class)).isEqualTo(ApiVersion.V2);
|
||||
assertThat(new ProducibleOperationArgumentResolver(acceptHeader(ActuatorMediaType.V3_JSON))
|
||||
.resolve(ApiVersion.class)).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleValuesAreAcceptableThenHighestOrdinalIsReturned() {
|
||||
assertThat(resolve(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON)))
|
||||
.isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleValuesAreAcceptableAsSingleHeaderThenHighestOrdinalIsReturned() {
|
||||
assertThat(resolve(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON)))
|
||||
.isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
private Map<String, List<String>> acceptHeader(String... types) {
|
||||
List<String> value = Arrays.asList(types);
|
||||
return value.isEmpty() ? Collections.emptyMap() : Collections.singletonMap("Accept", value);
|
||||
}
|
||||
|
||||
private ApiVersion resolve(Map<String, List<String>> httpHeaders) {
|
||||
return new ProducibleOperationArgumentResolver(httpHeaders).resolve(ApiVersion.class);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 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.
|
||||
|
@ -18,6 +18,7 @@ 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;
|
||||
|
@ -28,6 +29,7 @@ import reactor.core.publisher.Flux;
|
|||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
|
||||
|
@ -200,10 +202,10 @@ class CachingOperationInvokerTests {
|
|||
OperationInvoker target = mock(OperationInvoker.class);
|
||||
Object expectedV2 = new Object();
|
||||
Object expectedV3 = new Object();
|
||||
InvocationContext contextV2 = new InvocationContext(ApiVersion.V2, mock(SecurityContext.class),
|
||||
Collections.emptyMap());
|
||||
InvocationContext contextV3 = new InvocationContext(ApiVersion.V3, mock(SecurityContext.class),
|
||||
Collections.emptyMap());
|
||||
InvocationContext contextV2 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(),
|
||||
Arrays.asList(new ApiVersionArgumentResolver(ApiVersion.V2)));
|
||||
InvocationContext contextV3 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(),
|
||||
Arrays.asList(new ApiVersionArgumentResolver(ApiVersion.V3)));
|
||||
given(target.invoke(contextV2)).willReturn(expectedV2);
|
||||
given(target.invoke(contextV3)).willReturn(expectedV3);
|
||||
CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL);
|
||||
|
@ -240,4 +242,25 @@ class CachingOperationInvokerTests {
|
|||
|
||||
}
|
||||
|
||||
private static final class ApiVersionArgumentResolver implements OperationArgumentResolver {
|
||||
|
||||
private final ApiVersion apiVersion;
|
||||
|
||||
private ApiVersionArgumentResolver(ApiVersion apiVersion) {
|
||||
this.apiVersion = apiVersion;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T resolve(Class<T> type) {
|
||||
return (T) this.apiVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canResolve(Class<?> type) {
|
||||
return ApiVersion.class.equals(type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue