Browse Source
This commit adds the new `PathPatternRegistry`, which holds a sorted set of `PathPattern`s and allows for searching/adding patterns This registry is being used in `HandlerMapping` implementations and separates path pattern parsing/matching logic from the rest. Directly using `PathPattern` instances should improve the performance of those `HandlerMapping` implementations, since the parsing and generation of pattern variants (trailing slash, suffix patterns, etc) is done only once. Issue: SPR-14544pull/1293/head
24 changed files with 868 additions and 580 deletions
@ -0,0 +1,323 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2017 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.web.util.patterns; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.SortedSet; |
||||||
|
import java.util.TreeSet; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registry that holds {@code PathPattern}s instances |
||||||
|
* sorted according to their specificity (most specific patterns first). |
||||||
|
* <p>For a given path pattern string, {@code PathPattern} variants |
||||||
|
* can be generated and registered automatically, depending |
||||||
|
* on the {@code useTrailingSlashMatch}, {@code useSuffixPatternMatch} |
||||||
|
* and {@code fileExtensions} properties. |
||||||
|
* |
||||||
|
* @author Brian Clozel |
||||||
|
* @since 5.0 |
||||||
|
*/ |
||||||
|
public class PathPatternRegistry { |
||||||
|
|
||||||
|
private final PathPatternParser pathPatternParser; |
||||||
|
|
||||||
|
private final HashSet<PathPattern> patterns; |
||||||
|
|
||||||
|
private boolean useSuffixPatternMatch = false; |
||||||
|
|
||||||
|
private boolean useTrailingSlashMatch = false; |
||||||
|
|
||||||
|
private Set<String> fileExtensions = Collections.emptySet(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@code PathPatternRegistry} with defaults options for |
||||||
|
* pattern variants generation. |
||||||
|
* <p>By default, no pattern variant will be generated. |
||||||
|
*/ |
||||||
|
public PathPatternRegistry() { |
||||||
|
this.pathPatternParser = new PathPatternParser(); |
||||||
|
this.patterns = new HashSet<>(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether to match to paths irrespective of the presence of a trailing slash. |
||||||
|
*/ |
||||||
|
public boolean useSuffixPatternMatch() { |
||||||
|
return useSuffixPatternMatch; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether to use suffix pattern match (".*") when matching patterns to |
||||||
|
* requests. If enabled a path pattern such as "/users" will also |
||||||
|
* generate the following pattern variant: "/users.*". |
||||||
|
* <p>By default this is set to {@code false}. |
||||||
|
*/ |
||||||
|
public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { |
||||||
|
this.useSuffixPatternMatch = useSuffixPatternMatch; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether to generate path pattern variants with a trailing slash. |
||||||
|
*/ |
||||||
|
public boolean useTrailingSlashMatch() { |
||||||
|
return useTrailingSlashMatch; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether to match to paths irrespective of the presence of a trailing slash. |
||||||
|
* If enabled a path pattern such as "/users" will also generate the |
||||||
|
* following pattern variant: "/users/". |
||||||
|
* <p>The default value is {@code false}. |
||||||
|
*/ |
||||||
|
public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { |
||||||
|
this.useTrailingSlashMatch = useTrailingSlashMatch; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the set of file extensions to use for suffix pattern matching. |
||||||
|
*/ |
||||||
|
public Set<String> getFileExtensions() { |
||||||
|
return fileExtensions; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Configure the set of file extensions to use for suffix pattern matching. |
||||||
|
* For a given path "/users", each file extension will be used to |
||||||
|
* generate a path pattern variant such as "json" -> "/users.json". |
||||||
|
* <p>The default value is an empty {@code Set} |
||||||
|
*/ |
||||||
|
public void setFileExtensions(Set<String> fileExtensions) { |
||||||
|
this.fileExtensions = fileExtensions; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a (read-only) set of all patterns, sorted according to their specificity. |
||||||
|
*/ |
||||||
|
public Set<PathPattern> getPatterns() { |
||||||
|
return Collections.unmodifiableSet(this.patterns); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a {@code SortedSet} of {@code PathPattern}s matching the given {@code lookupPath}. |
||||||
|
* |
||||||
|
* <p>The returned set sorted with the most specific |
||||||
|
* patterns first, according to the given {@code lookupPath}. |
||||||
|
* @param lookupPath the URL lookup path to be matched against |
||||||
|
*/ |
||||||
|
public SortedSet<PathPattern> findMatches(String lookupPath) { |
||||||
|
return this.patterns.stream() |
||||||
|
.filter(pattern -> pattern.matches(lookupPath)) |
||||||
|
.collect(Collectors.toCollection(() -> |
||||||
|
new TreeSet<>(new PatternSetComparator(lookupPath)))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Process the path pattern data using the internal {@link PathPatternParser} |
||||||
|
* instance, producing a {@link PathPattern} object that can be used for fast matching |
||||||
|
* against paths. |
||||||
|
* |
||||||
|
* @param pathPattern the input path pattern, e.g. /foo/{bar} |
||||||
|
* @return a PathPattern for quickly matching paths against the specified path pattern |
||||||
|
*/ |
||||||
|
public PathPattern parsePattern(String pathPattern) { |
||||||
|
return this.pathPatternParser.parse(pathPattern); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add a {@link PathPattern} instance to this registry |
||||||
|
* @return true if this registry did not already contain the specified {@code PathPattern} |
||||||
|
*/ |
||||||
|
public boolean add(PathPattern pathPattern) { |
||||||
|
return this.patterns.add(pathPattern); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add all {@link PathPattern}s instance to this registry |
||||||
|
* @return true if this registry did not already contain at least one of the given {@code PathPattern}s |
||||||
|
*/ |
||||||
|
public boolean addAll(Collection<PathPattern> pathPatterns) { |
||||||
|
return this.patterns.addAll(pathPatterns); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Remove the given {@link PathPattern} from this registry |
||||||
|
* @return true if this registry contained the given {@code PathPattern} |
||||||
|
*/ |
||||||
|
public boolean remove(PathPattern pathPattern) { |
||||||
|
return this.patterns.remove(pathPattern); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Remove all {@link PathPattern}s from this registry |
||||||
|
*/ |
||||||
|
public void clear() { |
||||||
|
this.patterns.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse the given {@code rawPattern} and adds it to this registry, |
||||||
|
* as well as pattern variants, depending on the given options and |
||||||
|
* the nature of the input pattern. |
||||||
|
* <p>The following set of patterns will be added: |
||||||
|
* <ul> |
||||||
|
* <li>the pattern given as input, e.g. "/foo/{bar}" |
||||||
|
* <li>if {@link #useSuffixPatternMatch()}, variants for each given |
||||||
|
* {@link #getFileExtensions()}, such as "/foo/{bar}.pdf" or a variant for all extensions, |
||||||
|
* such as "/foo/{bar}.*" |
||||||
|
* <li>if {@link #useTrailingSlashMatch()}, a variant such as "/foo/{bar}/" |
||||||
|
* </ul> |
||||||
|
* @param rawPattern raw path pattern to parse and register |
||||||
|
* @return the list of {@link PathPattern} that were registered as a result |
||||||
|
*/ |
||||||
|
public List<PathPattern> register(String rawPattern) { |
||||||
|
List<PathPattern> newPatterns = new ArrayList<>(); |
||||||
|
PathPattern pattern = this.pathPatternParser.parse(rawPattern); |
||||||
|
newPatterns.add(pattern); |
||||||
|
if (StringUtils.hasLength(rawPattern) && !pattern.isCatchAll()) { |
||||||
|
if (this.useSuffixPatternMatch) { |
||||||
|
if (this.fileExtensions != null && !this.fileExtensions.isEmpty()) { |
||||||
|
for (String extension : this.fileExtensions) { |
||||||
|
newPatterns.add(this.pathPatternParser.parse(rawPattern + "." + extension)); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
newPatterns.add(this.pathPatternParser.parse(rawPattern + ".*")); |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.useTrailingSlashMatch && !rawPattern.endsWith("/")) { |
||||||
|
newPatterns.add(this.pathPatternParser.parse(rawPattern + "/")); |
||||||
|
} |
||||||
|
} |
||||||
|
this.patterns.addAll(newPatterns); |
||||||
|
return newPatterns; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Combine the patterns contained in the current registry |
||||||
|
* with the ones in the other, into a new {@code PathPatternRegistry} instance. |
||||||
|
* <p>Given the current registry contains "/prefix" and the other contains |
||||||
|
* "/foo" and "/bar/{item}", the combined result will be: a new registry |
||||||
|
* containing "/prefix/foo" and "/prefix/bar/{item}". |
||||||
|
* @param other other {@code PathPatternRegistry} to combine with |
||||||
|
* @return a new instance of {@code PathPatternRegistry} that combines both |
||||||
|
* @see PathPattern#combine(String) |
||||||
|
*/ |
||||||
|
public PathPatternRegistry combine(PathPatternRegistry other) { |
||||||
|
PathPatternRegistry result = new PathPatternRegistry(); |
||||||
|
result.setUseSuffixPatternMatch(this.useSuffixPatternMatch); |
||||||
|
result.setUseTrailingSlashMatch(this.useTrailingSlashMatch); |
||||||
|
result.setFileExtensions(this.fileExtensions); |
||||||
|
if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) { |
||||||
|
for (PathPattern pattern1 : this.patterns) { |
||||||
|
for (PathPattern pattern2 : other.patterns) { |
||||||
|
String combined = pattern1.combine(pattern2.getPatternString()); |
||||||
|
result.register(combined); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
else if (!this.patterns.isEmpty()) { |
||||||
|
result.patterns.addAll(this.patterns); |
||||||
|
} |
||||||
|
else if (!other.patterns.isEmpty()) { |
||||||
|
result.patterns.addAll(other.patterns); |
||||||
|
} |
||||||
|
else { |
||||||
|
result.register(""); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Given a full path, returns a {@link Comparator} suitable for sorting pattern |
||||||
|
* registries in order of explicitness for that path. |
||||||
|
* <p>The returned {@code Comparator} will |
||||||
|
* {@linkplain java.util.Collections#sort(java.util.List, java.util.Comparator) sort} |
||||||
|
* a list so that more specific patterns registries come before generic ones. |
||||||
|
* @param path the full path to use for comparison |
||||||
|
* @return a comparator capable of sorting patterns in order of explicitness |
||||||
|
*/ |
||||||
|
public Comparator<PathPatternRegistry> getComparator(final String path) { |
||||||
|
return (r1, r2) -> { |
||||||
|
PatternSetComparator comparator = new PatternSetComparator(path); |
||||||
|
Iterator<PathPattern> it1 = r1.patterns.stream() |
||||||
|
.sorted(comparator).collect(Collectors.toList()).iterator(); |
||||||
|
Iterator<PathPattern> it2 = r2.patterns.stream() |
||||||
|
.sorted(comparator).collect(Collectors.toList()).iterator(); |
||||||
|
while (it1.hasNext() && it2.hasNext()) { |
||||||
|
int result = comparator.compare(it1.next(), it2.next()); |
||||||
|
if (result != 0) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
if (it1.hasNext()) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
else if (it2.hasNext()) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
else { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private class PatternSetComparator implements Comparator<PathPattern> { |
||||||
|
|
||||||
|
private final String path; |
||||||
|
|
||||||
|
public PatternSetComparator(String path) { |
||||||
|
this.path = path; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int compare(PathPattern o1, PathPattern o2) { |
||||||
|
// Nulls get sorted to the end
|
||||||
|
if (o1 == null) { |
||||||
|
return (o2 == null ? 0 : +1); |
||||||
|
} |
||||||
|
else if (o2 == null) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
// exact matches get sorted first
|
||||||
|
if (o1.getPatternString().equals(path)) { |
||||||
|
return (o2.getPatternString().equals(path)) ? 0 : -1; |
||||||
|
} |
||||||
|
else if (o2.getPatternString().equals(path)) { |
||||||
|
return +1; |
||||||
|
} |
||||||
|
// compare pattern specificity
|
||||||
|
int result = o1.compareTo(o2); |
||||||
|
// if equal specificity, sort using pattern string
|
||||||
|
if (result == 0) { |
||||||
|
return o1.getPatternString().compareTo(o2.getPatternString()); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,160 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2017 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.web.util.patterns; |
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.hamcrest.Matchers; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.ExpectedException; |
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.hasSize; |
||||||
|
import static org.hamcrest.Matchers.is; |
||||||
|
import static org.junit.Assert.assertThat; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link PathPatternRegistry} |
||||||
|
* @author Brian Clozel |
||||||
|
*/ |
||||||
|
public class PathPatternRegistryTests { |
||||||
|
|
||||||
|
private PathPatternRegistry registry; |
||||||
|
|
||||||
|
@Rule |
||||||
|
public ExpectedException thrown = ExpectedException.none(); |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() throws Exception { |
||||||
|
this.registry = new PathPatternRegistry(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotRegisterInvalidPatterns() { |
||||||
|
this.thrown.expect(PatternParseException.class); |
||||||
|
this.thrown.expectMessage(Matchers.containsString("Expected close capture character after variable name")); |
||||||
|
this.registry.register("/{invalid"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldNotRegisterPatternVariants() { |
||||||
|
List<PathPattern> patterns = this.registry.register("/foo/{bar}"); |
||||||
|
assertPathPatternListContains(patterns, "/foo/{bar}"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRegisterTrailingSlashVariants() { |
||||||
|
this.registry.setUseTrailingSlashMatch(true); |
||||||
|
List<PathPattern> patterns = this.registry.register("/foo/{bar}"); |
||||||
|
assertPathPatternListContains(patterns, "/foo/{bar}", "/foo/{bar}/"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRegisterSuffixVariants() { |
||||||
|
this.registry.setUseSuffixPatternMatch(true); |
||||||
|
List<PathPattern> patterns = this.registry.register("/foo/{bar}"); |
||||||
|
assertPathPatternListContains(patterns, "/foo/{bar}", "/foo/{bar}.*"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRegisterExtensionsVariants() { |
||||||
|
Set<String> fileExtensions = new HashSet<>(); |
||||||
|
fileExtensions.add("json"); |
||||||
|
fileExtensions.add("xml"); |
||||||
|
this.registry.setUseSuffixPatternMatch(true); |
||||||
|
this.registry.setFileExtensions(fileExtensions); |
||||||
|
List<PathPattern> patterns = this.registry.register("/foo/{bar}"); |
||||||
|
assertPathPatternListContains(patterns, "/foo/{bar}", "/foo/{bar}.xml", "/foo/{bar}.json"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRegisterAllVariants() { |
||||||
|
Set<String> fileExtensions = new HashSet<>(); |
||||||
|
fileExtensions.add("json"); |
||||||
|
fileExtensions.add("xml"); |
||||||
|
this.registry.setUseSuffixPatternMatch(true); |
||||||
|
this.registry.setUseTrailingSlashMatch(true); |
||||||
|
this.registry.setFileExtensions(fileExtensions); |
||||||
|
List<PathPattern> patterns = this.registry.register("/foo/{bar}"); |
||||||
|
assertPathPatternListContains(patterns, "/foo/{bar}", |
||||||
|
"/foo/{bar}.xml", "/foo/{bar}.json", "/foo/{bar}/"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void combineEmptyRegistries() { |
||||||
|
PathPatternRegistry result = this.registry.combine(new PathPatternRegistry()); |
||||||
|
assertPathPatternListContains(result.getPatterns(), ""); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void combineWithEmptyRegistry() { |
||||||
|
this.registry.register("/foo"); |
||||||
|
PathPatternRegistry result = this.registry.combine(new PathPatternRegistry()); |
||||||
|
assertPathPatternListContains(result.getPatterns(), "/foo"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void combineRegistries() { |
||||||
|
this.registry.register("/foo"); |
||||||
|
PathPatternRegistry other = new PathPatternRegistry(); |
||||||
|
other.register("/bar"); |
||||||
|
other.register("/baz"); |
||||||
|
PathPatternRegistry result = this.registry.combine(other); |
||||||
|
assertPathPatternListContains(result.getPatterns(), "/foo/bar", "/foo/baz"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void registerPatternsWithSameSpecificity() { |
||||||
|
PathPattern fooOne = this.registry.parsePattern("/fo?"); |
||||||
|
PathPattern fooTwo = this.registry.parsePattern("/f?o"); |
||||||
|
assertThat(fooOne.compareTo(fooTwo), is(0)); |
||||||
|
|
||||||
|
this.registry.add(fooOne); |
||||||
|
this.registry.add(fooTwo); |
||||||
|
Set<PathPattern> matches = this.registry.findMatches("/foo"); |
||||||
|
assertPathPatternListContains(matches, "/f?o", "/fo?"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void findNoMatch() { |
||||||
|
this.registry.register("/foo/{bar}"); |
||||||
|
assertThat(this.registry.findMatches("/other"), hasSize(0)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void orderMatchesBySpecificity() { |
||||||
|
this.registry.register("/foo/{*baz}"); |
||||||
|
this.registry.register("/foo/bar/baz"); |
||||||
|
this.registry.register("/foo/bar/{baz}"); |
||||||
|
Set<PathPattern> matches = this.registry.findMatches("/foo/bar/baz"); |
||||||
|
assertPathPatternListContains(matches, "/foo/bar/baz", "/foo/bar/{baz}", |
||||||
|
"/foo/{*baz}"); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private void assertPathPatternListContains(Collection<PathPattern> parsedPatterns, String... pathPatterns) { |
||||||
|
List<String> patternList = parsedPatterns. |
||||||
|
stream().map(pattern -> pattern.getPatternString()).collect(Collectors.toList()); |
||||||
|
assertThat(patternList, Matchers.contains(pathPatterns)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue