Allows multiple patterns in Path Predicate.
fixes gh-256
This commit is contained in:
@@ -171,7 +171,7 @@ spring:
|
||||
This route would match if the request method was a `GET`.
|
||||
|
||||
=== Path Route Predicate Factory
|
||||
The Path Route Predicate Factory takes one parameter: a Spring `PathMatcher` pattern.
|
||||
The Path Route Predicate Factory takes two parameter: a list of Spring `PathMatcher` patterns and an optional flag to `matchOptionalTrailingSeparator`.
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
@@ -183,10 +183,10 @@ spring:
|
||||
- id: host_route
|
||||
uri: http://example.org
|
||||
predicates:
|
||||
- Path=/foo/{segment}
|
||||
- Path=/foo/{segment},/bar/{segment}
|
||||
----
|
||||
|
||||
This route would match if the request path was, for example: `/foo/1` or `/foo/bar`.
|
||||
This route would match if the request path was, for example: `/foo/1` or `/foo/bar` or `/bar/baz`.
|
||||
|
||||
This predicate extracts the URI template variables (like `segment` defined in the example above) as a map of names and values and places it in the `ServerWebExchange.getAttributes()` with a key defined in `PathRoutePredicate.URL_PREDICATE_VARS_ATTR`. Those values are then available for use by <<gateway-route-filters,GatewayFilter Factories>>
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
package org.springframework.cloud.gateway.handler.predicate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -26,6 +28,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
@@ -54,25 +57,39 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
|
||||
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return Arrays.asList(PATTERN_KEY, MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY);
|
||||
return Arrays.asList("patterns", MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShortcutType shortcutType() {
|
||||
return ShortcutType.GATHER_LIST_TAIL_FLAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate<ServerWebExchange> apply(Config config) {
|
||||
final ArrayList<PathPattern> pathPatterns = new ArrayList<>();
|
||||
synchronized (this.pathPatternParser) {
|
||||
pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchOptionalTrailingSeparator());
|
||||
config.pathPattern = this.pathPatternParser.parse(config.pattern);
|
||||
config.getPatterns().forEach(pattern -> {
|
||||
PathPattern pathPattern = this.pathPatternParser.parse(pattern);
|
||||
pathPatterns.add(pathPattern);
|
||||
});
|
||||
}
|
||||
return exchange -> {
|
||||
PathContainer path = parsePath(exchange.getRequest().getURI().getPath());
|
||||
|
||||
boolean match = config.pathPattern.matches(path);
|
||||
traceMatch("Pattern", config.pathPattern.getPatternString(), path, match);
|
||||
if (match) {
|
||||
PathMatchInfo uriTemplateVariables = config.pathPattern.matchAndExtract(path);
|
||||
Optional<PathPattern> optionalPathPattern = pathPatterns.stream()
|
||||
.filter(pattern -> pattern.matches(path))
|
||||
.findFirst();
|
||||
|
||||
if (optionalPathPattern.isPresent()) {
|
||||
PathPattern pathPattern = optionalPathPattern.get();
|
||||
traceMatch("Pattern", pathPattern.getPatternString(), path, true);
|
||||
PathMatchInfo uriTemplateVariables = pathPattern.matchAndExtract(path);
|
||||
exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
|
||||
return true;
|
||||
} else {
|
||||
traceMatch("Pattern", config.getPatterns(), path, false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -88,16 +105,30 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
|
||||
|
||||
@Validated
|
||||
public static class Config {
|
||||
private String pattern;
|
||||
private PathPattern pathPattern;
|
||||
private List<String> patterns = new ArrayList<>();
|
||||
private boolean matchOptionalTrailingSeparator = true;
|
||||
|
||||
@Deprecated
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
if (!CollectionUtils.isEmpty(this.patterns)) {
|
||||
return patterns.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Config setPattern(String pattern) {
|
||||
this.pattern = pattern;
|
||||
this.patterns = new ArrayList<>();
|
||||
this.patterns.add(pattern);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getPatterns() {
|
||||
return patterns;
|
||||
}
|
||||
|
||||
public Config setPatterns(List<String> patterns) {
|
||||
this.patterns = patterns;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -113,7 +144,7 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("pattern", pattern)
|
||||
.append("patterns", patterns)
|
||||
.append("matchOptionalTrailingSeparator", matchOptionalTrailingSeparator)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.route.builder;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.cloud.gateway.handler.AsyncPredicate;
|
||||
@@ -162,13 +163,13 @@ public class PredicateSpec extends UriSpec {
|
||||
|
||||
/**
|
||||
* A predicate that checks if the path of the request matches the given pattern
|
||||
* @param pattern the pattern to check the path against.
|
||||
* @param patterns the pattern to check the path against.
|
||||
* The pattern is a {@link org.springframework.util.PathMatcher} pattern
|
||||
* @return a {@link BooleanSpec} to be used to add logical operators
|
||||
*/
|
||||
public BooleanSpec path(String pattern) {
|
||||
public BooleanSpec path(String... patterns) {
|
||||
return asyncPredicate(getBean(PathRoutePredicateFactory.class)
|
||||
.applyAsync(c -> c.setPattern(pattern)));
|
||||
.applyAsync(c -> c.setPatterns(Arrays.asList(patterns))));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,9 +180,25 @@ public class PredicateSpec extends UriSpec {
|
||||
* when there is a trailing <code>/</code>
|
||||
* @return a {@link BooleanSpec} to be used to add logical operators
|
||||
*/
|
||||
@Deprecated
|
||||
public BooleanSpec path(String pattern, boolean matchOptionalTrailingSeparator) {
|
||||
return asyncPredicate(getBean(PathRoutePredicateFactory.class)
|
||||
.applyAsync(c -> c.setPattern(pattern).setMatchOptionalTrailingSeparator(matchOptionalTrailingSeparator)));
|
||||
.applyAsync(c -> c.setPatterns(Collections.singletonList(pattern))
|
||||
.setMatchOptionalTrailingSeparator(matchOptionalTrailingSeparator)));
|
||||
}
|
||||
|
||||
/**
|
||||
* A predicate that checks if the path of the request matches the given pattern
|
||||
* @param patterns the pattern to check the path against.
|
||||
* The pattern is a {@link org.springframework.util.PathMatcher} pattern
|
||||
* @param matchOptionalTrailingSeparator set to false if you do not want this path to match
|
||||
* when there is a trailing <code>/</code>
|
||||
* @return a {@link BooleanSpec} to be used to add logical operators
|
||||
*/
|
||||
public BooleanSpec path(boolean matchOptionalTrailingSeparator, String... patterns) {
|
||||
return asyncPredicate(getBean(PathRoutePredicateFactory.class)
|
||||
.applyAsync(c -> c.setPatterns(Arrays.asList(patterns))
|
||||
.setMatchOptionalTrailingSeparator(matchOptionalTrailingSeparator)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -68,6 +69,35 @@ public interface ShortcutConfigurable {
|
||||
.collect(Collectors.toList()));
|
||||
return map;
|
||||
}
|
||||
},
|
||||
|
||||
// list is all elements except last which is a boolean flag
|
||||
GATHER_LIST_TAIL_FLAG {
|
||||
@Override
|
||||
public Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf, SpelExpressionParser parser, BeanFactory beanFactory) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
// field order should be of size 1
|
||||
List<String> fieldOrder = shortcutConf.shortcutFieldOrder();
|
||||
Assert.isTrue(fieldOrder != null
|
||||
&& fieldOrder.size() == 2,
|
||||
"Shortcut Configuration Type GATHER_LIST_HEAD must have shortcutFieldOrder of size 2");
|
||||
List<String> values = new ArrayList<>(args.values());
|
||||
if (!values.isEmpty()) {
|
||||
// strip boolean flag if last entry is true or false
|
||||
int lastIdx = values.size() - 1;
|
||||
String lastValue = values.get(lastIdx);
|
||||
if (lastValue.equalsIgnoreCase("true")
|
||||
|| lastValue.equalsIgnoreCase("false")) {
|
||||
values = values.subList(0, lastIdx);
|
||||
map.put(fieldOrder.get(1), getValue(parser, beanFactory, lastValue));
|
||||
}
|
||||
}
|
||||
String fieldName = fieldOrder.get(0);
|
||||
map.put(fieldName, values.stream()
|
||||
.map(value -> getValue(parser, beanFactory, value))
|
||||
.collect(Collectors.toList()));
|
||||
return map;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf,
|
||||
|
||||
@@ -19,11 +19,15 @@ package org.springframework.cloud.gateway.handler.predicate;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -38,12 +42,7 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
public void pathRouteWorks() {
|
||||
testClient.get().uri("/abc/123/function")
|
||||
.header(HttpHeaders.HOST, "www.path.org")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
|
||||
.expectHeader().valueEquals(ROUTE_ID_HEADER, "path_test");
|
||||
expectPathRoute("/abc/123/function", "www.path.org", "path_test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,16 +56,49 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
public void defaultPathRouteWorks() {
|
||||
testClient.get().uri("/get")
|
||||
expectPathRoute("/get", "www.thispathshouldnotmatch.org", "default_path_to_httpbin");
|
||||
}
|
||||
|
||||
private void expectPathRoute(String uri, String host, String routeId) {
|
||||
testClient.get().uri(uri)
|
||||
.header(HttpHeaders.HOST, host)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
|
||||
.expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin");
|
||||
.expectHeader().valueEquals(ROUTE_ID_HEADER, routeId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mulitPathRouteWorks() {
|
||||
expectPathRoute("/anything/multi11", "www.pathmulti.org", "path_multi");
|
||||
expectPathRoute("/anything/multi22", "www.pathmulti.org", "path_multi");
|
||||
expectPathRoute("/anything/multi33", "www.pathmulti.org", "default_path_to_httpbin");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mulitPathDslRouteWorks() {
|
||||
expectPathRoute("/anything/multidsl1", "www.pathmultidsl.org", "path_multi_dsl");
|
||||
expectPathRoute("/anything/multidsl2", "www.pathmultidsl.org", "default_path_to_httpbin");
|
||||
expectPathRoute("/anything/multidsl3", "www.pathmultidsl.org", "path_multi_dsl");
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
public static class TestConfig { }
|
||||
public static class TestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
String uri;
|
||||
|
||||
@Bean
|
||||
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("path_multi_dsl", r -> r.host("**.pathmultidsl.org")
|
||||
.and().path(false, "/anything/multidsl1", "/anything/multidsl3")
|
||||
.filters(f -> f.prefixPath("/httpbin"))
|
||||
.uri(uri))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -87,6 +87,48 @@ public class ShortcutConfigurableTests {
|
||||
.containsExactly(42, "val1", "val2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeGatherListTailFlagFlagExists() {
|
||||
assertListTailFlag(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeGatherListTailFlagFlagMissing() {
|
||||
assertListTailFlag(false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void assertListTailFlag(boolean hasTailFlag) {
|
||||
parser = new SpelExpressionParser();
|
||||
ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() {
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return Arrays.asList("values", "flag");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShortcutType shortcutType() {
|
||||
return ShortcutType.GATHER_LIST_TAIL_FLAG;
|
||||
}
|
||||
};
|
||||
Map<String, String> args = new HashMap<>();
|
||||
args.put("1", "val0");
|
||||
args.put("2", "val1");
|
||||
args.put("3", "val2");
|
||||
if (hasTailFlag) {
|
||||
args.put("4", "false");
|
||||
}
|
||||
Map<String, Object> map = ShortcutType.GATHER_LIST_TAIL_FLAG.normalize(args, shortcutConfigurable, parser, this.beanFactory);
|
||||
assertThat(map).isNotNull().containsKey("values");
|
||||
assertThat((List)map.get("values"))
|
||||
.containsExactly("val0", "val1", "val2");
|
||||
if (hasTailFlag) {
|
||||
assertThat(map.get("flag")).isEqualTo("false");
|
||||
} else {
|
||||
assertThat(map).doesNotContainKeys("flag");
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
protected static class TestConfig {
|
||||
@Bean
|
||||
|
||||
@@ -183,6 +183,15 @@ spring:
|
||||
filters:
|
||||
- SetStatus=404
|
||||
|
||||
# =====================================
|
||||
- id: path_multi
|
||||
uri: ${test.uri}
|
||||
predicates:
|
||||
- Host=**.pathmulti.org
|
||||
- Path=/anything/multi1{num},/anything/multi2{num}
|
||||
filters:
|
||||
- SetPath=/anything/multi{num}
|
||||
|
||||
# =====================================
|
||||
- id: redirect_to_test
|
||||
uri: ${test.uri}
|
||||
|
||||
Reference in New Issue
Block a user