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:
Phillip Webb
2019-07-31 12:33:59 +01:00
parent b8bda1c03d
commit 890ea153bf
10 changed files with 356 additions and 25 deletions

View File

@@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link WebOperationRequestPredicate}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
class WebOperationRequestPredicateTests {
@@ -54,12 +55,37 @@ class WebOperationRequestPredicateTests {
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
void predicatesWithMultiplePathVariablesInTheSamplePlaceAreEqual() {
assertThat(predicateWithPath("/path/{foo1}/more/{bar1}"))
.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) {
return new WebOperationRequestPredicate(path, WebEndpointHttpMethod.GET, Collections.emptyList(),
Collections.emptyList());

View File

@@ -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.ReadOperation;
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.web.WebEndpointResponse;
import org.springframework.context.ApplicationContext;
@@ -50,6 +51,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
@@ -124,6 +126,20 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
.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
void readOperationWithSingleQueryParameters() {
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)
@Import(BaseConfiguration.class)
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")
static class QueryEndpoint {

View File

@@ -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) {
}
}
}