22 changed files with 788 additions and 953 deletions
@ -1,336 +0,0 @@
@@ -1,336 +0,0 @@
|
||||
/* |
||||
* 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.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<>(); |
||||
} |
||||
|
||||
public PathPatternRegistry(Set<PathPattern> patterns) { |
||||
this(); |
||||
this.patterns.addAll(patterns); |
||||
} |
||||
|
||||
/** |
||||
* 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) { |
||||
Set<String> fixedFileExtensions = (fileExtensions != null) ? fileExtensions.stream() |
||||
.map(ext -> (ext.charAt(0) != '.') ? "." + ext : ext) |
||||
.collect(Collectors.toSet()) : Collections.emptySet(); |
||||
this.fileExtensions = fixedFileExtensions; |
||||
} |
||||
|
||||
/** |
||||
* Return a (read-only) set of all patterns for matching (including generated pattern variants). |
||||
*/ |
||||
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); |
||||
} |
||||
|
||||
/** |
||||
* 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 = generatePathPatterns(rawPattern); |
||||
this.patterns.addAll(newPatterns); |
||||
return newPatterns; |
||||
} |
||||
|
||||
private String prependLeadingSlash(String pattern) { |
||||
if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { |
||||
return "/" + pattern; |
||||
} |
||||
else { |
||||
return pattern; |
||||
} |
||||
} |
||||
|
||||
private List<PathPattern> generatePathPatterns(String rawPattern) { |
||||
String fixedPattern = prependLeadingSlash(rawPattern); |
||||
List<PathPattern> patterns = new ArrayList<>(); |
||||
PathPattern pattern = this.pathPatternParser.parse(fixedPattern); |
||||
patterns.add(pattern); |
||||
if (StringUtils.hasLength(fixedPattern) && !pattern.isCatchAll()) { |
||||
if (this.useSuffixPatternMatch) { |
||||
if (this.fileExtensions != null && !this.fileExtensions.isEmpty()) { |
||||
for (String extension : this.fileExtensions) { |
||||
patterns.add(this.pathPatternParser.parse(fixedPattern + extension)); |
||||
} |
||||
} |
||||
else { |
||||
patterns.add(this.pathPatternParser.parse(fixedPattern + ".*")); |
||||
} |
||||
} |
||||
if (this.useTrailingSlashMatch && !fixedPattern.endsWith("/")) { |
||||
patterns.add(this.pathPatternParser.parse(fixedPattern + "/")); |
||||
} |
||||
} |
||||
return patterns; |
||||
} |
||||
|
||||
/** |
||||
* Parse the given {@code rawPattern} and removes it to this registry, |
||||
* as well as pattern variants, depending on the given options and |
||||
* the nature of the input pattern. |
||||
* |
||||
* @param rawPattern raw path pattern to parse and unregister |
||||
* @return the list of {@link PathPattern} that were unregistered as a result |
||||
*/ |
||||
public List<PathPattern> unregister(String rawPattern) { |
||||
List<PathPattern> unregisteredPatterns = generatePathPatterns(rawPattern); |
||||
this.patterns.removeAll(unregisteredPatterns); |
||||
return unregisteredPatterns; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,177 +0,0 @@
@@ -1,177 +0,0 @@
|
||||
/* |
||||
* 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.contains; |
||||
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 shouldFixFileExtensions() { |
||||
Set<String> fileExtensions = new HashSet<>(); |
||||
fileExtensions.add("json"); |
||||
fileExtensions.add("xml"); |
||||
this.registry.setFileExtensions(fileExtensions); |
||||
assertThat(this.registry.getFileExtensions(), contains(".json", ".xml")); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldPrependPatternsWithSlash() { |
||||
this.registry.register("foo/bar"); |
||||
assertThat(getPatternList(this.registry.getPatterns()), Matchers.containsInAnyOrder("/foo/bar")); |
||||
} |
||||
|
||||
@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}"); |
||||
assertThat(getPatternList(patterns), Matchers.containsInAnyOrder("/foo/{bar}")); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldRegisterTrailingSlashVariants() { |
||||
this.registry.setUseTrailingSlashMatch(true); |
||||
List<PathPattern> patterns = this.registry.register("/foo/{bar}"); |
||||
assertThat(getPatternList(patterns), Matchers.containsInAnyOrder("/foo/{bar}", "/foo/{bar}/")); |
||||
} |
||||
|
||||
@Test |
||||
public void shouldRegisterSuffixVariants() { |
||||
this.registry.setUseSuffixPatternMatch(true); |
||||
List<PathPattern> patterns = this.registry.register("/foo/{bar}"); |
||||
assertThat(getPatternList(patterns), Matchers.containsInAnyOrder("/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}"); |
||||
assertThat(getPatternList(patterns), |
||||
Matchers.containsInAnyOrder("/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}"); |
||||
assertThat(getPatternList(patterns), Matchers.containsInAnyOrder("/foo/{bar}", |
||||
"/foo/{bar}.xml", "/foo/{bar}.json", "/foo/{bar}/")); |
||||
} |
||||
|
||||
@Test |
||||
public void combineEmptyRegistries() { |
||||
PathPatternRegistry result = this.registry.combine(new PathPatternRegistry()); |
||||
assertThat(getPatternList(result.getPatterns()), Matchers.containsInAnyOrder("")); |
||||
} |
||||
|
||||
@Test |
||||
public void combineWithEmptyRegistry() { |
||||
this.registry.register("/foo"); |
||||
PathPatternRegistry result = this.registry.combine(new PathPatternRegistry()); |
||||
assertThat(getPatternList(result.getPatterns()), Matchers.containsInAnyOrder("/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); |
||||
assertThat(getPatternList(result.getPatterns()), Matchers.containsInAnyOrder("/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.register("/fo?"); |
||||
this.registry.register("/f?o"); |
||||
Set<PathPattern> matches = this.registry.findMatches("/foo"); |
||||
assertThat(getPatternList(matches), Matchers.contains("/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"); |
||||
assertThat(getPatternList(matches), Matchers.contains("/foo/bar/baz", "/foo/bar/{baz}", |
||||
"/foo/{*baz}")); |
||||
} |
||||
|
||||
|
||||
private List<String> getPatternList(Collection<PathPattern> parsedPatterns) { |
||||
return parsedPatterns.stream().map(pattern -> pattern.getPatternString()).collect(Collectors.toList()); |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue