Skip to content

Commit

Permalink
Add RouteMatcher
Browse files Browse the repository at this point in the history
Closes gh-22642
  • Loading branch information
rstoyanchev committed May 15, 2019
1 parent afc0ae3 commit 97c2de9
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 83 deletions.
100 changes: 100 additions & 0 deletions spring-core/src/main/java/org/springframework/util/RouteMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2002-2019 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
*
* https://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.util;

import java.util.Comparator;
import java.util.Map;

import org.springframework.lang.Nullable;

/**
* Contract for matching routes to patterns.
*
* <p>Equivalent to {@link PathMatcher}, but enables use of parsed
* representations of routes and patterns for efficiency reasons in scenarios
* where routes from incoming messages are continuously matched against a
* large number of message handler patterns.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public interface RouteMatcher {

/**
* Return a parsed representation of the given route.
* @param routeValue the route to parse
* @return the parsed representation of the route
*/
Route parseRoute(String routeValue);


/**
* Whether the given {@code route} contains pattern syntax which requires
* the {@link #match(String, Route)} method, or if it is a regular String
* that could be compared directly to others.
* @param route the route to check
* @return {@code true} if the given {@code route} represents a pattern
*/
boolean isPattern(String route);

/**
* Combines two patterns into a single pattern.
* @param pattern1 the first pattern
* @param pattern2 the second pattern
* @return the combination of the two patterns
* @throws IllegalArgumentException when the two patterns cannot be combined
*/
String combine(String pattern1, String pattern2);

/**
* Match the given route against the given pattern.
* @param pattern the pattern to try to match
* @param route the route to test against
* @return {@code true} if there is a match, {@code false} otherwise
*/
boolean match(String pattern, Route route);

/**
* Match the pattern to the route and extract template variables.
* @param pattern the pattern, possibly containing templates variables
* @param route the route to extract template variables from
* @return a map with template variables and values
*/
@Nullable
Map<String, String> matchAndExtract(String pattern, Route route);

/**
* Given a route, return a {@link Comparator} suitable for sorting patterns
* in order of explicitness for that route, so that more specific patterns
* come before more generic ones.
* @param route the full path to use for comparison
* @return a comparator capable of sorting patterns in order of explicitness
*/
Comparator<String> getPatternComparator(Route route);


/**
* A parsed representation of a route.
*/
interface Route {

/**
* The original route value.
*/
String value();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2002-2019 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
*
* https://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.util;

import java.util.Comparator;
import java.util.Map;

import org.springframework.lang.Nullable;

/**
* {@code RouteMatcher} that delegates to a {@link PathMatcher}.
*
* <p><strong>Note:</strong> This implementation is not efficient since
* {@code PathMatcher} treats paths and patterns as Strings. For more optimized
* performance use the {@code PathPatternRouteMatcher} from {@code spring-web}
* which enables use of parsed routes and patterns.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class SimpleRouteMatcher implements RouteMatcher {

private final PathMatcher pathMatcher;


public SimpleRouteMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher is required");
this.pathMatcher = pathMatcher;
}


public PathMatcher getPathMatcher() {
return this.pathMatcher;
}


@Override
public Route parseRoute(String route) {
return new DefaultRoute(route);
}

@Override
public boolean isPattern(String route) {
return this.pathMatcher.isPattern(route);
}

@Override
public String combine(String pattern1, String pattern2) {
return this.pathMatcher.combine(pattern1, pattern2);
}

@Override
public boolean match(String pattern, Route route) {
return this.pathMatcher.match(pattern, route.value());
}

@Override
@Nullable
public Map<String, String> matchAndExtract(String pattern, Route route) {
if (!match(pattern, route)) {
return null;
}
return this.pathMatcher.extractUriTemplateVariables(pattern, route.value());
}

@Override
public Comparator<String> getPatternComparator(Route route) {
return this.pathMatcher.getPatternComparator(route.value());
}


private static class DefaultRoute implements Route {

private final String path;


DefaultRoute(String path) {
this.path = path;
}


@Override
public String value() {
return this.path;
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand All @@ -17,7 +17,6 @@
package org.springframework.messaging.handler;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
Expand All @@ -29,12 +28,15 @@
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.RouteMatcher;
import org.springframework.util.SimpleRouteMatcher;
import org.springframework.util.StringUtils;

/**
* A {@link MessageCondition} for matching the destination of a Message
* against one or more destination patterns using a {@link PathMatcher}.
* {@link MessageCondition} to match the destination header of a Message
* against one or more patterns through a {@link RouteMatcher}.
*
* @author Rossen Stoyanchev
* @since 4.0
Expand All @@ -50,36 +52,41 @@ public class DestinationPatternsMessageCondition

private final Set<String> patterns;

private final PathMatcher pathMatcher;
private final RouteMatcher routeMatcher;


/**
* Creates a new instance with the given destination patterns.
* Each pattern that is not empty and does not start with "/" is prepended with "/".
* @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
* Constructor with patterns only. Creates and uses an instance of
* {@link AntPathMatcher} with default settings.
* <p>Non-empty patterns that don't start with "/" are prepended with "/".
* @param patterns the URL patterns to match to, or if 0 then always match
*/
public DestinationPatternsMessageCondition(String... patterns) {
this(patterns, null);
this(patterns, (PathMatcher) null);
}

/**
* Alternative constructor accepting a custom PathMatcher.
* @param patterns the URL patterns to use; if 0, the condition will match to every request.
* @param pathMatcher the PathMatcher to use
* Constructor with patterns and a {@code PathMatcher} instance.
* @param patterns the URL patterns to match to, or if 0 then always match
* @param matcher the {@code PathMatcher} to use
*/
public DestinationPatternsMessageCondition(String[] patterns, @Nullable PathMatcher pathMatcher) {
this(Arrays.asList(patterns), pathMatcher);
public DestinationPatternsMessageCondition(String[] patterns, @Nullable PathMatcher matcher) {
this(patterns, new SimpleRouteMatcher(matcher != null ? matcher : new AntPathMatcher()));
}

private DestinationPatternsMessageCondition(Collection<String> patterns, @Nullable PathMatcher pathMatcher) {
this.pathMatcher = (pathMatcher != null ? pathMatcher : new AntPathMatcher());
this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns, this.pathMatcher));
/**
* Constructor with patterns and a {@code RouteMatcher} instance.
* @param patterns the URL patterns to match to, or if 0 then always match
* @param routeMatcher the {@code RouteMatcher} to use
* @since 5.2
*/
public DestinationPatternsMessageCondition(String[] patterns, RouteMatcher routeMatcher) {
this(Collections.unmodifiableSet(prependLeadingSlash(patterns, routeMatcher)), routeMatcher);
}


private static Set<String> prependLeadingSlash(Collection<String> patterns, PathMatcher pathMatcher) {
boolean slashSeparator = pathMatcher.combine("a", "a").equals("a/a");
Set<String> result = new LinkedHashSet<>(patterns.size());
private static Set<String> prependLeadingSlash(String[] patterns, RouteMatcher routeMatcher) {
boolean slashSeparator = routeMatcher.combine("a", "a").equals("a/a");
Set<String> result = new LinkedHashSet<>(patterns.length);
for (String pattern : patterns) {
if (slashSeparator && StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
pattern = "/" + pattern;
Expand All @@ -89,6 +96,12 @@ private static Set<String> prependLeadingSlash(Collection<String> patterns, Path
return result;
}

private DestinationPatternsMessageCondition(Set<String> patterns, RouteMatcher routeMatcher) {
this.patterns = patterns;
this.routeMatcher = routeMatcher;
}



public Set<String> getPatterns() {
return this.patterns;
Expand Down Expand Up @@ -121,7 +134,7 @@ public DestinationPatternsMessageCondition combine(DestinationPatternsMessageCon
if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) {
for (String pattern1 : this.patterns) {
for (String pattern2 : other.patterns) {
result.add(this.pathMatcher.combine(pattern1, pattern2));
result.add(this.routeMatcher.combine(pattern1, pattern2));
}
}
}
Expand All @@ -134,7 +147,7 @@ else if (!other.patterns.isEmpty()) {
else {
result.add("");
}
return new DestinationPatternsMessageCondition(result, this.pathMatcher);
return new DestinationPatternsMessageCondition(result, this.routeMatcher);
}

/**
Expand All @@ -149,26 +162,41 @@ else if (!other.patterns.isEmpty()) {
@Override
@Nullable
public DestinationPatternsMessageCondition getMatchingCondition(Message<?> message) {
String destination = (String) message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
Object destination = message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
if (destination == null) {
return null;
}
if (this.patterns.isEmpty()) {
return this;
}

List<String> matches = new ArrayList<>();
List<String> matches = null;
for (String pattern : this.patterns) {
if (pattern.equals(destination) || this.pathMatcher.match(pattern, destination)) {
if (pattern.equals(destination) || matchPattern(pattern, destination)) {
if (matches == null) {
matches = new ArrayList<>();
}
matches.add(pattern);
}
}
if (matches.isEmpty()) {
if (CollectionUtils.isEmpty(matches)) {
return null;
}

matches.sort(this.pathMatcher.getPatternComparator(destination));
return new DestinationPatternsMessageCondition(matches, this.pathMatcher);
matches.sort(getPatternComparator(destination));
return new DestinationPatternsMessageCondition(new LinkedHashSet<>(matches), this.routeMatcher);
}

private boolean matchPattern(String pattern, Object destination) {
return destination instanceof RouteMatcher.Route ?
this.routeMatcher.match(pattern, (RouteMatcher.Route) destination) :
((SimpleRouteMatcher) this.routeMatcher).getPathMatcher().match(pattern, (String) destination);
}

private Comparator<String> getPatternComparator(Object destination) {
return destination instanceof RouteMatcher.Route ?
this.routeMatcher.getPatternComparator((RouteMatcher.Route) destination) :
((SimpleRouteMatcher) this.routeMatcher).getPathMatcher().getPatternComparator((String) destination);
}

/**
Expand All @@ -183,12 +211,12 @@ public DestinationPatternsMessageCondition getMatchingCondition(Message<?> messa
*/
@Override
public int compareTo(DestinationPatternsMessageCondition other, Message<?> message) {
String destination = (String) message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
Object destination = message.getHeaders().get(LOOKUP_DESTINATION_HEADER);
if (destination == null) {
return 0;
}

Comparator<String> patternComparator = this.pathMatcher.getPatternComparator(destination);
Comparator<String> patternComparator = getPatternComparator(destination);
Iterator<String> iterator = this.patterns.iterator();
Iterator<String> iteratorOther = other.patterns.iterator();
while (iterator.hasNext() && iteratorOther.hasNext()) {
Expand Down
Loading

0 comments on commit 97c2de9

Please sign in to comment.