Revert Path Pattern changes in WebFlux

Issue: SPR-14544
This commit is contained in:
Brian Clozel
2017-02-10 18:08:43 +01:00
parent 5b98a54c9b
commit fa4202f1bd
22 changed files with 790 additions and 955 deletions

View File

@@ -18,16 +18,14 @@ package org.springframework.web.cors.reactive;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
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
@@ -37,18 +35,27 @@ import org.springframework.web.util.patterns.PathPatternRegistry;
* 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 PathPatternRegistry patternRegistry = new PathPatternRegistry();
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
private final Map<PathPattern, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
private PathMatcher pathMatcher = new AntPathMatcher();
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.
@@ -73,20 +80,16 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
* Set CORS configuration based on URL patterns.
*/
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
this.patternRegistry.clear();
this.corsConfigurations.clear();
if (corsConfigurations != null) {
corsConfigurations.forEach((pattern, config) -> {
List<PathPattern> registered = this.patternRegistry.register(pattern);
registered.forEach(p -> this.corsConfigurations.put(p, config));
});
this.corsConfigurations.putAll(corsConfigurations);
}
}
/**
* Get the CORS configuration.
*/
public Map<PathPattern, CorsConfiguration> getCorsConfigurations() {
public Map<String, CorsConfiguration> getCorsConfigurations() {
return Collections.unmodifiableMap(this.corsConfigurations);
}
@@ -94,18 +97,17 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
* Register a {@link CorsConfiguration} for the specified path pattern.
*/
public void registerCorsConfiguration(String path, CorsConfiguration config) {
this.patternRegistry
.register(path)
.forEach(pattern -> this.corsConfigurations.put(pattern, config));
this.corsConfigurations.put(path, config);
}
@Override
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
String lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
SortedSet<PathPattern> matches = this.patternRegistry.findMatches(lookupPath);
if(!matches.isEmpty()) {
return this.corsConfigurations.get(matches.first());
for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
return entry.getValue();
}
}
return null;
}

View File

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