diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 7d6e28b4..5f221f72 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -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 <> diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java index d8d64ed9..47c7f7bc 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java @@ -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 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 apply(Config config) { + final ArrayList 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 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 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 getPatterns() { + return patterns; + } + + public Config setPatterns(List patterns) { + this.patterns = patterns; return this; } @@ -113,7 +144,7 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory c.setPattern(pattern))); + .applyAsync(c -> c.setPatterns(Arrays.asList(patterns)))); } /** @@ -179,9 +180,25 @@ public class PredicateSpec extends UriSpec { * when there is a trailing / * @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 / + * @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))); } /** diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java index 13b3ead8..43d09a5f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java @@ -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 normalize(Map args, ShortcutConfigurable shortcutConf, SpelExpressionParser parser, BeanFactory beanFactory) { + Map map = new HashMap<>(); + // field order should be of size 1 + List fieldOrder = shortcutConf.shortcutFieldOrder(); + Assert.isTrue(fieldOrder != null + && fieldOrder.size() == 2, + "Shortcut Configuration Type GATHER_LIST_HEAD must have shortcutFieldOrder of size 2"); + List 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 normalize(Map args, ShortcutConfigurable shortcutConf, diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java index e7fff517..d371b678 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java @@ -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(); + } + } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java index fbffc18b..ae2d1203 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java @@ -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 shortcutFieldOrder() { + return Arrays.asList("values", "flag"); + } + + @Override + public ShortcutType shortcutType() { + return ShortcutType.GATHER_LIST_TAIL_FLAG; + } + }; + Map args = new HashMap<>(); + args.put("1", "val0"); + args.put("2", "val1"); + args.put("3", "val2"); + if (hasTailFlag) { + args.put("4", "false"); + } + Map 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 diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 2687cdf9..31e846f4 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -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}