diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java index 041f5d247a..0440b8c242 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java @@ -19,10 +19,10 @@ package org.springframework.web.util.pattern; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; import org.springframework.http.server.reactive.PathContainer; import org.springframework.lang.Nullable; @@ -79,19 +79,17 @@ public class ParsingPathMatcher implements PathMatcher { @Override public Map extractUriTemplateVariables(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - Map results = pathPattern.matchAndExtract(PathContainer.parse(path, StandardCharsets.UTF_8)); - // Collapse PathMatchResults to simple value results (path parameters are lost in this translation) - Map boundVariables = null; - if (results.size() == 0) { - boundVariables = Collections.emptyMap(); + PathContainer pathContainer = PathContainer.parse(path, StandardCharsets.UTF_8); + PathMatchResult results = pathPattern.matchAndExtract(pathContainer); + // Collapse PathMatchResults to simple value results + // TODO: (path parameters are lost in this translation) + if (results.getUriVariables().size() == 0) { + return Collections.emptyMap(); } else { - boundVariables = new LinkedHashMap<>(); - for (Map.Entry entries: results.entrySet()) { - boundVariables.put(entries.getKey(), entries.getValue().value()); - } + return results.getUriVariables().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - return boundVariables; } @Override diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java index 179880163e..800a426342 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java @@ -27,6 +27,7 @@ import org.springframework.http.server.reactive.PathContainer.Element; import org.springframework.http.server.reactive.PathContainer.Segment; import org.springframework.http.server.reactive.PathContainer.Separator; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; @@ -155,6 +156,26 @@ public class PathPattern implements Comparable { return this.patternString; } + + // TODO: remove String-variants + + public boolean matches(String path) { + return matches(PathContainer.parse(path, StandardCharsets.UTF_8)); + } + + public PathMatchResult matchAndExtract(String path) { + return matchAndExtract(PathContainer.parse(path, StandardCharsets.UTF_8)); + } + + @Nullable + public PathRemainingMatchInfo getPathRemaining(@Nullable String path) { + return getPathRemaining(path != null ? + PathContainer.parse(path, StandardCharsets.UTF_8) : null); + } + + + + /** * @param pathContainer the candidate path container to attempt to match against this pattern * @return true if the path matches this pattern @@ -199,11 +220,11 @@ public class PathPattern implements Comparable { else { PathRemainingMatchInfo info; if (matchingContext.remainingPathIndex == pathContainer.elements().size()) { - info = new PathRemainingMatchInfo(EMPTY_PATH, matchingContext.getExtractedVariables()); + info = new PathRemainingMatchInfo(EMPTY_PATH, matchingContext.getPathMatchResult()); } else { info = new PathRemainingMatchInfo(PathContainer.subPath(pathContainer, matchingContext.remainingPathIndex), - matchingContext.getExtractedVariables()); + matchingContext.getPathMatchResult()); } return info; } @@ -230,13 +251,13 @@ public class PathPattern implements Comparable { * @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(PathContainer pathContainer) { + public PathMatchResult matchAndExtract(PathContainer pathContainer) { MatchingContext matchingContext = new MatchingContext(pathContainer, true); if (this.head != null && this.head.matches(0, matchingContext)) { - return matchingContext.getExtractedVariables(); + return matchingContext.getPathMatchResult(); } else if (!hasLength(pathContainer)) { - return Collections.emptyMap(); + return PathMatchResult.EMPTY; } else { throw new IllegalStateException("Pattern \"" + this + "\" is not a match for \"" + pathContainer.value() + "\""); @@ -447,45 +468,46 @@ public class PathPattern implements Comparable { return this.patternString; } + /** - * Represents the result of a successful variable match. This holds the key that matched, the - * value that was found for that key and, if any, the parameters attached to that path element. - * For example: "/{var}" against "/foo;a=b" will return a PathMathResult with 'key=var', - * 'value=foo' and parameters 'a=b'. + * Represents the result of a successful path match. This holds the keys that matched, the + * values that were found for each key and, if any, the path parameters (matrix variables) + * attached to that path element. + * For example: "/{var}" against "/foo;a=b" will return a PathMathResult with 'foo=bar' + * for URI variables and 'a=b' as path parameters for 'foo'. */ public static class PathMatchResult { - - private final String key; - - private final String value; - - private final MultiValueMap parameters; - public PathMatchResult(String key, String value, MultiValueMap parameters) { - this.key = key; - this.value = value; - this.parameters = parameters; + private static final PathMatchResult EMPTY = + new PathMatchResult(Collections.emptyMap(), Collections.emptyMap()); + + + private final Map uriVariables; + + private final Map> matrixVariables; + + + public PathMatchResult(Map uriVars, + @Nullable Map> matrixVars) { + + this.uriVariables = Collections.unmodifiableMap(uriVars); + this.matrixVariables = matrixVars != null ? + Collections.unmodifiableMap(matrixVars) : Collections.emptyMap(); } - /** - * @return match result key - */ - public String key() { - return key; + + public Map getUriVariables() { + return this.uriVariables; } - - /** - * @return match result value - */ - public String value() { - return this.value; + + public Map> getMatrixVariables() { + return this.matrixVariables; } - - /** - * @return match result parameters (empty map if no parameters) - */ - public MultiValueMap parameters() { - return this.parameters; + + @Override + public String toString() { + return "PathMatchResult[uriVariables=" + this.uriVariables + ", " + + "matrixVariables=" + this.matrixVariables + "]"; } } @@ -498,15 +520,15 @@ public class PathPattern implements Comparable { private final PathContainer pathRemaining; - private final Map matchingVariables; + private final PathMatchResult pathMatchResult; PathRemainingMatchInfo(@Nullable PathContainer pathRemaining) { - this(pathRemaining, Collections.emptyMap()); + this(pathRemaining, PathMatchResult.EMPTY); } - PathRemainingMatchInfo(@Nullable PathContainer pathRemaining, Map matchingVariables) { + PathRemainingMatchInfo(@Nullable PathContainer pathRemaining, PathMatchResult pathMatchResult) { this.pathRemaining = pathRemaining; - this.matchingVariables = matchingVariables; + this.pathMatchResult = pathMatchResult; } /** @@ -520,8 +542,15 @@ public class PathPattern implements Comparable { * 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 this.matchingVariables; + public Map getUriVariables() { + return this.pathMatchResult.getUriVariables(); + } + + /** + * Return the path parameters for each bound variable. + */ + public Map> getMatrixVariables() { + return this.pathMatchResult.getMatrixVariables(); } } @@ -595,7 +624,10 @@ public class PathPattern implements Comparable { boolean isMatchStartMatching = false; @Nullable - private Map extractedVariables; + private Map extractedUriVariables; + + @Nullable + private Map> extractedMatrixVariables; boolean extractingVariables; @@ -626,18 +658,25 @@ public class PathPattern implements Comparable { } public void set(String key, String value, MultiValueMap parameters) { - if (this.extractedVariables == null) { - extractedVariables = new HashMap<>(); + if (this.extractedUriVariables == null) { + this.extractedUriVariables = new HashMap<>(); + } + this.extractedUriVariables.put(key, value); + + if (!parameters.isEmpty()) { + if (this.extractedMatrixVariables == null) { + this.extractedMatrixVariables = new HashMap<>(); + } + this.extractedMatrixVariables.put(key, CollectionUtils.unmodifiableMultiValueMap(parameters)); } - extractedVariables.put(key, new PathMatchResult(key, value, parameters)); } - public Map getExtractedVariables() { - if (this.extractedVariables == null) { - return Collections.emptyMap(); + public PathMatchResult getPathMatchResult() { + if (this.extractedUriVariables == null) { + return PathMatchResult.EMPTY; } else { - return this.extractedVariables; + return new PathMatchResult(this.extractedUriVariables, this.extractedMatrixVariables); } } diff --git a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java index 0aa44c3c04..26e143ce97 100644 --- a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java @@ -29,17 +29,20 @@ import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.http.server.reactive.PathContainer; import org.springframework.http.server.reactive.PathContainer.Element; import org.springframework.util.AntPathMatcher; -import org.springframework.web.util.pattern.ParsingPathMatcher; -import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern.PathMatchResult; import org.springframework.web.util.pattern.PathPattern.PathRemainingMatchInfo; -import org.springframework.web.util.pattern.PathPatternParser; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Exercise matching of {@link PathPattern} objects. @@ -125,26 +128,26 @@ public class PathPatternMatcherTests { // CaptureVariablePathElement pp = parse("/{var}"); assertMatches(pp,"/resource"); - assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource")).getUriVariables().get("var")); assertMatches(pp,"/resource/"); - assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).getUriVariables().get("var")); assertNoMatch(pp,"/resource//"); pp = parse("/{var}/"); assertNoMatch(pp,"/resource"); assertMatches(pp,"/resource/"); - assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).getUriVariables().get("var")); assertNoMatch(pp,"/resource//"); // CaptureTheRestPathElement pp = parse("/{*var}"); assertMatches(pp,"/resource"); - assertEquals("/resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertEquals("/resource",pp.matchAndExtract(toPathContainer("/resource")).getUriVariables().get("var")); assertMatches(pp,"/resource/"); - assertEquals("/resource/",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertEquals("/resource/",pp.matchAndExtract(toPathContainer("/resource/")).getUriVariables().get("var")); assertMatches(pp,"/resource//"); - assertEquals("/resource//",pp.matchAndExtract(toPathContainer("/resource//")).get("var").value()); + assertEquals("/resource//",pp.matchAndExtract(toPathContainer("/resource//")).getUriVariables().get("var")); assertMatches(pp,"//resource//"); - assertEquals("//resource//",pp.matchAndExtract(toPathContainer("//resource//")).get("var").value()); + assertEquals("//resource//",pp.matchAndExtract(toPathContainer("//resource//")).getUriVariables().get("var")); // WildcardTheRestPathElement pp = parse("/**"); @@ -166,17 +169,17 @@ public class PathPatternMatcherTests { // RegexPathElement pp = parse("/{var1}_{var2}"); assertMatches(pp,"/res1_res2"); - assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var1").value()); - assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var2").value()); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2")).getUriVariables().get("var1")); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2")).getUriVariables().get("var2")); assertMatches(pp,"/res1_res2/"); - assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var1").value()); - assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var2").value()); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).getUriVariables().get("var1")); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).getUriVariables().get("var2")); assertNoMatch(pp,"/res1_res2//"); pp = parse("/{var1}_{var2}/"); assertNoMatch(pp,"/res1_res2"); assertMatches(pp,"/res1_res2/"); - assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var1").value()); - assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var2").value()); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).getUriVariables().get("var1")); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).getUriVariables().get("var2")); assertNoMatch(pp,"/res1_res2//"); pp = parse("/{var1}*"); assertMatches(pp,"/a"); @@ -210,25 +213,25 @@ public class PathPatternMatcherTests { // CaptureVariablePathElement pp = parser.parse("/{var}"); assertMatches(pp,"/resource"); - assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource")).getUriVariables().get("var")); assertNoMatch(pp,"/resource/"); assertNoMatch(pp,"/resource//"); pp = parser.parse("/{var}/"); assertNoMatch(pp,"/resource"); assertMatches(pp,"/resource/"); - assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).getUriVariables().get("var")); assertNoMatch(pp,"/resource//"); // CaptureTheRestPathElement pp = parser.parse("/{*var}"); assertMatches(pp,"/resource"); - assertEquals("/resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertEquals("/resource",pp.matchAndExtract(toPathContainer("/resource")).getUriVariables().get("var")); assertMatches(pp,"/resource/"); - assertEquals("/resource/",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertEquals("/resource/",pp.matchAndExtract(toPathContainer("/resource/")).getUriVariables().get("var")); assertMatches(pp,"/resource//"); - assertEquals("/resource//",pp.matchAndExtract(toPathContainer("/resource//")).get("var").value()); + assertEquals("/resource//",pp.matchAndExtract(toPathContainer("/resource//")).getUriVariables().get("var")); assertMatches(pp,"//resource//"); - assertEquals("//resource//",pp.matchAndExtract(toPathContainer("//resource//")).get("var").value()); + assertEquals("//resource//",pp.matchAndExtract(toPathContainer("//resource//")).getUriVariables().get("var")); // WildcardTheRestPathElement pp = parser.parse("/**"); @@ -250,15 +253,15 @@ public class PathPatternMatcherTests { // RegexPathElement pp = parser.parse("/{var1}_{var2}"); assertMatches(pp,"/res1_res2"); - assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var1").value()); - assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var2").value()); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2")).getUriVariables().get("var1")); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2")).getUriVariables().get("var2")); assertNoMatch(pp,"/res1_res2/"); assertNoMatch(pp,"/res1_res2//"); pp = parser.parse("/{var1}_{var2}/"); assertNoMatch(pp,"/res1_res2"); assertMatches(pp,"/res1_res2/"); - assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var1").value()); - assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var2").value()); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).getUriVariables().get("var1")); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).getUriVariables().get("var2")); assertNoMatch(pp,"/res1_res2//"); pp = parser.parse("/{var1}*"); assertMatches(pp,"/a"); @@ -337,23 +340,23 @@ public class PathPatternMatcherTests { PathPattern.PathRemainingMatchInfo pri = parse("/aaa/{bbb}/c?d/e*f/*/g").getPathRemaining(toPathContainer("/aaa/b/ccd/ef/x/g/i")); assertEquals("/i",pri.getPathRemaining()); - assertEquals("b",pri.getMatchingVariables().get("bbb").value()); + assertEquals("b",pri.getUriVariables().get("bbb")); pri = parse("/aaa/{bbb}/c?d/e*f/*/g/").getPathRemaining(toPathContainer("/aaa/b/ccd/ef/x/g/i")); assertEquals("i",pri.getPathRemaining()); - assertEquals("b",pri.getMatchingVariables().get("bbb").value()); + assertEquals("b",pri.getUriVariables().get("bbb")); pri = parse("/{aaa}_{bbb}/e*f/{x}/g").getPathRemaining(toPathContainer("/aa_bb/ef/x/g/i")); assertEquals("/i",pri.getPathRemaining()); - assertEquals("aa",pri.getMatchingVariables().get("aaa").value()); - assertEquals("bb",pri.getMatchingVariables().get("bbb").value()); - assertEquals("x",pri.getMatchingVariables().get("x").value()); + assertEquals("aa",pri.getUriVariables().get("aaa")); + assertEquals("bb",pri.getUriVariables().get("bbb")); + assertEquals("x",pri.getUriVariables().get("x")); assertNull(parse("/a/b").getPathRemaining(toPathContainer(""))); - assertNull(parse("/a/b").getPathRemaining(null)); + assertNull(parse("/a/b").getPathRemaining((String) null)); assertEquals("/a/b",parse("").getPathRemaining(toPathContainer("/a/b")).getPathRemaining()); assertEquals("",parse("").getPathRemaining(toPathContainer("")).getPathRemaining()); - assertNull(parse("").getPathRemaining(null).getPathRemaining()); + assertNull(parse("").getPathRemaining((String) null).getPathRemaining()); } @Test @@ -550,30 +553,30 @@ public class PathPatternMatcherTests { pp = parse("/{this}/{one}/{here}"); pri = getPathRemaining(pp, "/foo/bar/goo/boo"); assertEquals("/boo",pri.getPathRemaining()); - assertEquals("foo",pri.getMatchingVariables().get("this").value()); - assertEquals("bar",pri.getMatchingVariables().get("one").value()); - assertEquals("goo",pri.getMatchingVariables().get("here").value()); + assertEquals("foo",pri.getUriVariables().get("this")); + assertEquals("bar",pri.getUriVariables().get("one")); + assertEquals("goo",pri.getUriVariables().get("here")); pp = parse("/aaa/{foo}"); pri = getPathRemaining(pp, "/aaa/bbb"); assertEquals("",pri.getPathRemaining()); - assertEquals("bbb",pri.getMatchingVariables().get("foo").value()); + assertEquals("bbb",pri.getUriVariables().get("foo")); pp = parse("/aaa/bbb"); pri = getPathRemaining(pp, "/aaa/bbb"); assertEquals("",pri.getPathRemaining()); - assertEquals(0,pri.getMatchingVariables().size()); + assertEquals(0,pri.getUriVariables().size()); pp = parse("/*/{foo}/b*"); pri = getPathRemaining(pp, "/foo"); assertNull(pri); pri = getPathRemaining(pp, "/abc/def/bhi"); assertEquals("",pri.getPathRemaining()); - assertEquals("def",pri.getMatchingVariables().get("foo").value()); + assertEquals("def",pri.getUriVariables().get("foo")); pri = getPathRemaining(pp, "/abc/def/bhi/jkl"); assertEquals("/jkl",pri.getPathRemaining()); - assertEquals("def",pri.getMatchingVariables().get("foo").value()); + assertEquals("def",pri.getUriVariables().get("foo")); } @Test @@ -953,7 +956,7 @@ public class PathPatternMatcherTests { catch (IllegalStateException e) { assertEquals("Pattern \"\" is not a match for \"/abc\"", e.getMessage()); } - assertEquals(0, checkCapture("", "").size()); + assertEquals(0, checkCapture("", "").getUriVariables().size()); checkCapture("{id}", "99", "id", "99"); checkCapture("/customer/{customerId}", "/customer/78", "customerId", "78"); checkCapture("/customer/{customerId}/banana", "/customer/42/banana", "customerId", @@ -962,8 +965,8 @@ public class PathPatternMatcherTests { checkCapture("/foo/{bar}/boo/{baz}", "/foo/plum/boo/apple", "bar", "plum", "baz", "apple"); checkCapture("/{bla}.*", "/testing.html", "bla", "testing"); - Map extracted = checkCapture("/abc", "/abc"); - assertEquals(0, extracted.size()); + PathMatchResult extracted = checkCapture("/abc", "/abc"); + assertEquals(0, extracted.getUriVariables().size()); checkCapture("/{bla}/foo","/a/foo"); } @@ -973,14 +976,14 @@ public class PathPatternMatcherTests { PathPattern p = null; p = pp.parse("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar"); - Map result = matchAndExtract(p, "com.example-1.0.0.jar"); - assertEquals("com.example", result.get("symbolicName").value()); - assertEquals("1.0.0", result.get("version").value()); + PathMatchResult result = matchAndExtract(p, "com.example-1.0.0.jar"); + assertEquals("com.example", result.getUriVariables().get("symbolicName")); + assertEquals("1.0.0", result.getUriVariables().get("version")); p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar"); result = matchAndExtract(p, "com.example-sources-1.0.0.jar"); - assertEquals("com.example", result.get("symbolicName").value()); - assertEquals("1.0.0", result.get("version").value()); + assertEquals("com.example", result.getUriVariables().get("symbolicName")); + assertEquals("1.0.0", result.getUriVariables().get("version")); } @Test @@ -988,22 +991,22 @@ public class PathPatternMatcherTests { PathPatternParser pp = new PathPatternParser(); PathPattern p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar"); - Map result = p.matchAndExtract(toPathContainer("com.example-sources-1.0.0.jar")); - assertEquals("com.example", result.get("symbolicName").value()); - assertEquals("1.0.0", result.get("version").value()); + PathMatchResult result = p.matchAndExtract(toPathContainer("com.example-sources-1.0.0.jar")); + assertEquals("com.example", result.getUriVariables().get("symbolicName")); + assertEquals("1.0.0", result.getUriVariables().get("version")); p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar"); result = matchAndExtract(p,"com.example-sources-1.0.0-20100220.jar"); - assertEquals("com.example", result.get("symbolicName").value()); - assertEquals("1.0.0", result.get("version").value()); - assertEquals("2010", result.get("year").value()); - assertEquals("02", result.get("month").value()); - assertEquals("20", result.get("day").value()); + assertEquals("com.example", result.getUriVariables().get("symbolicName")); + assertEquals("1.0.0", result.getUriVariables().get("version")); + assertEquals("2010", result.getUriVariables().get("year")); + assertEquals("02", result.getUriVariables().get("month")); + assertEquals("20", result.getUriVariables().get("day")); p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar"); result = matchAndExtract(p, "com.example-sources-1.0.0.{12}.jar"); - assertEquals("com.example", result.get("symbolicName").value()); - assertEquals("1.0.0.{12}", result.get("version").value()); + assertEquals("com.example", result.getUriVariables().get("symbolicName")); + assertEquals("1.0.0.{12}", result.getUriVariables().get("version")); } @Test @@ -1137,12 +1140,12 @@ public class PathPatternMatcherTests { PathPatternParser parser = new PathPatternParser(); PathPattern p1 = parser.parse("/{foo}"); PathPattern p2 = parser.parse("/{foo}.*"); - Map r1 = matchAndExtract(p1, "/file.txt"); - Map r2 = matchAndExtract(p2, "/file.txt"); + PathMatchResult r1 = matchAndExtract(p1, "/file.txt"); + PathMatchResult r2 = matchAndExtract(p2, "/file.txt"); // works fine - assertEquals("file.txt", r1.get("foo").value()); - assertEquals("file", r2.get("foo").value()); + assertEquals("file.txt", r1.getUriVariables().get("foo")); + assertEquals("file", r2.getUriVariables().get("foo")); // This produces 2 (see comments in https://jira.spring.io/browse/SPR-14544 ) // Comparator patternComparator = new AntPathMatcher().getPatternComparator(""); @@ -1269,38 +1272,38 @@ public class PathPatternMatcherTests { @Test public void parameters() { // CaptureVariablePathElement - Map result = matchAndExtract("/abc/{var}","/abc/one;two=three;four=five"); - assertEquals("one",result.get("var").value()); - assertEquals("[three]",result.get("var").parameters().get("two").toString()); - assertEquals("[five]",result.get("var").parameters().get("four").toString()); + PathMatchResult result = matchAndExtract("/abc/{var}","/abc/one;two=three;four=five"); + assertEquals("one",result.getUriVariables().get("var")); + assertEquals("three",result.getMatrixVariables().get("var").getFirst("two")); + assertEquals("five",result.getMatrixVariables().get("var").getFirst("four")); // RegexPathElement result = matchAndExtract("/abc/{var1}_{var2}","/abc/123_456;a=b;c=d"); - assertEquals("123",result.get("var1").value()); - assertEquals("456",result.get("var2").value()); + assertEquals("123",result.getUriVariables().get("var1")); + assertEquals("456",result.getUriVariables().get("var2")); // vars associated with second variable - assertNull(result.get("var1").parameters().get("a")); - assertNull(result.get("var1").parameters().get("c")); - assertEquals("[b]",result.get("var2").parameters().get("a").toString()); - assertEquals("[d]",result.get("var2").parameters().get("c").toString()); + assertNull(result.getMatrixVariables().get("var1")); + assertNull(result.getMatrixVariables().get("var1")); + assertEquals("b",result.getMatrixVariables().get("var2").getFirst("a")); + assertEquals("d",result.getMatrixVariables().get("var2").getFirst("c")); // CaptureTheRestPathElement result = matchAndExtract("/{*var}","/abc/123_456;a=b;c=d"); - assertEquals("/abc/123_456",result.get("var").value()); - assertEquals("[b]",result.get("var").parameters().get("a").toString()); - assertEquals("[d]",result.get("var").parameters().get("c").toString()); + assertEquals("/abc/123_456",result.getUriVariables().get("var")); + assertEquals("b",result.getMatrixVariables().get("var").getFirst("a")); + assertEquals("d",result.getMatrixVariables().get("var").getFirst("c")); result = matchAndExtract("/{*var}","/abc/123_456;a=b;c=d/789;a=e;f=g"); - assertEquals("/abc/123_456/789",result.get("var").value()); - assertEquals("[b, e]",result.get("var").parameters().get("a").toString()); - assertEquals("[d]",result.get("var").parameters().get("c").toString()); - assertEquals("[g]",result.get("var").parameters().get("f").toString()); + assertEquals("/abc/123_456/789",result.getUriVariables().get("var")); + assertEquals("[b, e]",result.getMatrixVariables().get("var").get("a").toString()); + assertEquals("d",result.getMatrixVariables().get("var").getFirst("c")); + assertEquals("g",result.getMatrixVariables().get("var").getFirst("f")); result = matchAndExtract("/abc/{var}","/abc/one"); - assertEquals("one",result.get("var").value()); - assertEquals(0,result.get("var").parameters().size()); + assertEquals("one",result.getUriVariables().get("var")); + assertNull(result.getMatrixVariables().get("var")); } // --- - private Map matchAndExtract(String pattern, String path) { + private PathMatchResult matchAndExtract(String pattern, String path) { return parse(pattern).matchAndExtract(PathPatternMatcherTests.toPathContainer(path)); } @@ -1346,29 +1349,26 @@ public class PathPatternMatcherTests { assertFalse(pattern.matches(PathContainer)); } - private Map checkCapture(String uriTemplate, String path, String... keyValues) { + private PathMatchResult checkCapture(String uriTemplate, String path, String... keyValues) { PathPatternParser parser = new PathPatternParser(); PathPattern pattern = parser.parse(uriTemplate); - Map matchResults = pattern.matchAndExtract(toPathContainer(path)); + PathMatchResult matchResult = pattern.matchAndExtract(toPathContainer(path)); Map expectedKeyValues = new HashMap<>(); - if (keyValues != null) { - for (int i = 0; i < keyValues.length; i += 2) { - expectedKeyValues.put(keyValues[i], keyValues[i + 1]); - } + for (int i = 0; i < keyValues.length; i += 2) { + expectedKeyValues.put(keyValues[i], keyValues[i + 1]); } - Map capturedVariables = matchResults; for (Map.Entry me : expectedKeyValues.entrySet()) { - String value = capturedVariables.get(me.getKey()).value(); + String value = matchResult.getUriVariables().get(me.getKey()); if (value == null) { fail("Did not find key '" + me.getKey() + "' in captured variables: " - + capturedVariables); + + matchResult.getUriVariables()); } if (!value.equals(me.getValue())) { fail("Expected value '" + me.getValue() + "' for key '" + me.getKey() + "' but was '" + value + "'"); } } - return capturedVariables; + return matchResult; } private void checkExtractPathWithinPattern(String pattern, String path, String expected) { @@ -1399,7 +1399,7 @@ public class PathPatternMatcherTests { return pattern.getPathRemaining(toPathContainer(path)); } - private Map matchAndExtract(PathPattern p, String path) { + private PathMatchResult matchAndExtract(PathPattern p, String path) { return p.matchAndExtract(toPathContainer(path)); } diff --git a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java index 8ae5441b49..f7d9e9da04 100644 --- a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java @@ -19,14 +19,18 @@ package org.springframework.web.util.pattern; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import org.junit.Test; + import org.springframework.http.server.reactive.PathContainer; import org.springframework.web.util.pattern.PathPattern.PathMatchResult; import org.springframework.web.util.pattern.PatternParseException.PatternMessage; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Exercise the {@link PathPatternParser}. @@ -137,23 +141,23 @@ public class PathPatternParserTests { pathPattern = checkStructure("/{var:[^\\/]*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - Map result = matchAndExtract(pathPattern,"/foo"); - assertEquals("foo", result.get("var").value()); + PathMatchResult result = matchAndExtract(pathPattern,"/foo"); + assertEquals("foo", result.getUriVariables().get("var")); pathPattern = checkStructure("/{var:\\[*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); result = matchAndExtract(pathPattern,"/[[["); - assertEquals("[[[", result.get("var").value()); + assertEquals("[[[", result.getUriVariables().get("var")); pathPattern = checkStructure("/{var:[\\{]*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); result = matchAndExtract(pathPattern,"/{{{"); - assertEquals("{{{", result.get("var").value()); + assertEquals("{{{", result.getUriVariables().get("var")); pathPattern = checkStructure("/{var:[\\}]*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); result = matchAndExtract(pathPattern,"/}}}"); - assertEquals("}}}", result.get("var").value()); + assertEquals("}}}", result.getUriVariables().get("var")); pathPattern = checkStructure("*"); assertEquals(WildcardPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName()); @@ -467,7 +471,7 @@ public class PathPatternParserTests { assertFalse(pp.matches(PathPatternMatcherTests.toPathContainer(path))); } - private Map matchAndExtract(PathPattern pp, String path) { + private PathMatchResult matchAndExtract(PathPattern pp, String path) { return pp.matchAndExtract(PathPatternMatcherTests.toPathContainer(path)); } 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 3b2d4e1ba1..a4562087c8 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 @@ -343,7 +343,7 @@ public abstract class RequestPredicates { boolean match = this.pattern.matches(path); traceMatch("Pattern", this.pattern.getPatternString(), path, match); if (match) { - mergeTemplateVariables(request, this.pattern.matchAndExtract(request.path())); + mergeTemplateVariables(request, this.pattern.matchAndExtract(request.path()).getUriVariables()); return true; } else { @@ -355,7 +355,7 @@ public abstract class RequestPredicates { public Optional nest(ServerRequest request) { return Optional.ofNullable(this.pattern.getPathRemaining(request.path())) .map(info -> { - mergeTemplateVariables(request, info.getMatchingVariables()); + mergeTemplateVariables(request, info.getUriVariables()); String path = info.getPathRemaining(); if (!path.startsWith("/")) { path = "/" + path; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java index caa4376565..aa0a87208d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java @@ -119,7 +119,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition *
  • If there are patterns in both instances, combine the patterns in "this" with - * the patterns in "other" using {@link PathPattern#combine(String)}. + * the patterns in "other" using {@link PathPattern#combine(PathPattern)}. *
  • If only one instance has patterns, use them. *
  • If neither instance has patterns, use an empty String (i.e. ""). * @@ -130,8 +130,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition matches = getMatchingPatterns(lookupPath); - return matches.isEmpty() ? null : new PatternsRequestCondition(new ArrayList<>(matches), this.parser); + return matches.isEmpty() ? null : new PatternsRequestCondition( + new ArrayList(matches), this.parser); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 18b8a98ea2..32b962d77f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -17,18 +17,14 @@ package org.springframework.web.reactive.result.method; import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -import java.util.StringTokenizer; import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; @@ -36,9 +32,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.result.condition.NameValueExpression; @@ -113,31 +107,25 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe PathPattern bestPattern; Map uriVariables; + Map> matrixVariables; Set patterns = info.getPatternsCondition().getPatterns(); if (patterns.isEmpty()) { bestPattern = getPathPatternParser().parse(lookupPath); uriVariables = Collections.emptyMap(); + matrixVariables = Collections.emptyMap(); } else { bestPattern = patterns.iterator().next(); - uriVariables = bestPattern.matchAndExtract(lookupPath); - } - - // Let URI vars be stripped of semicolon content.. - Map> matrixVars = extractMatrixVariables(exchange, uriVariables); - exchange.getAttributes().put(MATRIX_VARIABLES_ATTRIBUTE, matrixVars); - - // Now decode URI variables - if (!uriVariables.isEmpty()) { - uriVariables = uriVariables.entrySet().stream().collect(Collectors.toMap( - Entry::getKey, e -> StringUtils.uriDecode(e.getValue(), StandardCharsets.UTF_8) - )); + PathPattern.PathMatchResult result = bestPattern.matchAndExtract(lookupPath); + uriVariables = result.getUriVariables(); + matrixVariables = result.getMatrixVariables(); } exchange.getAttributes().put(BEST_MATCHING_HANDLER_ATTRIBUTE, handlerMethod); exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables); + exchange.getAttributes().put(MATRIX_VARIABLES_ATTRIBUTE, matrixVariables); if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { Set mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); @@ -145,62 +133,6 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } } - private Map> extractMatrixVariables( - ServerWebExchange exchange, Map uriVariables) { - - Map> result = new LinkedHashMap<>(); - for (Entry uriVar : uriVariables.entrySet()) { - String uriVarValue = uriVar.getValue(); - - int equalsIndex = uriVarValue.indexOf('='); - if (equalsIndex == -1) { - continue; - } - - String semicolonContent; - int semicolonIndex = uriVarValue.indexOf(';'); - if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) { - semicolonContent = uriVarValue; - } - else { - semicolonContent = uriVarValue.substring(semicolonIndex + 1); - uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex)); - } - result.put(uriVar.getKey(), parseMatrixVariables(exchange, semicolonContent)); - } - return result; - } - - private static MultiValueMap parseMatrixVariables(ServerWebExchange exchange, - String semicolonContent) { - - MultiValueMap vars = new LinkedMultiValueMap<>(); - if (!StringUtils.hasText(semicolonContent)) { - return vars; - } - StringTokenizer pairs = new StringTokenizer(semicolonContent, ";"); - while (pairs.hasMoreTokens()) { - String pair = pairs.nextToken(); - int index = pair.indexOf('='); - if (index != -1) { - String name = pair.substring(0, index); - String rawValue = pair.substring(index + 1); - for (String value : StringUtils.commaDelimitedListToStringArray(rawValue)) { - vars.add(name, value); - } - } - else { - vars.add(pair, ""); - } - } - MultiValueMap decoded = new LinkedMultiValueMap<>(vars.size()); - vars.forEach((key, values) -> values.forEach(value -> { - String decodedValue = StringUtils.uriDecode(value, StandardCharsets.UTF_8); - decoded.add(key, decodedValue); - })); - return decoded; - } - /** * Iterate all RequestMappingInfos once again, look if any match by URL at * least and raise exceptions accordingly. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java index 3fe775d9ef..0b568d2d1f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestTests.java @@ -50,7 +50,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.server.UnsupportedMediaTypeStatusException; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import static org.springframework.web.reactive.function.BodyExtractors.toMono; /** diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index bc0a202e16..a8c7ea8280 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -294,41 +294,19 @@ public class RequestMappingInfoHandlerMappingTests { assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); assertEquals("2012", matrixVariables.getFirst("year")); assertEquals("cars", uriVariables.get("cars")); - - exchange = get("/cars;colors=red,blue,green;year=2012").toExchange(); - handleMatch(exchange, "/{cars:[^;]+}{params}"); - - matrixVariables = getMatrixVariables(exchange, "params"); - uriVariables = getUriTemplateVariables(exchange); - - assertNotNull(matrixVariables); - assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); - assertEquals("2012", matrixVariables.getFirst("year")); - assertEquals("cars", uriVariables.get("cars")); - assertEquals(";colors=red,blue,green;year=2012", uriVariables.get("params")); - - exchange = get("/cars").toExchange(); - handleMatch(exchange, "/{cars:[^;]+}{params}"); - - matrixVariables = getMatrixVariables(exchange, "params"); - uriVariables = getUriTemplateVariables(exchange); - - assertNull(matrixVariables); - assertEquals("cars", uriVariables.get("cars")); - assertEquals("", uriVariables.get("params")); } @Test public void handleMatchMatrixVariablesDecoding() throws Exception { ServerWebExchange exchange = method(HttpMethod.GET, URI.create("/path;mvar=a%2fb")).toExchange(); - handleMatch(exchange, "/path{filter}"); + handleMatch(exchange, "/{filter}"); MultiValueMap matrixVariables = getMatrixVariables(exchange, "filter"); Map uriVariables = getUriTemplateVariables(exchange); assertNotNull(matrixVariables); assertEquals(Collections.singletonList("a/b"), matrixVariables.get("mvar")); - assertEquals(";mvar=a/b", uriVariables.get("filter")); + assertEquals("path", uriVariables.get("filter")); }