/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.util.pattern; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.junit.Test; import org.springframework.web.util.pattern.CaptureTheRestPathElement; import org.springframework.web.util.pattern.CaptureVariablePathElement; import org.springframework.web.util.pattern.LiteralPathElement; import org.springframework.web.util.pattern.PathElement; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; import org.springframework.web.util.pattern.PatternParseException; import org.springframework.web.util.pattern.PatternParseException.PatternMessage; import org.springframework.web.util.pattern.RegexPathElement; import org.springframework.web.util.pattern.SeparatorPathElement; import org.springframework.web.util.pattern.SingleCharWildcardedPathElement; import org.springframework.web.util.pattern.WildcardPathElement; import org.springframework.web.util.pattern.WildcardTheRestPathElement; import static org.junit.Assert.*; /** * Exercise the {@link PathPatternParser}. * * @author Andy Clement */ public class PathPatternParserTests { private PathPattern pathPattern; @Test public void basicPatterns() { checkStructure("/"); checkStructure("/foo"); checkStructure("foo"); checkStructure("foo/"); checkStructure("/foo/"); checkStructure(""); } @Test public void singleCharWildcardPatterns() { pathPattern = checkStructure("?"); assertPathElements(pathPattern, SingleCharWildcardedPathElement.class); checkStructure("/?/"); checkStructure("/?abc?/"); } @Test public void multiwildcardPattern() { pathPattern = checkStructure("/**"); assertPathElements(pathPattern, WildcardTheRestPathElement.class); pathPattern = checkStructure("/**acb"); // this is not double wildcard use, it is / then **acb (an odd, unnecessary use of double *) assertPathElements(pathPattern, 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); pathPattern = checkStructure("{*foobar}"); assertPathElements(pathPattern, CaptureTheRestPathElement.class); pathPattern = checkStructure("/{*foobar}"); assertPathElements(pathPattern, 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.REGEX_PATTERN_SYNTAX_EXCEPTION); pathPattern = checkStructure("/{var:\\\\}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); assertTrue(pathPattern.matches("/\\")); pathPattern = checkStructure("/{var:\\/}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); assertFalse(pathPattern.matches("/aaa")); pathPattern = checkStructure("/{var:a{1,2}}", 1); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); pathPattern = checkStructure("/{var:[^\\/]*}", 1); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); Map<String, String> result = pathPattern.matchAndExtract("/foo"); assertEquals("foo", result.get("var")); pathPattern = checkStructure("/{var:\\[*}", 1); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); result = pathPattern.matchAndExtract("/[[["); assertEquals("[[[", result.get("var")); pathPattern = checkStructure("/{var:[\\{]*}", 1); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); result = pathPattern.matchAndExtract("/{{{"); assertEquals("{{{", result.get("var")); pathPattern = checkStructure("/{var:[\\}]*}", 1); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); result = pathPattern.matchAndExtract("/}}}"); assertEquals("}}}", result.get("var")); pathPattern = checkStructure("*"); assertEquals(WildcardPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName()); checkStructure("/*"); checkStructure("/*/"); checkStructure("*/"); checkStructure("/*/"); pathPattern = checkStructure("/*a*/"); assertEquals(RegexPathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); pathPattern = checkStructure("*/"); assertEquals(WildcardPathElement.class.getName(), pathPattern.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"); pathPattern = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar"); assertEquals(RegexPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName()); } @Test public void completeCapturingPatterns() { pathPattern = checkStructure("{foo}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().getClass().getName()); checkStructure("/{foo}"); checkStructure("/{f}/"); checkStructure("/{foo}/{bar}/{wibble}"); } @Test public void completeCaptureWithConstraints() { pathPattern = checkStructure("{foo:...}"); assertPathElements(pathPattern, CaptureVariablePathElement.class); pathPattern = checkStructure("{foo:[0-9]*}"); assertPathElements(pathPattern, CaptureVariablePathElement.class); checkError("{foo:}", 5, PatternMessage.MISSING_REGEX_CONSTRAINT); } @Test public void partialCapturingPatterns() { pathPattern = checkStructure("{foo}abc"); assertEquals(RegexPathElement.class.getName(), pathPattern.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("/{", 2, PatternMessage.MISSING_CLOSE_CAPTURE); checkError("}", 0, PatternMessage.MISSING_OPEN_CAPTURE); checkError("/}", 1, PatternMessage.MISSING_OPEN_CAPTURE); checkError("def}", 3, PatternMessage.MISSING_OPEN_CAPTURE); checkError("/{/}", 2, PatternMessage.MISSING_CLOSE_CAPTURE); checkError("/{{/}", 2, PatternMessage.ILLEGAL_NESTED_CAPTURE); checkError("/{abc{/}", 5, 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() { pathPattern = checkStructure("///aaa"); assertEquals(6, pathPattern.getNormalizedLength()); assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, LiteralPathElement.class); pathPattern = checkStructure("///aaa////aaa/b"); assertEquals(15, pathPattern.getNormalizedLength()); assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class); pathPattern = checkStructure("/////**"); assertEquals(5, pathPattern.getNormalizedLength()); assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, 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); 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); 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); 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); assertEquals(p3, patterns.get(0)); assertEquals(p2, patterns.get(1)); } 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) { pathPattern = parse(pattern); assertEquals(pattern, pathPattern.getPatternString()); // assertEquals(expectedSeparatorCount, pathPattern.getSeparatorCount()); return pathPattern; } private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage, String... expectedInserts) { try { pathPattern = parse(pattern); fail("Expected to fail"); } catch (PatternParseException ppe) { 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 (Class<? extends PathElement> sectionClass : sectionClasses) { if (head == null) { fail("Ran out of data in parsed pattern. Pattern is: " + p.toChainString()); } assertEquals("Not expected section type. Pattern is: " + p.toChainString(), sectionClass.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; } }