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 68fa32ec6c..4a7b130880 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 0000000000..a823d94244 --- /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 a239feb6c5..c55739a280 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 f41b67ad05..5ab9d2cc5d 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 -> {