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
This commit is contained in:
Andy Clement 2017-04-14 07:48:51 -07:00
parent 88f8df4dce
commit 316a680577
4 changed files with 136 additions and 36 deletions

View File

@ -151,35 +151,37 @@ public class PathPattern implements Comparable<PathPattern> {
* 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<PathPattern> {
}
/**
* @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<String, String> matchAndExtract(String path) {
MatchingContext matchingContext = new MatchingContext(path, true);

View File

@ -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<String, String> matchingVariables;
PathRemainingMatchInfo(String pathRemaining) {
this.pathRemaining = pathRemaining;
}
PathRemainingMatchInfo(String pathRemaining, Map<String, String> 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<String, String> getMatchingVariables() {
return matchingVariables;
}
}

View File

@ -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");

View File

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