Allow endpoint @Selector to capture all paths
Update `@Selector` with a `match` attribute that can be used to select all remaining path segments. An endpoint method like this: select(@Selector(match = Match.ALL_REMAINING) String... selection) Will now have all reaming path segments injected into the `selection` parameter. Closes gh-17743
This commit is contained in:
parent
b8bda1c03d
commit
890ea153bf
|
@ -23,8 +23,11 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@code Selector} can be used on a parameter of an {@link Endpoint @Endpoint} method
|
* A {@code @Selector} can be used on a parameter of an {@link Endpoint @Endpoint} method
|
||||||
* to indicate that the parameter is used to select a subset of the endpoint's data.
|
* to indicate that the parameter is used to select a subset of the endpoint's data.
|
||||||
|
* <p>
|
||||||
|
* A {@code @Selector} may change the way that the endpoint is exposed to the user. For
|
||||||
|
* example, HTTP mapped endpoints will map select parameters to path variables.
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
|
@ -34,4 +37,31 @@ import java.lang.annotation.Target;
|
||||||
@Documented
|
@Documented
|
||||||
public @interface Selector {
|
public @interface Selector {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match type that should be used for the selection.
|
||||||
|
* @return the match type
|
||||||
|
* @since 2.2.0
|
||||||
|
*/
|
||||||
|
Match match() default Match.SINGLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match types that can be used with the {@code @Selector}.
|
||||||
|
*/
|
||||||
|
enum Match {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture a single item. For example, in the case of a web application a single
|
||||||
|
* path segment. The parameter value be converted from a {@code String} source.
|
||||||
|
*/
|
||||||
|
SINGLE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture all remaining times. For example, in the case of a web application all
|
||||||
|
* remaining path segments. The parameter value be converted from a
|
||||||
|
* {@code String[]} source.
|
||||||
|
*/
|
||||||
|
ALL_REMAINING
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.web;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
@ -31,10 +32,14 @@ import org.springframework.util.StringUtils;
|
||||||
*/
|
*/
|
||||||
public final class WebOperationRequestPredicate {
|
public final class WebOperationRequestPredicate {
|
||||||
|
|
||||||
private static final Pattern PATH_VAR_PATTERN = Pattern.compile("\\{.*?}");
|
private static final Pattern PATH_VAR_PATTERN = Pattern.compile("(\\{\\*?).+?}");
|
||||||
|
|
||||||
|
private static final Pattern ALL_REMAINING_PATH_SEGMENTS_VAR_PATTERN = Pattern.compile("^.*\\{\\*(.+?)}$");
|
||||||
|
|
||||||
private final String path;
|
private final String path;
|
||||||
|
|
||||||
|
private final String matchAllRemainingPathSegmentsVariable;
|
||||||
|
|
||||||
private final String canonicalPath;
|
private final String canonicalPath;
|
||||||
|
|
||||||
private final WebEndpointHttpMethod httpMethod;
|
private final WebEndpointHttpMethod httpMethod;
|
||||||
|
@ -53,12 +58,23 @@ public final class WebOperationRequestPredicate {
|
||||||
public WebOperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod, Collection<String> consumes,
|
public WebOperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod, Collection<String> consumes,
|
||||||
Collection<String> produces) {
|
Collection<String> produces) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.canonicalPath = PATH_VAR_PATTERN.matcher(path).replaceAll("{*}");
|
this.canonicalPath = extractCanonicalPath(path);
|
||||||
|
this.matchAllRemainingPathSegmentsVariable = extractMatchAllRemainingPathSegmentsVariable(path);
|
||||||
this.httpMethod = httpMethod;
|
this.httpMethod = httpMethod;
|
||||||
this.consumes = consumes;
|
this.consumes = consumes;
|
||||||
this.produces = produces;
|
this.produces = produces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractCanonicalPath(String path) {
|
||||||
|
Matcher matcher = PATH_VAR_PATTERN.matcher(path);
|
||||||
|
return matcher.replaceAll("$1*}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractMatchAllRemainingPathSegmentsVariable(String path) {
|
||||||
|
Matcher matcher = ALL_REMAINING_PATH_SEGMENTS_VAR_PATTERN.matcher(path);
|
||||||
|
return matcher.matches() ? matcher.group(1) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the path for the operation.
|
* Returns the path for the operation.
|
||||||
* @return the path
|
* @return the path
|
||||||
|
@ -67,6 +83,16 @@ public final class WebOperationRequestPredicate {
|
||||||
return this.path;
|
return this.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the variable used to catch all remaining path segments
|
||||||
|
* {@code null}.
|
||||||
|
* @return the variable name
|
||||||
|
* @since 2.2.0
|
||||||
|
*/
|
||||||
|
public String getMatchAllRemainingPathSegmentsVariable() {
|
||||||
|
return this.matchAllRemainingPathSegmentsVariable;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the HTTP method for the operation.
|
* Returns the HTTP method for the operation.
|
||||||
* @return the HTTP method
|
* @return the HTTP method
|
||||||
|
|
|
@ -18,14 +18,15 @@ package org.springframework.boot.actuate.endpoint.web.annotation;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Parameter;
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
|
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
|
||||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod;
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
|
@ -52,26 +53,51 @@ class RequestPredicateFactory {
|
||||||
|
|
||||||
WebOperationRequestPredicate getRequestPredicate(String rootPath, DiscoveredOperationMethod operationMethod) {
|
WebOperationRequestPredicate getRequestPredicate(String rootPath, DiscoveredOperationMethod operationMethod) {
|
||||||
Method method = operationMethod.getMethod();
|
Method method = operationMethod.getMethod();
|
||||||
String path = getPath(rootPath, method);
|
Parameter[] selectorParameters = Arrays.stream(method.getParameters()).filter(this::hasSelector)
|
||||||
|
.toArray(Parameter[]::new);
|
||||||
|
Parameter allRemainingPathSegmentsParameter = getAllRemainingPathSegmentsParameter(selectorParameters);
|
||||||
|
String path = getPath(rootPath, selectorParameters, allRemainingPathSegmentsParameter != null);
|
||||||
WebEndpointHttpMethod httpMethod = determineHttpMethod(operationMethod.getOperationType());
|
WebEndpointHttpMethod httpMethod = determineHttpMethod(operationMethod.getOperationType());
|
||||||
Collection<String> consumes = getConsumes(httpMethod, method);
|
Collection<String> consumes = getConsumes(httpMethod, method);
|
||||||
Collection<String> produces = getProduces(operationMethod, method);
|
Collection<String> produces = getProduces(operationMethod, method);
|
||||||
return new WebOperationRequestPredicate(path, httpMethod, consumes, produces);
|
return new WebOperationRequestPredicate(path, httpMethod, consumes, produces);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPath(String rootPath, Method method) {
|
private Parameter getAllRemainingPathSegmentsParameter(Parameter[] selectorParameters) {
|
||||||
return rootPath + Stream.of(method.getParameters()).filter(this::hasSelector).map(this::slashName)
|
Parameter trailingPathsParameter = null;
|
||||||
.collect(Collectors.joining());
|
for (int i = 0; i < selectorParameters.length; i++) {
|
||||||
|
Parameter selectorParameter = selectorParameters[i];
|
||||||
|
Selector selector = selectorParameter.getAnnotation(Selector.class);
|
||||||
|
if (selector.match() == Match.ALL_REMAINING) {
|
||||||
|
Assert.state(trailingPathsParameter == null,
|
||||||
|
"@Selector annotation with Match.ALL_REMAINING must be unique");
|
||||||
|
trailingPathsParameter = selectorParameter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trailingPathsParameter != null) {
|
||||||
|
Assert.state(trailingPathsParameter == selectorParameters[selectorParameters.length - 1],
|
||||||
|
"@Selector annotation with Match.ALL_REMAINING must be the last parameter");
|
||||||
|
}
|
||||||
|
return trailingPathsParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPath(String rootPath, Parameter[] selectorParameters, boolean matchRemainingPathSegments) {
|
||||||
|
StringBuilder path = new StringBuilder(rootPath);
|
||||||
|
for (int i = 0; i < selectorParameters.length; i++) {
|
||||||
|
path.append("/{");
|
||||||
|
if (i == selectorParameters.length - 1 && matchRemainingPathSegments) {
|
||||||
|
path.append("*");
|
||||||
|
}
|
||||||
|
path.append(selectorParameters[i].getName());
|
||||||
|
path.append("}");
|
||||||
|
}
|
||||||
|
return path.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasSelector(Parameter parameter) {
|
private boolean hasSelector(Parameter parameter) {
|
||||||
return parameter.getAnnotation(Selector.class) != null;
|
return parameter.getAnnotation(Selector.class) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String slashName(Parameter parameter) {
|
|
||||||
return "/{" + parameter.getName() + "}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private Collection<String> getConsumes(WebEndpointHttpMethod httpMethod, Method method) {
|
private Collection<String> getConsumes(WebEndpointHttpMethod httpMethod, Method method) {
|
||||||
if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) {
|
if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) {
|
||||||
return this.endpointMediaTypes.getConsumed();
|
return this.endpointMediaTypes.getConsumed();
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.web.jersey;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -50,6 +51,7 @@ import org.springframework.boot.actuate.endpoint.web.Link;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
@ -90,7 +92,13 @@ public class JerseyEndpointResourceFactory {
|
||||||
|
|
||||||
private Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
|
private Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
|
||||||
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
|
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
|
||||||
Builder resourceBuilder = Resource.builder().path(endpointMapping.createSubPath(requestPredicate.getPath()));
|
String path = requestPredicate.getPath();
|
||||||
|
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
|
||||||
|
if (matchAllRemainingPathSegmentsVariable != null) {
|
||||||
|
path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}",
|
||||||
|
"{" + matchAllRemainingPathSegmentsVariable + ": .*}");
|
||||||
|
}
|
||||||
|
Builder resourceBuilder = Resource.builder().path(endpointMapping.createSubPath(path));
|
||||||
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
|
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
|
||||||
.consumes(StringUtils.toStringArray(requestPredicate.getConsumes()))
|
.consumes(StringUtils.toStringArray(requestPredicate.getConsumes()))
|
||||||
.produces(StringUtils.toStringArray(requestPredicate.getProduces()))
|
.produces(StringUtils.toStringArray(requestPredicate.getProduces()))
|
||||||
|
@ -111,6 +119,8 @@ public class JerseyEndpointResourceFactory {
|
||||||
*/
|
*/
|
||||||
private static final class OperationInflector implements Inflector<ContainerRequestContext, Object> {
|
private static final class OperationInflector implements Inflector<ContainerRequestContext, Object> {
|
||||||
|
|
||||||
|
private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
|
||||||
|
|
||||||
private static final List<Function<Object, Object>> BODY_CONVERTERS;
|
private static final List<Function<Object, Object>> BODY_CONVERTERS;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
@ -159,7 +169,24 @@ public class JerseyEndpointResourceFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> extractPathParameters(ContainerRequestContext requestContext) {
|
private Map<String, Object> extractPathParameters(ContainerRequestContext requestContext) {
|
||||||
return extract(requestContext.getUriInfo().getPathParameters());
|
Map<String, Object> pathParameters = extract(requestContext.getUriInfo().getPathParameters());
|
||||||
|
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
|
||||||
|
.getMatchAllRemainingPathSegmentsVariable();
|
||||||
|
if (matchAllRemainingPathSegmentsVariable != null) {
|
||||||
|
String remainingPathSegments = (String) pathParameters.get(matchAllRemainingPathSegmentsVariable);
|
||||||
|
pathParameters.put(matchAllRemainingPathSegmentsVariable, tokenizePathSegments(remainingPathSegments));
|
||||||
|
}
|
||||||
|
return pathParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] tokenizePathSegments(String path) {
|
||||||
|
String[] segments = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR, false, true);
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
if (segments[i].contains("%")) {
|
||||||
|
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> extractQueryParameters(ContainerRequestContext requestContext) {
|
private Map<String, Object> extractQueryParameters(ContainerRequestContext requestContext) {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.boot.actuate.endpoint.web.reactive;
|
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.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -47,6 +48,7 @@ import org.springframework.security.access.SecurityConfig;
|
||||||
import org.springframework.security.access.vote.RoleVoter;
|
import org.springframework.security.access.vote.RoleVoter;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
@ -264,15 +266,17 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||||
*/
|
*/
|
||||||
private static final class ReactiveWebOperationAdapter implements ReactiveWebOperation {
|
private static final class ReactiveWebOperationAdapter implements ReactiveWebOperation {
|
||||||
|
|
||||||
private final OperationInvoker invoker;
|
private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
|
||||||
|
|
||||||
private final String operationId;
|
private final WebOperation operation;
|
||||||
|
|
||||||
|
private final OperationInvoker invoker;
|
||||||
|
|
||||||
private final Supplier<Mono<? extends SecurityContext>> securityContextSupplier;
|
private final Supplier<Mono<? extends SecurityContext>> securityContextSupplier;
|
||||||
|
|
||||||
private ReactiveWebOperationAdapter(WebOperation operation) {
|
private ReactiveWebOperationAdapter(WebOperation operation) {
|
||||||
|
this.operation = operation;
|
||||||
this.invoker = getInvoker(operation);
|
this.invoker = getInvoker(operation);
|
||||||
this.operationId = operation.getId();
|
|
||||||
this.securityContextSupplier = getSecurityContextSupplier();
|
this.securityContextSupplier = getSecurityContextSupplier();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,12 +309,28 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||||
@Override
|
@Override
|
||||||
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body) {
|
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body) {
|
||||||
Map<String, Object> arguments = getArguments(exchange, body);
|
Map<String, Object> arguments = getArguments(exchange, body);
|
||||||
|
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
|
||||||
|
.getMatchAllRemainingPathSegmentsVariable();
|
||||||
|
if (matchAllRemainingPathSegmentsVariable != null) {
|
||||||
|
arguments.put(matchAllRemainingPathSegmentsVariable,
|
||||||
|
tokenizePathSegments((String) arguments.get(matchAllRemainingPathSegmentsVariable)));
|
||||||
|
}
|
||||||
return this.securityContextSupplier.get()
|
return this.securityContextSupplier.get()
|
||||||
.map((securityContext) -> new InvocationContext(securityContext, arguments))
|
.map((securityContext) -> new InvocationContext(securityContext, arguments))
|
||||||
.flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
|
.flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
|
||||||
exchange.getRequest().getMethod()));
|
exchange.getRequest().getMethod()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] tokenizePathSegments(String path) {
|
||||||
|
String[] segments = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR, false, true);
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
if (segments[i].contains("%")) {
|
||||||
|
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> getArguments(ServerWebExchange exchange, Map<String, String> body) {
|
private Map<String, Object> getArguments(ServerWebExchange exchange, Map<String, String> body) {
|
||||||
Map<String, Object> arguments = new LinkedHashMap<>();
|
Map<String, Object> arguments = new LinkedHashMap<>();
|
||||||
arguments.putAll(getTemplateVariables(exchange));
|
arguments.putAll(getTemplateVariables(exchange));
|
||||||
|
@ -345,7 +365,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Actuator web endpoint '" + this.operationId + "'";
|
return "Actuator web endpoint '" + this.operation.getId() + "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.boot.actuate.endpoint.web.servlet;
|
package org.springframework.boot.actuate.endpoint.web.servlet;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -42,6 +43,8 @@ import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicat
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
@ -162,9 +165,15 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
|
private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
|
||||||
|
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
||||||
|
String path = predicate.getPath();
|
||||||
|
String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
|
||||||
|
if (matchAllRemainingPathSegmentsVariable != null) {
|
||||||
|
path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}", "**");
|
||||||
|
}
|
||||||
ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, operation,
|
ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, operation,
|
||||||
new ServletWebOperationAdapter(operation));
|
new ServletWebOperationAdapter(operation));
|
||||||
registerMapping(createRequestMappingInfo(operation), new OperationHandler(servletWebOperation),
|
registerMapping(createRequestMappingInfo(predicate, path), new OperationHandler(servletWebOperation),
|
||||||
this.handleMethod);
|
this.handleMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,9 +190,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||||
return servletWebOperation;
|
return servletWebOperation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestMappingInfo createRequestMappingInfo(WebOperation operation) {
|
private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
|
||||||
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
PatternsRequestCondition patterns = patternsRequestConditionForPattern(path);
|
||||||
PatternsRequestCondition patterns = patternsRequestConditionForPattern(predicate.getPath());
|
|
||||||
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
|
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
|
||||||
RequestMethod.valueOf(predicate.getHttpMethod().name()));
|
RequestMethod.valueOf(predicate.getHttpMethod().name()));
|
||||||
ConsumesRequestCondition consumes = new ConsumesRequestCondition(
|
ConsumesRequestCondition consumes = new ConsumesRequestCondition(
|
||||||
|
@ -275,6 +283,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||||
*/
|
*/
|
||||||
private class ServletWebOperationAdapter implements ServletWebOperation {
|
private class ServletWebOperationAdapter implements ServletWebOperation {
|
||||||
|
|
||||||
|
private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
|
||||||
|
|
||||||
private final WebOperation operation;
|
private final WebOperation operation;
|
||||||
|
|
||||||
ServletWebOperationAdapter(WebOperation operation) {
|
ServletWebOperationAdapter(WebOperation operation) {
|
||||||
|
@ -302,6 +312,11 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||||
private Map<String, Object> getArguments(HttpServletRequest request, Map<String, String> body) {
|
private Map<String, Object> getArguments(HttpServletRequest request, Map<String, String> body) {
|
||||||
Map<String, Object> arguments = new LinkedHashMap<>();
|
Map<String, Object> arguments = new LinkedHashMap<>();
|
||||||
arguments.putAll(getTemplateVariables(request));
|
arguments.putAll(getTemplateVariables(request));
|
||||||
|
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
|
||||||
|
.getMatchAllRemainingPathSegmentsVariable();
|
||||||
|
if (matchAllRemainingPathSegmentsVariable != null) {
|
||||||
|
arguments.put(matchAllRemainingPathSegmentsVariable, getRemainingPathSegments(request));
|
||||||
|
}
|
||||||
if (body != null && HttpMethod.POST.name().equals(request.getMethod())) {
|
if (body != null && HttpMethod.POST.name().equals(request.getMethod())) {
|
||||||
arguments.putAll(body);
|
arguments.putAll(body);
|
||||||
}
|
}
|
||||||
|
@ -310,6 +325,30 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||||
return arguments;
|
return arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Object getRemainingPathSegments(HttpServletRequest request) {
|
||||||
|
String[] pathTokens = tokenize(request, HandlerMapping.LOOKUP_PATH, true);
|
||||||
|
String[] patternTokens = tokenize(request, HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, false);
|
||||||
|
int numberOfRemainingPathSegments = pathTokens.length - patternTokens.length + 1;
|
||||||
|
Assert.state(numberOfRemainingPathSegments >= 0, "Unable to extract remaining path segments");
|
||||||
|
String[] remainingPathSegments = new String[numberOfRemainingPathSegments];
|
||||||
|
System.arraycopy(pathTokens, patternTokens.length - 1, remainingPathSegments, 0,
|
||||||
|
numberOfRemainingPathSegments);
|
||||||
|
return remainingPathSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] tokenize(HttpServletRequest request, String attributeName, boolean decode) {
|
||||||
|
String value = (String) request.getAttribute(attributeName);
|
||||||
|
String[] segments = StringUtils.tokenizeToStringArray(value, PATH_SEPARATOR, false, true);
|
||||||
|
if (decode) {
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
if (segments[i].contains("%")) {
|
||||||
|
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private Map<String, String> getTemplateVariables(HttpServletRequest request) {
|
private Map<String, String> getTemplateVariables(HttpServletRequest request) {
|
||||||
return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
|
return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
|
||||||
|
|
|
@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* Tests for {@link WebOperationRequestPredicate}.
|
* Tests for {@link WebOperationRequestPredicate}.
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
class WebOperationRequestPredicateTests {
|
class WebOperationRequestPredicateTests {
|
||||||
|
|
||||||
|
@ -54,12 +55,37 @@ class WebOperationRequestPredicateTests {
|
||||||
assertThat(predicateWithPath("/path/{foo1}")).isEqualTo(predicateWithPath("/path/{foo2}"));
|
assertThat(predicateWithPath("/path/{foo1}")).isEqualTo(predicateWithPath("/path/{foo2}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void predicatesWithSingleWildcardPathVariablesInTheSamplePlaceAreEqual() {
|
||||||
|
assertThat(predicateWithPath("/path/{*foo1}")).isEqualTo(predicateWithPath("/path/{*foo2}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void predicatesWithSingleWildcardPathVariableAndRegularVariableInTheSamplePlaceAreNotEqual() {
|
||||||
|
assertThat(predicateWithPath("/path/{*foo1}")).isNotEqualTo(predicateWithPath("/path/{foo2}"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void predicatesWithMultiplePathVariablesInTheSamplePlaceAreEqual() {
|
void predicatesWithMultiplePathVariablesInTheSamplePlaceAreEqual() {
|
||||||
assertThat(predicateWithPath("/path/{foo1}/more/{bar1}"))
|
assertThat(predicateWithPath("/path/{foo1}/more/{bar1}"))
|
||||||
.isEqualTo(predicateWithPath("/path/{foo2}/more/{bar2}"));
|
.isEqualTo(predicateWithPath("/path/{foo2}/more/{bar2}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void predicateWithWildcardPathVariableReturnsMatchAllRemainingPathSegmentsVariable() {
|
||||||
|
assertThat(predicateWithPath("/path/{*foo1}").getMatchAllRemainingPathSegmentsVariable()).isEqualTo("foo1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void predicateWithRegularPathVariableDoesNotReturnMatchAllRemainingPathSegmentsVariable() {
|
||||||
|
assertThat(predicateWithPath("/path/{foo1}").getMatchAllRemainingPathSegmentsVariable()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void predicateWithNoPathVariableDoesNotReturnMatchAllRemainingPathSegmentsVariable() {
|
||||||
|
assertThat(predicateWithPath("/path/foo1").getMatchAllRemainingPathSegmentsVariable()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
private WebOperationRequestPredicate predicateWithPath(String path) {
|
private WebOperationRequestPredicate predicateWithPath(String path) {
|
||||||
return new WebOperationRequestPredicate(path, WebEndpointHttpMethod.GET, Collections.emptyList(),
|
return new WebOperationRequestPredicate(path, WebEndpointHttpMethod.GET, Collections.emptyList(),
|
||||||
Collections.emptyList());
|
Collections.emptyList());
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
|
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
@ -50,6 +51,7 @@ import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -124,6 +126,20 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
||||||
.expectBody().jsonPath("All").isEqualTo(true));
|
.expectBody().jsonPath("All").isEqualTo(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchAllRemainingPathsSelectorShouldMatchFullPath() {
|
||||||
|
load(MatchAllRemainingEndpointConfiguration.class,
|
||||||
|
(client) -> client.get().uri("/matchallremaining/one/two/three").exchange().expectStatus().isOk()
|
||||||
|
.expectBody().jsonPath("selection").isEqualTo("one|two|three"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchAllRemainingPathsSelectorShouldDecodePath() {
|
||||||
|
load(MatchAllRemainingEndpointConfiguration.class,
|
||||||
|
(client) -> client.get().uri("/matchallremaining/one/two%20three/").exchange().expectStatus().isOk()
|
||||||
|
.expectBody().jsonPath("selection").isEqualTo("one|two three"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void readOperationWithSingleQueryParameters() {
|
void readOperationWithSingleQueryParameters() {
|
||||||
load(QueryEndpointConfiguration.class, (client) -> client.get().uri("/query?one=1&two=2").exchange()
|
load(QueryEndpointConfiguration.class, (client) -> client.get().uri("/query?one=1&two=2").exchange()
|
||||||
|
@ -418,6 +434,17 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@Import(BaseConfiguration.class)
|
||||||
|
static class MatchAllRemainingEndpointConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
MatchAllRemainingEndpoint matchAllRemainingEndpoint() {
|
||||||
|
return new MatchAllRemainingEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@Import(BaseConfiguration.class)
|
@Import(BaseConfiguration.class)
|
||||||
static class QueryEndpointConfiguration {
|
static class QueryEndpointConfiguration {
|
||||||
|
@ -625,6 +652,16 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Endpoint(id = "matchallremaining")
|
||||||
|
static class MatchAllRemainingEndpoint {
|
||||||
|
|
||||||
|
@ReadOperation
|
||||||
|
Map<String, String> select(@Selector(match = Match.ALL_REMAINING) String... selection) {
|
||||||
|
return Collections.singletonMap("selection", StringUtils.arrayToDelimitedString(selection, "|"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Endpoint(id = "query")
|
@Endpoint(id = "query")
|
||||||
static class QueryEndpoint {
|
static class QueryEndpoint {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2019 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.endpoint.web.annotation;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
|
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||||
|
import org.springframework.core.annotation.AnnotationAttributes;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link RequestPredicateFactory}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class RequestPredicateFactoryTests {
|
||||||
|
|
||||||
|
private final RequestPredicateFactory factory = new RequestPredicateFactory(
|
||||||
|
new EndpointMediaTypes(Collections.emptyList(), Collections.emptyList()));
|
||||||
|
|
||||||
|
private String rootPath = "/root";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRequestPredicateWhenHasMoreThanOneMatchAllThrowsException() {
|
||||||
|
DiscoveredOperationMethod operationMethod = getDiscoveredOperationMethod(MoreThanOneMatchAll.class);
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy(() -> this.factory.getRequestPredicate(this.rootPath, operationMethod))
|
||||||
|
.withMessage("@Selector annotation with Match.ALL_REMAINING must be unique");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRequestPredicateWhenMatchAllIsNotLastParameterThrowsException() {
|
||||||
|
DiscoveredOperationMethod operationMethod = getDiscoveredOperationMethod(MatchAllIsNotLastParameter.class);
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy(() -> this.factory.getRequestPredicate(this.rootPath, operationMethod))
|
||||||
|
.withMessage("@Selector annotation with Match.ALL_REMAINING must be the last parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRequestPredicateReturnsRedicateWithPath() {
|
||||||
|
DiscoveredOperationMethod operationMethod = getDiscoveredOperationMethod(ValidSelectors.class);
|
||||||
|
WebOperationRequestPredicate requestPredicate = this.factory.getRequestPredicate(this.rootPath,
|
||||||
|
operationMethod);
|
||||||
|
assertThat(requestPredicate.getPath()).isEqualTo("/root/{one}/{*two}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiscoveredOperationMethod getDiscoveredOperationMethod(Class<?> source) {
|
||||||
|
Method method = source.getDeclaredMethods()[0];
|
||||||
|
AnnotationAttributes attributes = new AnnotationAttributes();
|
||||||
|
attributes.put("produces", "application/json");
|
||||||
|
return new DiscoveredOperationMethod(method, OperationType.READ, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MoreThanOneMatchAll {
|
||||||
|
|
||||||
|
void test(@Selector(match = Match.ALL_REMAINING) String[] one,
|
||||||
|
@Selector(match = Match.ALL_REMAINING) String[] two) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MatchAllIsNotLastParameter {
|
||||||
|
|
||||||
|
void test(@Selector(match = Match.ALL_REMAINING) String[] one, @Selector String[] two) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ValidSelectors {
|
||||||
|
|
||||||
|
void test(@Selector String[] one, @Selector(match = Match.ALL_REMAINING) String[] two) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -577,7 +577,6 @@ endpoint.
|
||||||
|
|
||||||
[[production-ready-endpoints-custom-web-predicate-path]]
|
[[production-ready-endpoints-custom-web-predicate-path]]
|
||||||
===== Path
|
===== Path
|
||||||
|
|
||||||
The path of the predicate is determined by the ID of the endpoint and the base path of
|
The path of the predicate is determined by the ID of the endpoint and the base path of
|
||||||
web-exposed endpoints. The default base path is `/actuator`. For example, an endpoint with
|
web-exposed endpoints. The default base path is `/actuator`. For example, an endpoint with
|
||||||
the ID `sessions` will use `/actuator/sessions` as its path in the predicate.
|
the ID `sessions` will use `/actuator/sessions` as its path in the predicate.
|
||||||
|
@ -585,13 +584,14 @@ the ID `sessions` will use `/actuator/sessions` as its path in the predicate.
|
||||||
The path can be further customized by annotating one or more parameters of the operation
|
The path can be further customized by annotating one or more parameters of the operation
|
||||||
method with `@Selector`. Such a parameter is added to the path predicate as a path
|
method with `@Selector`. Such a parameter is added to the path predicate as a path
|
||||||
variable. The variable's value is passed into the operation method when the endpoint
|
variable. The variable's value is passed into the operation method when the endpoint
|
||||||
operation is invoked.
|
operation is invoked. If you want to capture all remaining path elements, you can add
|
||||||
|
`@Selector(Match=ALL_REMAINING)` to the last parameter and make it a type that is
|
||||||
|
conversion compatible with a `String[]`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[[production-ready-endpoints-custom-web-predicate-http-method]]
|
[[production-ready-endpoints-custom-web-predicate-http-method]]
|
||||||
===== HTTP method
|
===== HTTP method
|
||||||
|
|
||||||
The HTTP method of the predicate is determined by the operation type, as shown in
|
The HTTP method of the predicate is determined by the operation type, as shown in
|
||||||
the following table:
|
the following table:
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue