Add PathPatternRegistry

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-14544
This commit is contained in:
Brian Clozel
2017-02-08 14:03:22 +01:00
parent a4da313a0a
commit 18c04815a7
24 changed files with 869 additions and 581 deletions

View File

@@ -19,13 +19,14 @@ package org.springframework.web.cors.reactive;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedSet;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.util.patterns.PathPattern;
import org.springframework.web.util.patterns.PathPatternRegistry;
/**
* Provide a per reactive request {@link CorsConfiguration} instance based on a
@@ -35,27 +36,18 @@ import org.springframework.web.server.support.HttpRequestPathHelper;
* as well as Ant-style path patterns (such as {@code "/admin/**"}).
*
* @author Sebastien Deleuze
* @author Brian Clozel
* @since 5.0
*/
public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
private final PathPatternRegistry patternRegistry = new PathPatternRegistry();
private PathMatcher pathMatcher = new AntPathMatcher();
private final Map<PathPattern, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
/**
* Set the PathMatcher implementation to use for matching URL paths
* against registered URL patterns. Default is AntPathMatcher.
* @see AntPathMatcher
*/
public void setPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher must not be null");
this.pathMatcher = pathMatcher;
}
/**
* Set if context path and request URI should be URL-decoded. Both are returned
* <i>undecoded</i> by the Servlet API, in contrast to the servlet path.
@@ -79,9 +71,11 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
/**
* Set CORS configuration based on URL patterns.
*/
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
public void setCorsConfigurations(Map<PathPattern, CorsConfiguration> corsConfigurations) {
this.patternRegistry.clear();
this.corsConfigurations.clear();
if (corsConfigurations != null) {
this.patternRegistry.addAll(corsConfigurations.keySet());
this.corsConfigurations.putAll(corsConfigurations);
}
}
@@ -89,7 +83,7 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
/**
* Get the CORS configuration.
*/
public Map<String, CorsConfiguration> getCorsConfigurations() {
public Map<PathPattern, CorsConfiguration> getCorsConfigurations() {
return Collections.unmodifiableMap(this.corsConfigurations);
}
@@ -97,17 +91,18 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
* Register a {@link CorsConfiguration} for the specified path pattern.
*/
public void registerCorsConfiguration(String path, CorsConfiguration config) {
this.corsConfigurations.put(path, config);
this.patternRegistry
.register(path)
.forEach(pattern -> this.corsConfigurations.put(pattern, config));
}
@Override
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
String lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
return entry.getValue();
}
SortedSet<PathPattern> matches = this.patternRegistry.findMatches(lookupPath);
if(!matches.isEmpty()) {
return this.corsConfigurations.get(matches.first());
}
return null;
}

View File

@@ -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;
}
}
}