Allows multiple patterns in Path Predicate.

fixes gh-256
This commit is contained in:
Spencer Gibb
2018-12-10 17:22:27 -05:00
parent 4ea874a3c6
commit 7a5502fd7a
7 changed files with 188 additions and 27 deletions

View File

@@ -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>>

View File

@@ -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();
}

View File

@@ -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)));
}
/**

View File

@@ -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,

View File

@@ -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();
}
}
}

View File

@@ -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

View File

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