Browse Source

Remove PathPatternComparator

Direct comparison of a pattern (as a String) to the path does not make
much sense now that we deal with URL encoding through PathContainer
which exposes (safely) decoded path segments.

Removing the PathPatternComparator also means we can keep patterns
pre-sorted instead of sorting them all the time. That probably offsets
any benefits from comparing to the lookup path for direct matches and
patterns are still sorted according to specificity.
pull/1735/head
Rossen Stoyanchev 9 years ago
parent
commit
a5e54788cc
  1. 72
      spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternComparator.java
  2. 5
      spring-webflux/src/main/java/org/springframework/web/reactive/handler/PathPatternRegistry.java
  3. 82
      spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java
  4. 7
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java
  5. 8
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java
  6. 12
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java
  7. 10
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
  8. 5
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java

72
spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternComparator.java

@ -1,72 +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.pattern;
import java.util.Comparator;
import org.springframework.lang.Nullable;
/**
* {@link PathPattern} comparator that takes account of a specified
* path and sorts anything that exactly matches it to be first.
*
* <p>Patterns that have the same specificity are then compared
* using their String representation, in order to avoid
* considering them as "duplicates" when sorting them
* in {@link java.util.Set} collections.
*
* @author Brian Clozel
* @since 5.0
*/
public class PathPatternComparator implements Comparator<PathPattern> {
private final String path;
public PathPatternComparator(String path) {
this.path = path;
}
@Override
public int compare(@Nullable PathPattern o1, @Nullable 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;
}
}

5
spring-webflux/src/main/java/org/springframework/web/reactive/handler/PathPatternRegistry.java

@ -28,7 +28,6 @@ import java.util.stream.Collectors;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternComparator;
import org.springframework.web.util.pattern.PathPatternParser; import org.springframework.web.util.pattern.PathPatternParser;
/** /**
@ -101,10 +100,10 @@ public class PathPatternRegistry<T> {
* @param lookupPath the URL lookup path to be matched against * @param lookupPath the URL lookup path to be matched against
*/ */
public Optional<PathMatchResult<T>> findFirstMatch(String lookupPath) { public Optional<PathMatchResult<T>> findFirstMatch(String lookupPath) {
PathPatternComparator comparator = new PathPatternComparator(lookupPath);
return this.patternsMap.entrySet().stream() return this.patternsMap.entrySet().stream()
.filter(entry -> entry.getKey().matches(lookupPath)) .filter(entry -> entry.getKey().matches(lookupPath))
.reduce((e1, e2) -> comparator.compare(e1.getKey(), e2.getKey()) < 0 ? e1 : e2) .sorted(Comparator.comparing(Map.Entry::getKey))
.findFirst()
.map(entry -> new PathMatchResult<>(entry.getKey(), entry.getValue())); .map(entry -> new PathMatchResult<>(entry.getKey(), entry.getValue()));
} }

82
spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java

@ -26,12 +26,13 @@ import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.http.server.reactive.PathContainer;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternComparator;
import org.springframework.web.util.pattern.PathPatternParser; import org.springframework.web.util.pattern.PathPatternParser;
/** /**
@ -44,17 +45,18 @@ import org.springframework.web.util.pattern.PathPatternParser;
*/ */
public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> { public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
private final List<PathPattern> patterns; private final Set<PathPattern> patterns;
private final PathPatternParser parser; private final PathPatternParser parser;
/** /**
* Creates a new instance with the given URL patterns. * Creates a new instance with the given URL patterns.
* Each pattern is prepended with "/" if not already. * Each pattern is prepended with "/" if not already.
* @param patterns 0 or more URL patterns; if 0 the condition will match to every request. * @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
*/ */
public PatternsRequestCondition(String... patterns) { public PatternsRequestCondition(String... patterns) {
this(asList(patterns), null); this(patterns, null);
} }
/** /**
@ -64,44 +66,39 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
* @param patternParser for parsing string patterns * @param patternParser for parsing string patterns
*/ */
public PatternsRequestCondition(String[] patterns, PathPatternParser patternParser) { public PatternsRequestCondition(String[] patterns, PathPatternParser patternParser) {
this(Arrays.asList(patterns), patternParser);
this(asList(patterns), patternParser);
} }
/** /**
* Private constructor accepting a collection of raw patterns. * Private constructor accepting a collection of raw patterns.
*/ */
private PatternsRequestCondition(Collection<String> patterns, PathPatternParser patternParser) { private PatternsRequestCondition(Collection<String> patterns, PathPatternParser parser) {
this.parser = patternParser != null ? patternParser : new PathPatternParser(); this.parser = (parser != null ? parser : new PathPatternParser());
this.patterns = new ArrayList<>(); this.patterns = toSortedSet(patterns.stream().map(pattern -> parse(pattern, this.parser)));
patterns.forEach(pattern -> { }
if (StringUtils.hasText(pattern) && !pattern.startsWith("/")) {
pattern = "/" + pattern; private static PathPattern parse(String pattern, PathPatternParser parser) {
} if (StringUtils.hasText(pattern) && !pattern.startsWith("/")) {
this.patterns.add(this.parser.parse(pattern)); pattern = "/" + pattern;
}); }
return parser.parse(pattern);
}
private static Set<PathPattern> toSortedSet(Stream<PathPattern> stream) {
Set<PathPattern> result = stream.sorted().collect(Collectors.toCollection(TreeSet::new));
return Collections.unmodifiableSet(result);
} }
/** /**
* Private constructor accepting a list of path patterns. * Private constructor accepting a list of path patterns.
*/ */
private PatternsRequestCondition(List<PathPattern> patterns, PathPatternParser patternParser) { private PatternsRequestCondition(List<PathPattern> patterns, PathPatternParser patternParser) {
this.patterns = patterns; this.patterns = toSortedSet(patterns.stream());
this.parser = patternParser; this.parser = patternParser;
} }
private static List<String> asList(String... patterns) {
return (patterns != null ? Arrays.asList(patterns) : Collections.emptyList());
}
public Set<PathPattern> getPatterns() { public Set<PathPattern> getPatterns() {
return new TreeSet<>(this.patterns); return this.patterns;
}
public Set<String> getPatternStrings() {
return this.patterns.stream()
.map(PathPattern::toString).collect(Collectors.toSet());
} }
@Override @Override
@ -143,14 +140,12 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
else { else {
combined.add(this.parser.parse("")); combined.add(this.parser.parse(""));
} }
return new PatternsRequestCondition(combined, this.parser); return new PatternsRequestCondition(combined, this.parser);
} }
/** /**
* Checks if any of the patterns match the given request and returns an instance * Checks if any of the patterns match the given request and returns an instance
* that is guaranteed to contain matching patterns, sorted with a * that is guaranteed to contain matching patterns, sorted.
* {@link PathPatternComparator}.
* @param exchange the current exchange * @param exchange the current exchange
* @return the same instance if the condition contains no patterns; * @return the same instance if the condition contains no patterns;
* or a new condition with sorted matching patterns; * or a new condition with sorted matching patterns;
@ -162,11 +157,9 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
if (this.patterns.isEmpty()) { if (this.patterns.isEmpty()) {
return this; return this;
} }
SortedSet<PathPattern> matches = getMatchingPatterns(exchange);
String lookupPath = exchange.getRequest().getPath().pathWithinApplication().value(); return matches.isEmpty() ? null :
SortedSet<PathPattern> matches = getMatchingPatterns(lookupPath); new PatternsRequestCondition(new ArrayList<PathPattern>(matches), this.parser);
return matches.isEmpty() ? null : new PatternsRequestCondition(
new ArrayList<PathPattern>(matches), this.parser);
} }
/** /**
@ -175,20 +168,19 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
* {@link #getMatchingCondition(ServerWebExchange)}. * {@link #getMatchingCondition(ServerWebExchange)}.
* This method is provided as an alternative to be used if no request is available * This method is provided as an alternative to be used if no request is available
* (e.g. introspection, tooling, etc). * (e.g. introspection, tooling, etc).
* @param lookupPath the lookup path to match to existing patterns * @param exchange the current exchange
* @return a sorted set of matching patterns sorted with the closest match first * @return a sorted set of matching patterns sorted with the closest match first
*/ */
public SortedSet<PathPattern> getMatchingPatterns(String lookupPath) { private SortedSet<PathPattern> getMatchingPatterns(ServerWebExchange exchange) {
PathContainer lookupPath = exchange.getRequest().getPath().pathWithinApplication();
return patterns.stream() return patterns.stream()
.filter(pattern -> pattern.matches(lookupPath)) .filter(pattern -> pattern.matches(lookupPath))
.collect(Collectors.toCollection(() -> .collect(Collectors.toCollection(TreeSet::new));
new TreeSet<>(new PathPatternComparator(lookupPath))));
} }
/** /**
* Compare the two conditions based on the URL patterns they contain. * Compare the two conditions based on the URL patterns they contain.
* Patterns are compared one at a time, from top to bottom via * Patterns are compared one at a time, from top to bottom. If all compared
* {@link PathPatternComparator}. If all compared
* patterns match equally, but one instance has more patterns, it is * patterns match equally, but one instance has more patterns, it is
* considered a closer match. * considered a closer match.
* <p>It is assumed that both instances have been obtained via * <p>It is assumed that both instances have been obtained via
@ -198,14 +190,10 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
*/ */
@Override @Override
public int compareTo(PatternsRequestCondition other, ServerWebExchange exchange) { public int compareTo(PatternsRequestCondition other, ServerWebExchange exchange) {
String lookupPath = exchange.getRequest().getPath().pathWithinApplication().value(); Iterator<PathPattern> iterator = this.patterns.iterator();
PathPatternComparator comparator = new PathPatternComparator(lookupPath); Iterator<PathPattern> iteratorOther = other.getPatterns().iterator();
Iterator<PathPattern> iterator = this.patterns.stream()
.sorted(comparator).collect(Collectors.toList()).iterator();
Iterator<PathPattern> iteratorOther = other.getPatterns().stream()
.sorted(comparator).collect(Collectors.toList()).iterator();
while (iterator.hasNext() && iteratorOther.hasNext()) { while (iterator.hasNext() && iteratorOther.hasNext()) {
int result = comparator.compare(iterator.next(), iteratorOther.next()); int result = iterator.next().compareTo(iteratorOther.next());
if (result != 0) { if (result != 0) {
return result; return result;
} }

7
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java

@ -292,7 +292,7 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
* @param lookupPath the lookup path within the current mapping * @param lookupPath the lookup path within the current mapping
* @param exchange the current exchange * @param exchange the current exchange
* @return the best-matching handler method, or {@code null} if no match * @return the best-matching handler method, or {@code null} if no match
* @see #handleMatch(Object, String, ServerWebExchange) * @see #handleMatch(Object, HandlerMethod, String, ServerWebExchange)
* @see #handleNoMatch(Set, String, ServerWebExchange) * @see #handleNoMatch(Set, String, ServerWebExchange)
*/ */
@Nullable @Nullable
@ -400,11 +400,6 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
@Nullable @Nullable
protected abstract T getMappingForMethod(Method method, Class<?> handlerType); protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
/**
* Extract and return the URL paths contained in a mapping.
*/
protected abstract Set<String> getMappingPathPatterns(T mapping);
/** /**
* Check if a mapping matches the current request and return a (potentially * Check if a mapping matches the current request and return a (potentially
* new) mapping with conditions relevant to the current request. * new) mapping with conditions relevant to the current request.

8
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java

@ -66,14 +66,6 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
} }
/**
* Get the URL path patterns associated with this {@link RequestMappingInfo}.
*/
@Override
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
return info.getPatternsCondition().getPatternStrings();
}
/** /**
* Check if the given RequestMappingInfo matches the current request and * Check if the given RequestMappingInfo matches the current request and
* return a (potentially new) instance with conditions that match the * return a (potentially new) instance with conditions that match the

12
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java

@ -17,9 +17,7 @@
package org.springframework.web.reactive.result.method; package org.springframework.web.reactive.result.method;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Set;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
@ -35,7 +33,10 @@ import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.ParsingPathMatcher; import org.springframework.web.util.pattern.ParsingPathMatcher;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
/** /**
* Unit tests for {@link AbstractHandlerMethodMapping}. * Unit tests for {@link AbstractHandlerMethodMapping}.
@ -149,11 +150,6 @@ public class HandlerMethodMappingTests {
return methodName.startsWith("handler") ? methodName : null; return methodName.startsWith("handler") ? methodName : null;
} }
@Override
protected Set<String> getMappingPathPatterns(String key) {
return (this.pathMatcher.isPattern(key) ? Collections.emptySet() : Collections.singleton(key));
}
@Override @Override
protected String getMatchingMapping(String pattern, ServerWebExchange exchange) { protected String getMatchingMapping(String pattern, ServerWebExchange exchange) {
String lookupPath = exchange.getRequest().getURI().getPath(); String lookupPath = exchange.getRequest().getURI().getPath();

10
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java

@ -21,7 +21,6 @@ import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -94,15 +93,6 @@ public class RequestMappingInfoHandlerMappingTests {
} }
@Test
public void getMappingPathPatterns() throws Exception {
String[] patterns = {"/foo/*", "/foo", "/bar/*", "/bar"};
RequestMappingInfo info = paths(patterns).build();
Set<String> actual = this.handlerMapping.getMappingPathPatterns(info);
assertEquals(new HashSet<>(Arrays.asList(patterns)), actual);
}
@Test @Test
public void getHandlerDirectMatch() throws Exception { public void getHandlerDirectMatch() throws Exception {
Method expected = on(TestController.class).annot(getMapping("/foo").params()).resolveMethod(); Method expected = on(TestController.class).annot(getMapping("/foo").params()).resolveMethod();

5
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java

@ -39,6 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.util.pattern.PathPattern;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -133,9 +134,9 @@ public class RequestMappingHandlerMappingTests {
assertNotNull(info); assertNotNull(info);
Set<String> paths = info.getPatternsCondition().getPatternStrings(); Set<PathPattern> paths = info.getPatternsCondition().getPatterns();
assertEquals(1, paths.size()); assertEquals(1, paths.size());
assertEquals(path, paths.iterator().next()); assertEquals(path, paths.iterator().next().getPatternString());
Set<RequestMethod> methods = info.getMethodsCondition().getMethods(); Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
assertEquals(1, methods.size()); assertEquals(1, methods.size());

Loading…
Cancel
Save