Introduce PathPatternParser for optimized path matching

This commit introduces a PathPatternParser which parses request pattern
strings into PathPattern objects which can then be used to fast
match incoming string paths. The parser and matching supports the syntax
as described in SPR-14544. The code is optimized around the common usages
of request patterns and is designed to create very little transient
garbage when matching.

Issue: SPR-14544
This commit is contained in:
Andy Clement
2016-10-11 15:14:08 -07:00
committed by Brian Clozel
parent 6f029392c7
commit f58ffad939
44 changed files with 3880 additions and 73 deletions

View File

@@ -0,0 +1,905 @@
/*
* Copyright 2016 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.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Exercise matching of {@link PathPattern} objects.
*
* @author Andy Clement
*/
public class PathPatternMatcherTests {
@Test
public void basicMatching() {
checkMatches(null, null);
checkMatches("", "");
checkMatches("", null);
checkNoMatch("/abc", null);
checkMatches(null, "");
checkNoMatch(null, "/abc");
checkMatches("/", "/");
checkNoMatch("/", "/a");
checkMatches("f", "f");
checkMatches("/foo", "/foo");
checkMatches("/foo/", "/foo/");
checkMatches("/foo/bar", "/foo/bar");
checkMatches("foo/bar", "foo/bar");
checkMatches("/foo/bar/", "/foo/bar/");
checkMatches("foo/bar/", "foo/bar/");
checkMatches("/foo/bar/woo", "/foo/bar/woo");
checkNoMatch("foo", "foobar");
checkMatches("/foo/bar", "/foo/bar");
checkNoMatch("/foo/bar", "/foo/baz");
// TODO Need more tests for escaped separators in path patterns and paths?
checkMatches("/foo\\/bar","/foo\\/bar"); // chain string is Separator(/) Literal(foo\) Separator(/) Literal(bar)
}
@Test
public void questionMarks() {
checkNoMatch("a", "ab");
checkMatches("/f?o/bar", "/foo/bar");
checkNoMatch("/foo/b2r", "/foo/bar");
checkNoMatch("?", "te");
checkMatches("?", "a");
checkMatches("???", "abc");
checkNoMatch("tes?", "te");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
checkMatches(".?.a", ".a.a");
checkNoMatch(".?.a", ".aba");
}
@Test
public void captureTheRest() {
checkCapture("/customer/{*something}", "/customer/99", "something", "/99");
checkCapture("/customer/{*something}", "/customer/aa/bb/cc", "something",
"/aa/bb/cc");
checkCapture("/customer/{*something}", "/customer/", "something", "/");
checkCapture("/customer/////{*something}", "/customer/", "something", "/");
checkCapture("/customer/{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer///{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer/{*something}", "/customer", "something", "");
checkCapture("/{*something}", "", "something", "");
}
@Test
public void multipleSelectorsInPattern() {
checkMatches("///abc","/abc");
checkMatches("//","/");
checkMatches("abc","abc");
checkMatches("///abc//d/e","/abc/d/e");
checkMatches("///abc//{def}//////xyz","/abc/foo/xyz");
}
@Test
public void multipleSelectorsInPath() {
checkMatches("/abc","////abc");
checkMatches("/","//");
checkMatches("/abc//def///ghi","/abc/def/ghi");
}
@Test
public void multipleSelectorsInPatternAndPath() {
checkMatches("///one///two///three","//one/////two///////three");
checkMatches("//one//two//three","/one/////two/three");
checkCapture("///{foo}///bar","/one/bar","foo","one");
}
@Test
public void wildcards() {
checkMatches("/*/bar", "/foo/bar");
checkNoMatch("/*/bar", "/foo/baz");
checkMatches("/f*/bar", "/foo/bar");
checkMatches("/*/bar", "/foo/bar");
checkMatches("/a*b*c*d/bar", "/abcd/bar");
checkMatches("*a*", "testa");
checkMatches("a/*","a/");
checkMatches("a/*","a/a");
checkNoMatch("a/*","a/a/");
}
@Test
public void trailingSeparators() {
checkNoMatch("aaa/", "aaa");
}
@Test
public void constrainedMatches() {
checkCapture("{foo:[0-9]*}", "123", "foo", "123");
checkNoMatch("{foo:[0-9]*}", "abc");
checkNoMatch("/{foo:[0-9]*}", "abc");
checkCapture("/*/{foo:....}/**","/foo/barg/foo","foo","barg");
checkCapture("/*/{foo:....}/**","/foo/barg/abc/def/ghi","foo","barg");
checkNoMatch("{foo:....}", "99");
checkMatches("{foo:..}", "99");
checkCapture("/{abc:\\{\\}}","/{}","abc","{}");
checkCapture("/{abc:\\[\\]}","/[]","abc","[]");
checkCapture("/{abc:\\\\\\\\}","/\\\\"); // this is fun...
}
@Test
public void antPathMatcherTests() {
// test exact matching
checkMatches("test", "test");
checkMatches("/test", "/test");
checkMatches("http://example.org", "http://example.org");
checkNoMatch("/test.jpg", "test.jpg");
checkNoMatch("test", "/test");
checkNoMatch("/test", "test");
// test matching with ?'s
checkMatches("t?st", "test");
checkMatches("??st", "test");
checkMatches("tes?", "test");
checkMatches("te??", "test");
checkMatches("?es?", "test");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
// test matching with *'s
checkMatches("*", "test");
checkMatches("test*", "test");
checkMatches("test*", "testTest");
checkMatches("test/*", "test/Test");
checkMatches("test/*", "test/t");
checkMatches("test/*", "test/");
checkMatches("*test*", "AnothertestTest");
checkMatches("*test", "Anothertest");
checkMatches("*.*", "test.");
checkMatches("*.*", "test.test");
checkMatches("*.*", "test.test.test");
checkMatches("test*aaa", "testblaaaa");
checkNoMatch("test*", "tst");
checkNoMatch("test*", "tsttest");
checkNoMatch("test*", "test/");
checkNoMatch("test*", "test/t");
checkNoMatch("test/*", "test");
checkNoMatch("*test*", "tsttst");
checkNoMatch("*test", "tsttst");
checkNoMatch("*.*", "tsttst");
checkNoMatch("test*aaa", "test");
checkNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and /'s
checkMatches("/?", "/a");
checkMatches("/?/a", "/a/a");
checkMatches("/a/?", "/a/b");
checkMatches("/??/a", "/aa/a");
checkMatches("/a/??", "/a/bb");
checkMatches("/?", "/a");
checkMatches("/**", "");
checkMatches("/books/**", "/books");
checkMatches("/books////**", "/books");
checkMatches("/books////**", "/books////");
checkMatches("/**", "/testing/testing");
checkMatches("/*/**", "/testing/testing");
checkMatches("/bla*bla/test", "/blaXXXbla/test");
checkMatches("/*bla/test", "/XXXbla/test");
checkNoMatch("/bla*bla/test", "/blaXXXbl/test");
checkNoMatch("/*bla/test", "XXXblab/test");
checkNoMatch("/*bla/test", "XXXbl/test");
checkNoMatch("/????", "/bala/bla");
checkMatches("/foo/bar/**", "/foo/bar/");
checkMatches("/{bla}.html", "/testing.html");
checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
}
@Test
public void matchStart() {
checkStartMatches("test/{a}_{b}/foo", "test/a_b");
checkStartMatches("test/?/abc", "test/a");
checkStartMatches("test/{*foobar}", "test/");
checkStartMatches("test/*/bar", "test/a");
checkStartMatches("test/{foo}/bar", "test/abc");
checkStartMatches("test//foo", "test//");
checkStartMatches("test/foo", "test/");
checkStartMatches("test/*", "test/");
checkStartMatches("test", "test");
checkStartNoMatch("test", "tes");
checkStartMatches("test/", "test");
// test exact matching
checkStartMatches("test", "test");
checkStartMatches("/test", "/test");
checkStartNoMatch("/test.jpg", "test.jpg");
checkStartNoMatch("test", "/test");
checkStartNoMatch("/test", "test");
// test matching with ?'s
checkStartMatches("t?st", "test");
checkStartMatches("??st", "test");
checkStartMatches("tes?", "test");
checkStartMatches("te??", "test");
checkStartMatches("?es?", "test");
checkStartNoMatch("tes?", "tes");
checkStartNoMatch("tes?", "testt");
checkStartNoMatch("tes?", "tsst");
// test matching with *'s
checkStartMatches("*", "test");
checkStartMatches("test*", "test");
checkStartMatches("test*", "testTest");
checkStartMatches("test/*", "test/Test");
checkStartMatches("test/*", "test/t");
checkStartMatches("test/*", "test/");
checkStartMatches("*test*", "AnothertestTest");
checkStartMatches("*test", "Anothertest");
checkStartMatches("*.*", "test.");
checkStartMatches("*.*", "test.test");
checkStartMatches("*.*", "test.test.test");
checkStartMatches("test*aaa", "testblaaaa");
checkStartNoMatch("test*", "tst");
checkStartNoMatch("test*", "test/");
checkStartNoMatch("test*", "tsttest");
checkStartNoMatch("test*", "test/t");
checkStartMatches("test/*", "test");
checkStartMatches("test/t*.txt", "test");
checkStartNoMatch("*test*", "tsttst");
checkStartNoMatch("*test", "tsttst");
checkStartNoMatch("*.*", "tsttst");
checkStartNoMatch("test*aaa", "test");
checkStartNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and /'s
checkStartMatches("/?", "/a");
checkStartMatches("/?/a", "/a/a");
checkStartMatches("/a/?", "/a/b");
checkStartMatches("/??/a", "/aa/a");
checkStartMatches("/a/??", "/a/bb");
checkStartMatches("/?", "/a");
checkStartMatches("/**", "/testing/testing");
checkStartMatches("/*/**", "/testing/testing");
checkStartMatches("test*/**", "test/");
checkStartMatches("test*/**", "test/t");
checkStartMatches("/bla*bla/test", "/blaXXXbla/test");
checkStartMatches("/*bla/test", "/XXXbla/test");
checkStartNoMatch("/bla*bla/test", "/blaXXXbl/test");
checkStartNoMatch("/*bla/test", "XXXblab/test");
checkStartNoMatch("/*bla/test", "XXXbl/test");
checkStartNoMatch("/????", "/bala/bla");
checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing/");
checkStartMatches("/*bla*/*/bla/*",
"/XXXblaXXXX/testing/bla/testing");
checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing");
checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing.jpg");
checkStartMatches("/abc/{foo}","/abc/def");
checkStartNoMatch("/abc/{foo}","/abc/def/");
checkStartMatches("/abc/{foo}/","/abc/def/");
checkStartNoMatch("/abc/{foo}/","/abc/def/ghi");
checkStartMatches("/abc/{foo}/","/abc/def");
checkStartMatches("", "");
checkStartMatches("", null);
checkStartMatches("/abc", null);
checkStartMatches(null, "");
checkStartMatches(null, null);
checkStartNoMatch(null, "/abc");
}
@Test
public void caseSensitivity() {
PathPatternParser pp = new PathPatternParser();
pp.setCaseSensitive(false);
PathPattern p = pp.parse("abc");
assertTrue(p.matches("AbC"));
assertFalse(p.matches("def"));
p = pp.parse("fOo");
assertTrue(p.matches("FoO"));
p = pp.parse("/fOo/bAr");
assertTrue(p.matches("/FoO/BaR"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("abc");
assertFalse(p.matches("AbC"));
p = pp.parse("fOo");
assertFalse(p.matches("FoO"));
p = pp.parse("/fOo/bAr");
assertFalse(p.matches("/FoO/BaR"));
p = pp.parse("/fOO/bAr");
assertTrue(p.matches("/fOO/bAr"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("{foo:[A-Z]*}");
assertTrue(p.matches("abc"));
assertTrue(p.matches("ABC"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("{foo:[A-Z]*}");
assertFalse(p.matches("abc"));
assertTrue(p.matches("ABC"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("ab?");
assertTrue(p.matches("AbC"));
p = pp.parse("fO?");
assertTrue(p.matches("FoO"));
p = pp.parse("/fO?/bA?");
assertTrue(p.matches("/FoO/BaR"));
assertFalse(p.matches("/bAr/fOo"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("ab?");
assertFalse(p.matches("AbC"));
p = pp.parse("fO?");
assertFalse(p.matches("FoO"));
p = pp.parse("/fO?/bA?");
assertFalse(p.matches("/FoO/BaR"));
p = pp.parse("/fO?/bA?");
assertTrue(p.matches("/fOO/bAr"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("{abc:[A-Z]*}_{def:[A-Z]*}");
assertTrue(p.matches("abc_abc"));
assertTrue(p.matches("ABC_aBc"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("{abc:[A-Z]*}_{def:[A-Z]*}");
assertFalse(p.matches("abc_abc"));
assertTrue(p.matches("ABC_ABC"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("*?a?*");
assertTrue(p.matches("bab"));
assertTrue(p.matches("bAb"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("*?A?*");
assertFalse(p.matches("bab"));
assertTrue(p.matches("bAb"));
}
@Test
public void alternativeDelimiter() {
try {
separator = '.';
// test exact matching
checkMatches("test", "test");
checkMatches(".test", ".test");
checkNoMatch(".test/jpg", "test/jpg");
checkNoMatch("test", ".test");
checkNoMatch(".test", "test");
// test matching with ?'s
checkMatches("t?st", "test");
checkMatches("??st", "test");
checkMatches("tes?", "test");
checkMatches("te??", "test");
checkMatches("?es?", "test");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
// test matching with *'s
checkMatches("*", "test");
checkMatches("test*", "test");
checkMatches("test*", "testTest");
checkMatches("*test*", "AnothertestTest");
checkMatches("*test", "Anothertest");
checkMatches("*/*", "test/");
checkMatches("*/*", "test/test");
checkMatches("*/*", "test/test/test");
checkMatches("test*aaa", "testblaaaa");
checkNoMatch("test*", "tst");
checkNoMatch("test*", "tsttest");
checkNoMatch("*test*", "tsttst");
checkNoMatch("*test", "tsttst");
checkNoMatch("*/*", "tsttst");
checkNoMatch("test*aaa", "test");
checkNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and .'s
checkMatches(".?", ".a");
checkMatches(".?.a", ".a.a");
checkMatches(".a.?", ".a.b");
checkMatches(".??.a", ".aa.a");
checkMatches(".a.??", ".a.bb");
checkMatches(".?", ".a");
// test matching with **'s
checkMatches(".**", ".testing.testing");
checkMatches(".*.**", ".testing.testing");
checkMatches(".bla*bla.test", ".blaXXXbla.test");
checkMatches(".*bla.test", ".XXXbla.test");
checkNoMatch(".bla*bla.test", ".blaXXXbl.test");
checkNoMatch(".*bla.test", "XXXblab.test");
checkNoMatch(".*bla.test", "XXXbl.test");
}
finally {
separator = PathPatternParser.DEFAULT_SEPARATOR;
}
}
@Test
public void extractPathWithinPattern() throws Exception {
checkExtractPathWithinPattern("/welcome*/", "/welcome/","welcome");
checkExtractPathWithinPattern("/docs/commit.html","/docs/commit.html","");
checkExtractPathWithinPattern("/docs/*","/docs/cvs/commit","cvs/commit");
checkExtractPathWithinPattern("/docs/cvs/*.html","/docs/cvs/commit.html","commit.html");
checkExtractPathWithinPattern("/docs/**","/docs/cvs/commit","cvs/commit");
checkExtractPathWithinPattern("/doo/{*foobar}","/doo/customer.html","customer.html");
checkExtractPathWithinPattern("/doo/{*foobar}","/doo/daa/customer.html","daa/customer.html");
checkExtractPathWithinPattern("/*.html","/commit.html","commit.html");
checkExtractPathWithinPattern("/docs/*/*/*/*","/docs/cvs/other/commit.html","cvs/other/commit.html");
checkExtractPathWithinPattern("/d?cs/**","/docs/cvs/commit","docs/cvs/commit");
checkExtractPathWithinPattern("/docs/c?s/*.html","/docs/cvs/commit.html","cvs/commit.html");
checkExtractPathWithinPattern("/d?cs/*/*.html","/docs/cvs/commit.html","docs/cvs/commit.html");
checkExtractPathWithinPattern("/a/b/c*d*/*.html","/a/b/cod/foo.html","cod/foo.html");
checkExtractPathWithinPattern("a/{foo}/b/{bar}","a/c/b/d","c/b/d");
checkExtractPathWithinPattern("a/{foo}_{bar}/d/e","a/b_c/d/e","b_c/d/e");
checkExtractPathWithinPattern("aaa//*///ccc///ddd","aaa/bbb/ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa/*/ccc/ddd","aaa//bbb//ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa//*///ccc///ddd","aaa//bbb//ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa//*///ccc///ddd","aaa/////bbb//ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa/c*/ddd/","aaa/ccc///ddd///","ccc/ddd");
checkExtractPathWithinPattern("", "", "");
checkExtractPathWithinPattern("/", "", "");
checkExtractPathWithinPattern("", "/", "");
checkExtractPathWithinPattern("//", "", "");
checkExtractPathWithinPattern("", "//", "");
checkExtractPathWithinPattern("//", "//", "");
checkExtractPathWithinPattern("//", "/", "");
checkExtractPathWithinPattern("/", "//", "");
}
@Test
public void extractUriTemplateVariables() throws Exception {
checkCapture("/hotels/{hotel}", "/hotels/1", "hotel","1");
checkCapture("/h?tels/{hotel}","/hotels/1","hotel","1");
checkCapture("/hotels/{hotel}/bookings/{booking}","/hotels/1/bookings/2","hotel","1","booking","2");
checkCapture("/*/hotels/*/{hotel}","/foo/hotels/bar/1","hotel","1");
checkCapture("/{page}.html","/42.html","page","42");
checkCapture("/{page}.*","/42.html","page","42");
checkCapture("/A-{B}-C","/A-b-C","B","b");
checkCapture("/{name}.{extension}","/test.html","name","test","extension","html");
try {
checkCapture("/{one}/", "//", "one", "");
fail("Expected exception");
} catch (IllegalStateException e) {
assertEquals("Pattern \"/{one}/\" is not a match for \"//\"",e.getMessage());
}
try {
checkCapture("", "/abc");
fail("Expected exception");
} catch (IllegalStateException e) {
assertEquals("Pattern \"\" is not a match for \"/abc\"",e.getMessage());
}
assertEquals(0,checkCapture("", "").size());
checkCapture("{id}", "99", "id", "99");
checkCapture("/customer/{customerId}", "/customer/78", "customerId", "78");
checkCapture("/customer/{customerId}/banana", "/customer/42/banana", "customerId",
"42");
checkCapture("{id}/{id2}", "99/98", "id", "99", "id2", "98");
checkCapture("/foo/{bar}/boo/{baz}", "/foo/plum/boo/apple", "bar", "plum", "baz",
"apple");
checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
Map<String,String> extracted = checkCapture("/abc","/abc");
assertEquals(0,extracted.size());
}
@Test
public void extractUriTemplateVariablesRegex() {
PathPatternParser pp = new PathPatternParser();
PathPattern p = null;
p = pp.parse("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar");
Map<String, String> result = p.matchAndExtract("com.example-1.0.0.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar");
result = p.matchAndExtract("com.example-sources-1.0.0.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
}
@Test
public void extractUriTemplateVarsRegexQualifiers() {
PathPatternParser pp = new PathPatternParser();
PathPattern p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
Map<String, String> result = p.matchAndExtract("com.example-sources-1.0.0.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar");
result = p.matchAndExtract("com.example-sources-1.0.0-20100220.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
assertEquals("2010", result.get("year"));
assertEquals("02", result.get("month"));
assertEquals("20", result.get("day"));
p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar");
result = p.matchAndExtract("com.example-sources-1.0.0.{12}.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0.{12}", result.get("version"));
}
@Test
public void extractUriTemplateVarsRegexCapturingGroups() {
PathPatternParser pp = new PathPatternParser();
PathPattern pathMatcher = pp.parse("/web/{id:foo(bar)?}_{goo}");
exception.expect(IllegalArgumentException.class);
exception.expectMessage(containsString("The number of capturing groups in the pattern"));
pathMatcher.matchAndExtract("/web/foobar_goo");
}
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void combine() {
TestPathCombiner pathMatcher = new TestPathCombiner();
assertEquals("", pathMatcher.combine(null, null));
assertEquals("/hotels", pathMatcher.combine("/hotels", null));
assertEquals("/hotels", pathMatcher.combine(null, "/hotels"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "/booking"));
assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "booking"));
assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "/booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "/booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels/", "booking"));
assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels/*", "{hotel}"));
assertEquals("/hotels/**/{hotel}", pathMatcher.combine("/hotels/**", "{hotel}"));
assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels", "{hotel}"));
assertEquals("/hotels/{hotel}.*", pathMatcher.combine("/hotels", "{hotel}.*"));
assertEquals("/hotels/*/booking/{booking}",
pathMatcher.combine("/hotels/*/booking", "{booking}"));
assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.html"));
assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel"));
assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.*"));
// TODO this seems rather bogus, should we eagerly show an error?
assertEquals("/d/e/f/hotel.html", pathMatcher.combine("/a/b/c/*.html", "/d/e/f/hotel.*"));
assertEquals("/*.html", pathMatcher.combine("/**", "/*.html"));
assertEquals("/*.html", pathMatcher.combine("/*", "/*.html"));
assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html"));
assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858
assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970
assertEquals("/{foo:.*[^0-9].*}/edit/",
pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062
assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test"));
// SPR-10554
assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975
assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); // SPR-12975
assertEquals("",pathMatcher.combine(null, null));
assertEquals("",pathMatcher.combine(null, ""));
assertEquals("",pathMatcher.combine("",null));
assertEquals("",pathMatcher.combine(null, null));
assertEquals("",pathMatcher.combine("", ""));
assertEquals("/hotel",pathMatcher.combine("", "/hotel"));
assertEquals("/hotel",pathMatcher.combine("/hotel", null));
assertEquals("/hotel",pathMatcher.combine("/hotel", ""));
// TODO Do we need special handling when patterns contain multiple dots?
}
@Test
public void combineWithTwoFileExtensionPatterns() {
TestPathCombiner pathMatcher = new TestPathCombiner();
exception.expect(IllegalArgumentException.class);
pathMatcher.combine("/*.html", "/*.txt");
}
@Test
public void patternComparator() {
Comparator<PathPattern> comparator = new PatternComparatorConsideringPath(
"/hotels/new");
assertEquals(0, comparator.compare(null, null));
assertEquals(1, comparator.compare(null, parse("/hotels/new")));
assertEquals(-1, comparator.compare(parse("/hotels/new"), null));
assertEquals(0, comparator.compare(parse("/hotels/new"), parse("/hotels/new")));
assertEquals(-1, comparator.compare(parse("/hotels/new"), parse("/hotels/*")));
assertEquals(1, comparator.compare(parse("/hotels/*"), parse("/hotels/new")));
assertEquals(0, comparator.compare(parse("/hotels/*"), parse("/hotels/*")));
assertEquals(-1,
comparator.compare(parse("/hotels/new"), parse("/hotels/{hotel}")));
assertEquals(1,
comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/new")));
assertEquals(0,
comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/{hotel}")));
assertEquals(-1, comparator.compare(parse("/hotels/{hotel}/booking"),
parse("/hotels/{hotel}/bookings/{booking}")));
assertEquals(1, comparator.compare(parse("/hotels/{hotel}/bookings/{booking}"),
parse("/hotels/{hotel}/booking")));
assertEquals(-1,
comparator.compare(
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"),
parse("/**")));
assertEquals(1, comparator.compare(parse("/**"),
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")));
assertEquals(0, comparator.compare(parse("/**"), parse("/**")));
assertEquals(-1,
comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/*")));
assertEquals(1, comparator.compare(parse("/hotels/*"), parse("/hotels/{hotel}")));
assertEquals(-1, comparator.compare(parse("/hotels/*"), parse("/hotels/*/**")));
assertEquals(1, comparator.compare(parse("/hotels/*/**"), parse("/hotels/*")));
assertEquals(-1,
comparator.compare(parse("/hotels/new"), parse("/hotels/new.*")));
// SPR-6741
assertEquals(-1,
comparator.compare(
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"),
parse("/hotels/**")));
assertEquals(1, comparator.compare(parse("/hotels/**"),
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")));
assertEquals(1, comparator.compare(parse("/hotels/foo/bar/**"),
parse("/hotels/{hotel}")));
assertEquals(-1, comparator.compare(parse("/hotels/{hotel}"),
parse("/hotels/foo/bar/**")));
// SPR-8683
assertEquals(1, comparator.compare(parse("/**"), parse("/hotels/{hotel}")));
// longer is better
assertEquals(1, comparator.compare(parse("/hotels"), parse("/hotels2")));
// SPR-13139
assertEquals(-1, comparator.compare(parse("*"), parse("*/**")));
assertEquals(1, comparator.compare(parse("*/**"), parse("*")));
}
@Test
public void pathPatternComparator() {
PathPatternComparator ppc = new PathPatternComparator();
assertEquals(0,ppc.compare(null, null));
assertEquals(1,ppc.compare(null, parse("")));
assertEquals(-1,ppc.compare(parse(""), null));
assertEquals(0,ppc.compare(parse(""), parse("")));
}
@Test
public void patternCompareTo() {
PathPatternParser p = new PathPatternParser();
PathPattern pp = p.parse("/abc");
assertEquals(-1,pp.compareTo(null));
}
@Test
public void patternComparatorSort() {
Comparator<PathPattern> comparator = new PatternComparatorConsideringPath(
"/hotels/new");
List<PathPattern> paths = new ArrayList<>(3);
PathPatternParser pp = new PathPatternParser();
paths.add(null);
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertNull(paths.get(1));
paths.clear();
paths.add(pp.parse("/hotels/new"));
paths.add(null);
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertNull(paths.get(1));
paths.clear();
paths.add(pp.parse("/hotels/*"));
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/*", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/new"));
paths.add(pp.parse("/hotels/*"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/*", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/**"));
paths.add(pp.parse("/hotels/*"));
Collections.sort(paths, comparator);
assertEquals("/hotels/*", paths.get(0).getPatternString());
assertEquals("/hotels/**", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/*"));
paths.add(pp.parse("/hotels/**"));
Collections.sort(paths, comparator);
assertEquals("/hotels/*", paths.get(0).getPatternString());
assertEquals("/hotels/**", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/{hotel}"));
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/{hotel}", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/new"));
paths.add(pp.parse("/hotels/{hotel}"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/{hotel}", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/*"));
paths.add(pp.parse("/hotels/{hotel}"));
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/{hotel}", paths.get(1).getPatternString());
assertEquals("/hotels/*", paths.get(2).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/ne*"));
paths.add(pp.parse("/hotels/n*"));
Collections.shuffle(paths);
Collections.sort(paths, comparator);
assertEquals("/hotels/ne*", paths.get(0).getPatternString());
assertEquals("/hotels/n*", paths.get(1).getPatternString());
paths.clear();
// comparator = new PatternComparatorConsideringPath("/hotels/new.html");
// paths.add(pp.parse("/hotels/new.*"));
// paths.add(pp.parse("/hotels/{hotel}"));
// Collections.shuffle(paths);
// Collections.sort(paths, comparator);
// assertEquals("/hotels/new.*", paths.get(0).toPatternString());
// assertEquals("/hotels/{hotel}", paths.get(1).toPatternString());
// paths.clear();
comparator = new PatternComparatorConsideringPath("/web/endUser/action/login.html");
paths.add(pp.parse("/*/login.*"));
paths.add(pp.parse("/*/endUser/action/login.*"));
Collections.sort(paths, comparator);
assertEquals("/*/endUser/action/login.*", paths.get(0).getPatternString());
assertEquals("/*/login.*", paths.get(1).getPatternString());
paths.clear();
}
@Test // SPR-13286
public void caseInsensitive() {
PathPatternParser pp = new PathPatternParser();
pp.setCaseSensitive(false);
PathPattern p = pp.parse("/group/{groupName}/members");
assertTrue(p.matches("/group/sales/members"));
assertTrue(p.matches("/Group/Sales/Members"));
assertTrue(p.matches("/group/Sales/members"));
}
@Test
public void patternmessage() {
PatternMessage[] values = PatternMessage.values();
assertNotNull(values);
for (PatternMessage pm: values) {
String name = pm.toString();
assertEquals(pm.ordinal(),PatternMessage.valueOf(name).ordinal());
}
}
private PathPattern parse(String path) {
PathPatternParser pp = new PathPatternParser();
return pp.parse(path);
}
private char separator = PathPatternParser.DEFAULT_SEPARATOR;
private void checkMatches(String uriTemplate, String path) {
PathPatternParser parser = (separator == PathPatternParser.DEFAULT_SEPARATOR
? new PathPatternParser() : new PathPatternParser(separator));
PathPattern p = parser.parse(uriTemplate);
assertTrue(p.matches(path));
}
private void checkStartNoMatch(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
PathPattern pattern = p.parse(uriTemplate);
assertFalse(pattern.matchStart(path));
}
private void checkStartMatches(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
PathPattern pattern = p.parse(uriTemplate);
assertTrue(pattern.matchStart(path));
}
private void checkNoMatch(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
PathPattern pattern = p.parse(uriTemplate);
assertFalse(pattern.matches(path));
}
private Map<String,String> checkCapture(String uriTemplate, String path, String... keyValues) {
PathPatternParser parser = new PathPatternParser();
PathPattern pattern = parser.parse(uriTemplate);
Map<String, String> matchResults = pattern.matchAndExtract(path);
Map<String, String> expectedKeyValues = new HashMap<>();
if (keyValues != null) {
for (int i = 0; i < keyValues.length; i += 2) {
expectedKeyValues.put(keyValues[i], keyValues[i + 1]);
}
}
Map<String, String> capturedVariables = matchResults;
for (Map.Entry<String, String> me : expectedKeyValues.entrySet()) {
String value = capturedVariables.get(me.getKey());
if (value == null) {
fail("Did not find key '" + me.getKey() + "' in captured variables: "
+ capturedVariables);
}
if (!value.equals(me.getValue())) {
fail("Expected value '" + me.getValue() + "' for key '" + me.getKey()
+ "' but was '" + value + "'");
}
}
return capturedVariables;
}
private void checkExtractPathWithinPattern(String pattern, String path, String expected) {
PathPatternParser ppp = new PathPatternParser();
PathPattern pp = ppp.parse(pattern);
String s = pp.extractPathWithinPattern(path);
assertEquals(expected,s);
}
static class TestPathCombiner {
PathPatternParser pp = new PathPatternParser();
public String combine(String string1, String string2) {
PathPattern pattern1 = pp.parse(string1);
return pattern1.combine(string2);
}
}
}

View File

@@ -0,0 +1,465 @@
/*
* Copyright 2016 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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Exercise the {@link PathPatternParser}.
*
* @author Andy Clement
*/
public class PathPatternParserTests {
private PathPattern p;
@Test
public void basicPatterns() {
checkStructure("/");
checkStructure("/foo");
checkStructure("foo");
checkStructure("foo/");
checkStructure("/foo/");
checkStructure("//");
}
@Test
public void singleCharWildcardPatterns() {
p = checkStructure("?");
assertPathElements(p , SingleCharWildcardedPathElement.class);
checkStructure("/?/");
checkStructure("//?abc?/");
}
@Test
public void multiwildcardPattern() {
p = checkStructure("/**");
assertPathElements(p,WildcardTheRestPathElement.class);
p = checkStructure("/**acb"); // this is not double wildcard use, it is / then **acb (an odd, unnecessary use of double *)
assertPathElements(p,SeparatorPathElement.class, RegexPathElement.class);
}
@Test
public void toStringTests() {
assertEquals("CaptureTheRest(/{*foobar})", checkStructure("/{*foobar}").toChainString());
assertEquals("CaptureVariable({foobar})", checkStructure("{foobar}").toChainString());
assertEquals("Literal(abc)", checkStructure("abc").toChainString());
assertEquals("Regex({a}_*_{b})", checkStructure("{a}_*_{b}").toChainString());
assertEquals("Separator(/)", checkStructure("/").toChainString());
assertEquals("SingleCharWildcarding(?a?b?c)", checkStructure("?a?b?c").toChainString());
assertEquals("Wildcard(*)", checkStructure("*").toChainString());
assertEquals("WildcardTheRest(/**)", checkStructure("/**").toChainString());
}
@Test
public void captureTheRestPatterns() {
checkError("/{*foobar}x{abc}", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
p = checkStructure("{*foobar}");
assertPathElements(p, CaptureTheRestPathElement.class);
p = checkStructure("/{*foobar}");
assertPathElements(p, CaptureTheRestPathElement.class);
checkError("/{*foobar}/", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*foobar}abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*f%obar}", 4, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}abc",10,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{f*oobar}",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}/abc",10,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{abc}{*foobar}",1,PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{abc}{*foobar}{foo}",15,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
}
@Test
public void equalsAndHashcode() {
PathPatternParser caseInsensitiveParser = new PathPatternParser();
caseInsensitiveParser.setCaseSensitive(false);
PathPatternParser caseSensitiveParser = new PathPatternParser();
PathPattern pp1 = caseInsensitiveParser.parse("/abc");
PathPattern pp2 = caseInsensitiveParser.parse("/abc");
PathPattern pp3 = caseInsensitiveParser.parse("/def");
assertEquals(pp1,pp2);
assertEquals(pp1.hashCode(),pp2.hashCode());
assertNotEquals(pp1, pp3);
assertFalse(pp1.equals("abc"));
pp1 = caseInsensitiveParser.parse("/abc");
pp2 = caseSensitiveParser.parse("/abc");
assertFalse(pp1.equals(pp2));
assertNotEquals(pp1.hashCode(),pp2.hashCode());
PathPatternParser alternateSeparatorParser = new PathPatternParser(':');
pp1 = caseInsensitiveParser.parse("abc");
pp2 = alternateSeparatorParser.parse("abc");
assertFalse(pp1.equals(pp2));
assertNotEquals(pp1.hashCode(),pp2.hashCode());
}
@Test
public void regexPathElementPatterns() {
checkError("/{var:[^/]*}", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{var:abc", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{var:a{{1,2}}}", 6, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
p = checkStructure("/{var:\\\\}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
assertTrue(p.matches("/\\"));
p = checkStructure("/{var:\\/}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
assertFalse(p.matches("/aaa"));
p = checkStructure("/{var:a{1,2}}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
p = checkStructure("/{var:[^\\/]*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
Map<String, String> result = p.matchAndExtract("/foo");
assertEquals("foo",result.get("var"));
p = checkStructure("/{var:\\[*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/[[[");
assertEquals("[[[",result.get("var"));
p = checkStructure("/{var:[\\{]*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/{{{");
assertEquals("{{{",result.get("var"));
p = checkStructure("/{var:[\\}]*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/}}}");
assertEquals("}}}",result.get("var"));
p = checkStructure("*");
assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName());
checkStructure("/*");
checkStructure("/*/");
checkStructure("*/");
checkStructure("/*/");
p = checkStructure("/*a*/");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().next.getClass().getName());
p = checkStructure("*/");
assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName());
checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo");
checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
checkError("/{bar}/{bar}_{foo}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
p = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName());
}
@Test
public void completeCapturingPatterns() {
p = checkStructure("{foo}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().getClass().getName());
checkStructure("/{foo}");
checkStructure("//{f}/");
checkStructure("/{foo}/{bar}/{wibble}");
}
@Test
public void completeCaptureWithConstraints() {
p = checkStructure("{foo:...}");
assertPathElements(p, CaptureVariablePathElement.class);
p = checkStructure("{foo:[0-9]*}");
assertPathElements(p, CaptureVariablePathElement.class);
checkError("{foo:}",5,PatternMessage.MISSING_REGEX_CONSTRAINT);
}
@Test
public void partialCapturingPatterns() {
p = checkStructure("{foo}abc");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName());
checkStructure("abc{foo}");
checkStructure("/abc{foo}");
checkStructure("{foo}def/");
checkStructure("/abc{foo}def/");
checkStructure("{foo}abc{bar}");
checkStructure("{foo}abc{bar}/");
checkStructure("/{foo}abc{bar}/");
}
@Test
public void illegalCapturePatterns() {
checkError("{abc/",4,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc:}/",5,PatternMessage.MISSING_REGEX_CONSTRAINT);
checkError("{",1,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc",4,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{/}",1,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{",3,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("}",0,PatternMessage.MISSING_OPEN_CAPTURE);
checkError("/}",1,PatternMessage.MISSING_OPEN_CAPTURE);
checkError("def}",3,PatternMessage.MISSING_OPEN_CAPTURE);
checkError("//{/}",3,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{{/}",3,PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("//{abc{/}",6,PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("/{0abc}/abc",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR);
checkError("/{a?bc}/abc",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{abc}_{abc}",1,PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
checkError("/foobar/{abc}_{abc}",8,PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
checkError("/foobar/{abc:..}_{abc:..}",8,PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
PathPattern pp = parse("/{abc:foo(bar)}");
try {
pp.matchAndExtract("/foo");
fail("Should have raised exception");
} catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage());
}
try {
pp.matchAndExtract("/foobar");
fail("Should have raised exception");
} catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage());
}
}
@Test
public void badPatterns() {
// checkError("/{foo}{bar}/",6,PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
checkError("/{?}/",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"?");
checkError("/{a?b}/",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR,"?");
checkError("/{%%$}",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"%");
checkError("/{ }",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR," ");
checkError("/{%:[0-9]*}",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"%");
}
@Test
public void patternPropertyGetCaptureCountTests() {
// Test all basic section types
assertEquals(1,parse("{foo}").getCapturedVariableCount());
assertEquals(0,parse("foo").getCapturedVariableCount());
assertEquals(1,parse("{*foobar}").getCapturedVariableCount());
assertEquals(1,parse("/{*foobar}").getCapturedVariableCount());
assertEquals(0,parse("/**").getCapturedVariableCount());
assertEquals(1,parse("{abc}asdf").getCapturedVariableCount());
assertEquals(1,parse("{abc}_*").getCapturedVariableCount());
assertEquals(2,parse("{abc}_{def}").getCapturedVariableCount());
assertEquals(0,parse("/").getCapturedVariableCount());
assertEquals(0,parse("a?b").getCapturedVariableCount());
assertEquals(0,parse("*").getCapturedVariableCount());
// Test on full templates
assertEquals(0,parse("/foo/bar").getCapturedVariableCount());
assertEquals(1,parse("/{foo}").getCapturedVariableCount());
assertEquals(2,parse("/{foo}/{bar}").getCapturedVariableCount());
assertEquals(4,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getCapturedVariableCount());
}
@Test
public void patternPropertyGetWildcardCountTests() {
// Test all basic section types
assertEquals(computeScore(1,0),parse("{foo}").getScore());
assertEquals(computeScore(0,0),parse("foo").getScore());
assertEquals(computeScore(0,0),parse("{*foobar}").getScore());
// assertEquals(1,parse("/**").getScore());
assertEquals(computeScore(1,0),parse("{abc}asdf").getScore());
assertEquals(computeScore(1,1),parse("{abc}_*").getScore());
assertEquals(computeScore(2,0),parse("{abc}_{def}").getScore());
assertEquals(computeScore(0,0),parse("/").getScore());
assertEquals(computeScore(0,0),parse("a?b").getScore()); // currently deliberate
assertEquals(computeScore(0,1),parse("*").getScore());
// Test on full templates
assertEquals(computeScore(0,0),parse("/foo/bar").getScore());
assertEquals(computeScore(1,0),parse("/{foo}").getScore());
assertEquals(computeScore(2,0),parse("/{foo}/{bar}").getScore());
assertEquals(computeScore(4,0),parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getScore());
assertEquals(computeScore(4,3),parse("/{foo}/*/*_*/{bar}_{goo}_{wibble}/abc/bar").getScore());
}
@Test
public void multipleSeparatorPatterns() {
p = checkStructure("///aaa");
assertEquals(4,p.getNormalizedLength());
assertPathElements(p,SeparatorPathElement.class,LiteralPathElement.class);
p = checkStructure("///aaa////aaa/b");
assertEquals(10,p.getNormalizedLength());
assertPathElements(p,SeparatorPathElement.class, LiteralPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
p = checkStructure("/////**");
assertEquals(1,p.getNormalizedLength());
assertPathElements(p,WildcardTheRestPathElement.class);
}
@Test
public void patternPropertyGetLengthTests() {
// Test all basic section types
assertEquals(1,parse("{foo}").getNormalizedLength());
assertEquals(3,parse("foo").getNormalizedLength());
assertEquals(1,parse("{*foobar}").getNormalizedLength());
assertEquals(1,parse("/{*foobar}").getNormalizedLength());
assertEquals(1,parse("/**").getNormalizedLength());
assertEquals(5,parse("{abc}asdf").getNormalizedLength());
assertEquals(3,parse("{abc}_*").getNormalizedLength());
assertEquals(3,parse("{abc}_{def}").getNormalizedLength());
assertEquals(1,parse("/").getNormalizedLength());
assertEquals(3,parse("a?b").getNormalizedLength());
assertEquals(1,parse("*").getNormalizedLength());
// Test on full templates
assertEquals(8,parse("/foo/bar").getNormalizedLength());
assertEquals(2,parse("/{foo}").getNormalizedLength());
assertEquals(4,parse("/{foo}/{bar}").getNormalizedLength());
assertEquals(16,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getNormalizedLength());
}
@Test
public void compareTests() {
PathPattern p1,p2,p3;
// Based purely on number of captures
p1 = parse("{a}");
p2 = parse("{a}/{b}");
p3 = parse("{a}/{b}/{c}");
assertEquals(-1,p1.compareTo(p2)); // Based on number of captures
List<PathPattern> patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p1,patterns.get(0));
// Based purely on length
p1 = parse("/a/b/c");
p2 = parse("/a/boo/c/doo");
p3 = parse("/asdjflaksjdfjasdf");
assertEquals(1,p1.compareTo(p2));
patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p3,patterns.get(0));
// Based purely on 'wildness'
p1 = parse("/*");
p2 = parse("/*/*");
p3 = parse("/*/*/*_*");
assertEquals(-1,p1.compareTo(p2));
patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p1,patterns.get(0));
// Based purely on catchAll
p1 = parse("{*foobar}");
p2 = parse("{*goo}");
assertEquals(0,p1.compareTo(p2));
p1 = parse("/{*foobar}");
p2 = parse("/abc/{*ww}");
assertEquals(+1,p1.compareTo(p2));
assertEquals(-1,p2.compareTo(p1));
p3 = parse("/this/that/theother");
assertTrue(p1.isCatchAll());
assertTrue(p2.isCatchAll());
assertFalse(p3.isCatchAll());
patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p3,patterns.get(0));
assertEquals(p2,patterns.get(1));
patterns = new ArrayList<>();
patterns.add(parse("/abc"));
patterns.add(null);
patterns.add(parse("/def"));
Collections.sort(patterns,new PathPatternComparator());
assertNull(patterns.get(2));
}
// ---
private PathPattern parse(String pattern) {
PathPatternParser patternParser = new PathPatternParser();
return patternParser.parse(pattern);
}
/**
* Verify the parsed chain of sections matches the original pattern and the separator count
* that has been determined is correct.
*/
private PathPattern checkStructure(String pattern) {
int count = 0;
for (int i=0;i<pattern.length();i++) {
if (pattern.charAt(i)=='/') {
// if (peekDoubleWildcard(pattern,i)) {
// // it is /**
// i+=2;
// } else {
count++;
// }
}
}
return checkStructure(pattern,count);
}
private PathPattern checkStructure(String pattern, int expectedSeparatorCount) {
p = parse(pattern);
assertEquals(pattern,p.getPatternString());
// assertEquals(expectedSeparatorCount,p.getSeparatorCount());
return p;
}
private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage, String... expectedInserts) {
try {
p = parse(pattern);
fail("Expected to fail");
} catch (PatternParseException ppe) {
// System.out.println(ppe.toDetailedString());
assertEquals(ppe.toDetailedString(), expectedPos, ppe.getPosition());
assertEquals(ppe.toDetailedString(), expectedMessage, ppe.getMessageType());
if (expectedInserts.length!=0) {
assertEquals(ppe.getInserts().length,expectedInserts.length);
for (int i=0;i<expectedInserts.length;i++) {
assertEquals("Insert at position "+i+" is wrong",expectedInserts[i],ppe.getInserts()[i]);
}
}
}
}
@SafeVarargs
private final void assertPathElements(PathPattern p, Class<? extends PathElement>... sectionClasses) {
PathElement head = p.getHeadSection();
for (int i=0;i<sectionClasses.length;i++) {
if (head == null) {
fail("Ran out of data in parsed pattern. Pattern is: "+p.toChainString());
}
assertEquals("Not expected section type. Pattern is: "+p.toChainString(),sectionClasses[i].getSimpleName(),head.getClass().getSimpleName());
head = head.next;
}
}
// Mirrors the score computation logic in PathPattern
private int computeScore(int capturedVariableCount, int wildcardCount) {
return capturedVariableCount+wildcardCount*100;
}
}