From 316a680577a392464c6da60d989b00acd2a5e472 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Fri, 14 Apr 2017 07:48:51 -0700 Subject: [PATCH] Modify getPathRemaining to return remaining path and bound variables Without this change it was necessary to call getPathRemaining and then chop up the path and make a call to matchAndExtract to get the bound variables for the path part that matched. With this change this is all done in the call to getPathRemaining which returns an object holding the remaining path and the bound variables. Issue: SPR-15419 --- .../web/util/patterns/PathPattern.java | 23 ++--- .../util/patterns/PathRemainingMatchInfo.java | 58 +++++++++++++ .../patterns/PathPatternMatcherTests.java | 87 +++++++++++++------ .../function/server/RequestPredicates.java | 4 +- 4 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PathRemainingMatchInfo.java diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java index 68fa32ec6c0..4a7b1308809 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java @@ -151,35 +151,37 @@ public class PathPattern implements Comparable { * For a given path return the remaining piece that is not covered by this PathPattern. * * @param path a path that may or may not match this path pattern - * @return the remaining path after as much has been consumed as possible by this pattern, - * result can be the empty string if the path is entirely consumed or it will be null - * if the path does not match + * @return a {@link PathRemainingMatchInfo} describing the match result or null if the path does not match + * this pattern */ - public String getPathRemaining(String path) { + public PathRemainingMatchInfo getPathRemaining(String path) { if (head == null) { if (path == null) { - return path; + return new PathRemainingMatchInfo(path); } else { - return hasLength(path)?path:""; + return new PathRemainingMatchInfo(hasLength(path)?path:""); } } else if (!hasLength(path)) { return null; } - MatchingContext matchingContext = new MatchingContext(path, false); + MatchingContext matchingContext = new MatchingContext(path, true); matchingContext.setMatchAllowExtraPath(); boolean matches = head.matches(0, matchingContext); if (!matches) { return null; } else { + PathRemainingMatchInfo info; if (matchingContext.remainingPathIndex == path.length()) { - return ""; + info = new PathRemainingMatchInfo("", matchingContext.getExtractedVariables()); } else { - return path.substring(matchingContext.remainingPathIndex); + info = new PathRemainingMatchInfo(path.substring(matchingContext.remainingPathIndex), + matchingContext.getExtractedVariables()); } + return info; } } @@ -200,8 +202,9 @@ public class PathPattern implements Comparable { } /** - * @param path a path to match against this pattern + * @param path a path that matches this pattern from which to extract variables * @return a map of extracted variables - an empty map if no variables extracted. + * @throws IllegalStateException if the path does not match the pattern */ public Map matchAndExtract(String path) { MatchingContext matchingContext = new MatchingContext(path, true); diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathRemainingMatchInfo.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PathRemainingMatchInfo.java new file mode 100644 index 00000000000..a823d94244f --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathRemainingMatchInfo.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.web.util.patterns; + +import java.util.Map; + +/** + * A holder for the result of a {@link PathPattern#getPathRemaining(String)} call. Holds + * information on the path left after the first part has successfully matched a pattern + * and any variables bound in that first part that matched. + * + * @author Andy Clement + * @since 5.0 + */ +public class PathRemainingMatchInfo { + + private String pathRemaining; + + private Map matchingVariables; + + PathRemainingMatchInfo(String pathRemaining) { + this.pathRemaining = pathRemaining; + } + + PathRemainingMatchInfo(String pathRemaining, Map matchingVariables) { + this.pathRemaining = pathRemaining; + this.matchingVariables = matchingVariables; + } + + /** + * @return the part of a path that was not matched by a pattern + */ + public String getPathRemaining() { + return pathRemaining; + } + + /** + * @return variables that were bound in the part of the path that was successfully matched. + * Will be an empty map if no variables were bound + */ + public Map getMatchingVariables() { + return matchingVariables; + } +} diff --git a/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java b/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java index a239feb6c51..c55739a280c 100644 --- a/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java @@ -40,25 +40,22 @@ public class PathPatternMatcherTests { @Test public void pathRemainderBasicCases_spr15336() { - // getPathRemaining: Given some pattern and some path, return the bit of the path - // that was left over after the pattern part was matched. - - // Cover all PathElement kinds: - assertEquals("/bar", parse("/foo").getPathRemaining("/foo/bar")); - assertEquals("/", parse("/foo").getPathRemaining("/foo/")); - assertEquals("/bar",parse("/foo*").getPathRemaining("/foo/bar")); - assertEquals("/bar", parse("/*").getPathRemaining("/foo/bar")); - assertEquals("/bar", parse("/{foo}").getPathRemaining("/foo/bar")); + // Cover all PathElement kinds + assertEquals("/bar", parse("/foo").getPathRemaining("/foo/bar").getPathRemaining()); + assertEquals("/", parse("/foo").getPathRemaining("/foo/").getPathRemaining()); + assertEquals("/bar",parse("/foo*").getPathRemaining("/foo/bar").getPathRemaining()); + assertEquals("/bar", parse("/*").getPathRemaining("/foo/bar").getPathRemaining()); + assertEquals("/bar", parse("/{foo}").getPathRemaining("/foo/bar").getPathRemaining()); assertNull(parse("/foo").getPathRemaining("/bar/baz")); - assertEquals("",parse("/**").getPathRemaining("/foo/bar")); - assertEquals("",parse("/{*bar}").getPathRemaining("/foo/bar")); - assertEquals("/bar",parse("/a?b/d?e").getPathRemaining("/aab/dde/bar")); - assertEquals("/bar",parse("/{abc}abc").getPathRemaining("/xyzabc/bar")); - assertEquals("/bar",parse("/*y*").getPathRemaining("/xyzxyz/bar")); - assertEquals("",parse("/").getPathRemaining("/")); - assertEquals("a",parse("/").getPathRemaining("/a")); - assertEquals("a/",parse("/").getPathRemaining("/a/")); - assertEquals("/bar",parse("/a{abc}").getPathRemaining("/a/bar")); + assertEquals("",parse("/**").getPathRemaining("/foo/bar").getPathRemaining()); + assertEquals("",parse("/{*bar}").getPathRemaining("/foo/bar").getPathRemaining()); + assertEquals("/bar",parse("/a?b/d?e").getPathRemaining("/aab/dde/bar").getPathRemaining()); + assertEquals("/bar",parse("/{abc}abc").getPathRemaining("/xyzabc/bar").getPathRemaining()); + assertEquals("/bar",parse("/*y*").getPathRemaining("/xyzxyz/bar").getPathRemaining()); + assertEquals("",parse("/").getPathRemaining("/").getPathRemaining()); + assertEquals("a",parse("/").getPathRemaining("/a").getPathRemaining()); + assertEquals("a/",parse("/").getPathRemaining("/a/").getPathRemaining()); + assertEquals("/bar",parse("/a{abc}").getPathRemaining("/a/bar").getPathRemaining()); } @Test @@ -72,19 +69,27 @@ public class PathPatternMatcherTests { // With a /** on the end have to check if there is any more data post // 'the match' it starts with a separator assertNull(parse("/resource/**").getPathRemaining("/resourceX")); - assertEquals("",parse("/resource/**").getPathRemaining("/resource")); + assertEquals("",parse("/resource/**").getPathRemaining("/resource").getPathRemaining()); // Similar to above for the capture-the-rest variant assertNull(parse("/resource/{*foo}").getPathRemaining("/resourceX")); - assertEquals("",parse("/resource/{*foo}").getPathRemaining("/resource")); + assertEquals("",parse("/resource/{*foo}").getPathRemaining("/resource").getPathRemaining()); + + PathRemainingMatchInfo pri = parse("/aaa/{bbb}/c?d/e*f/*/g").getPathRemaining("/aaa/b/ccd/ef/x/g/i"); + assertEquals("/i",pri.getPathRemaining()); + assertEquals("b",pri.getMatchingVariables().get("bbb")); + + pri = parse("/{aaa}_{bbb}/e*f/{x}/g").getPathRemaining("/aa_bb/ef/x/g/i"); + assertEquals("/i",pri.getPathRemaining()); + assertEquals("aa",pri.getMatchingVariables().get("aaa")); + assertEquals("bb",pri.getMatchingVariables().get("bbb")); + assertEquals("x",pri.getMatchingVariables().get("x")); - assertEquals("/i",parse("/aaa/{bbb}/c?d/e*f/*/g").getPathRemaining("/aaa/b/ccd/ef/x/g/i")); - assertNull(parse("/a/b").getPathRemaining("")); assertNull(parse("/a/b").getPathRemaining(null)); - assertEquals("/a/b",parse("").getPathRemaining("/a/b")); - assertEquals("",parse("").getPathRemaining("")); - assertNull(parse("").getPathRemaining(null)); + assertEquals("/a/b",parse("").getPathRemaining("/a/b").getPathRemaining()); + assertEquals("",parse("").getPathRemaining("").getPathRemaining()); + assertNull(parse("").getPathRemaining(null).getPathRemaining()); } @Test @@ -275,6 +280,38 @@ public class PathPatternMatcherTests { checkCapture("/{bla}.*", "/testing.html", "bla", "testing"); } + @Test + public void pathRemainingEnhancements_spr15419() { + // It would be nice to partially match a path and get any bound variables in one step + PathPattern pp = parse("/{this}/{one}/{here}"); + PathRemainingMatchInfo pri = pp.getPathRemaining("/foo/bar/goo/boo"); + assertEquals("/boo",pri.getPathRemaining()); + assertEquals("foo",pri.getMatchingVariables().get("this")); + assertEquals("bar",pri.getMatchingVariables().get("one")); + assertEquals("goo",pri.getMatchingVariables().get("here")); + + pp = parse("/aaa/{foo}"); + pri = pp.getPathRemaining("/aaa/bbb"); + assertEquals("",pri.getPathRemaining()); + assertEquals("bbb",pri.getMatchingVariables().get("foo")); + + pp = parse("/aaa/bbb"); + pri = pp.getPathRemaining("/aaa/bbb"); + assertEquals("",pri.getPathRemaining()); + assertEquals(0,pri.getMatchingVariables().size()); + + pp = parse("/*/{foo}/b*"); + pri = pp.getPathRemaining("/foo"); + assertNull(pri); + pri = pp.getPathRemaining("/abc/def/bhi"); + assertEquals("",pri.getPathRemaining()); + assertEquals("def",pri.getMatchingVariables().get("foo")); + + pri = pp.getPathRemaining("/abc/def/bhi/jkl"); + assertEquals("/jkl",pri.getPathRemaining()); + assertEquals("def",pri.getMatchingVariables().get("foo")); + } + @Test public void matchStart() { checkStartMatches("test/{a}_{b}/foo", "test/a_b"); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java index f41b67ad05a..5ab9d2cc5d3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -42,6 +42,7 @@ import org.springframework.web.server.WebSession; import org.springframework.web.util.UriUtils; import org.springframework.web.util.patterns.PathPattern; import org.springframework.web.util.patterns.PathPatternParser; +import org.springframework.web.util.patterns.PathRemainingMatchInfo; /** * Implementations of {@link RequestPredicate} that implement various useful @@ -353,7 +354,8 @@ public abstract class RequestPredicates { @Override public Optional nest(ServerRequest request) { - String remainingPath = this.pattern.getPathRemaining(request.path()); + PathRemainingMatchInfo info = this.pattern.getPathRemaining(request.path()); + String remainingPath = (info == null ? null : info.getPathRemaining()); return Optional.ofNullable(remainingPath) .map(path -> !path.startsWith("/") ? "/" + path : path) .map(path -> {