diff --git a/spring-core/src/main/java/org/springframework/util/RouteMatcher.java b/spring-core/src/main/java/org/springframework/util/RouteMatcher.java
new file mode 100644
index 0000000000..22d384895a
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/util/RouteMatcher.java
@@ -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.
+ *
+ *
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 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 getPatternComparator(Route route);
+
+
+ /**
+ * A parsed representation of a route.
+ */
+ interface Route {
+
+ /**
+ * The original route value.
+ */
+ String value();
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java b/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java
new file mode 100644
index 0000000000..c2c7052cf3
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java
@@ -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}.
+ *
+ * Note: 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 matchAndExtract(String pattern, Route route) {
+ if (!match(pattern, route)) {
+ return null;
+ }
+ return this.pathMatcher.extractUriTemplateVariables(pattern, route.value());
+ }
+
+ @Override
+ public Comparator 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;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java
index 113c582153..c8a5bf219b 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java
@@ -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.
@@ -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;
@@ -29,12 +28,15 @@ import java.util.Set;
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
@@ -50,36 +52,41 @@ public class DestinationPatternsMessageCondition
private final Set 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.
+ * 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 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 prependLeadingSlash(Collection patterns, PathMatcher pathMatcher) {
- boolean slashSeparator = pathMatcher.combine("a", "a").equals("a/a");
- Set result = new LinkedHashSet<>(patterns.size());
+ private static Set prependLeadingSlash(String[] patterns, RouteMatcher routeMatcher) {
+ boolean slashSeparator = routeMatcher.combine("a", "a").equals("a/a");
+ Set result = new LinkedHashSet<>(patterns.length);
for (String pattern : patterns) {
if (slashSeparator && StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
pattern = "/" + pattern;
@@ -89,6 +96,12 @@ public class DestinationPatternsMessageCondition
return result;
}
+ private DestinationPatternsMessageCondition(Set patterns, RouteMatcher routeMatcher) {
+ this.patterns = patterns;
+ this.routeMatcher = routeMatcher;
+ }
+
+
public Set getPatterns() {
return this.patterns;
@@ -121,7 +134,7 @@ public class DestinationPatternsMessageCondition
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));
}
}
}
@@ -134,7 +147,7 @@ public class DestinationPatternsMessageCondition
else {
result.add("");
}
- return new DestinationPatternsMessageCondition(result, this.pathMatcher);
+ return new DestinationPatternsMessageCondition(result, this.routeMatcher);
}
/**
@@ -149,7 +162,7 @@ public class DestinationPatternsMessageCondition
@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;
}
@@ -157,18 +170,33 @@ public class DestinationPatternsMessageCondition
return this;
}
- List matches = new ArrayList<>();
+ List 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 getPatternComparator(Object destination) {
+ return destination instanceof RouteMatcher.Route ?
+ this.routeMatcher.getPatternComparator((RouteMatcher.Route) destination) :
+ ((SimpleRouteMatcher) this.routeMatcher).getPathMatcher().getPatternComparator((String) destination);
}
/**
@@ -183,12 +211,12 @@ public class DestinationPatternsMessageCondition
*/
@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 patternComparator = this.pathMatcher.getPatternComparator(destination);
+ Comparator patternComparator = getPatternComparator(destination);
Iterator iterator = this.patterns.iterator();
Iterator iteratorOther = other.patterns.iterator();
while (iterator.hasNext() && iteratorOther.hasNext()) {
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java
index a63e0c44d5..bb8a236999 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java
@@ -56,7 +56,8 @@ import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
-import org.springframework.util.PathMatcher;
+import org.springframework.util.RouteMatcher;
+import org.springframework.util.SimpleRouteMatcher;
import org.springframework.util.StringValueResolver;
import org.springframework.validation.Validator;
@@ -91,7 +92,7 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandlerBy default, {@link AntPathMatcher} is used with separator set to ".".
+ * Set the {@code RouteMatcher} to use for mapping messages to handlers
+ * based on the route patterns they're configured with.
+ * By default, {@link SimpleRouteMatcher} is used, backed by
+ * {@link AntPathMatcher} with "." as separator. For greater
+ * efficiency consider using the {@code PathPatternRouteMatcher} from
+ * {@code spring-web} instead.
*/
- public void setPathMatcher(PathMatcher pathMatcher) {
- Assert.notNull(pathMatcher, "PathMatcher must not be null");
- this.pathMatcher = pathMatcher;
+ public void setRouteMatcher(RouteMatcher routeMatcher) {
+ Assert.notNull(routeMatcher, "RouteMatcher must not be null");
+ this.routeMatcher = routeMatcher;
}
/**
- * Return the PathMatcher implementation to use for matching destinations.
+ * Return the {@code RouteMatcher} used to map messages to handlers.
*/
- public PathMatcher getPathMatcher() {
- return this.pathMatcher;
+ public RouteMatcher getRouteMatcher() {
+ return this.routeMatcher;
}
/**
@@ -289,14 +294,15 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler this.valueResolver.resolveStringValue(s))
.toArray(String[]::new);
}
- return new CompositeMessageCondition(new DestinationPatternsMessageCondition(destinations, this.pathMatcher));
+ return new CompositeMessageCondition(
+ new DestinationPatternsMessageCondition(destinations, this.routeMatcher));
}
@Override
protected Set getDirectLookupMappings(CompositeMessageCondition mapping) {
Set result = new LinkedHashSet<>();
for (String pattern : mapping.getCondition(DestinationPatternsMessageCondition.class).getPatterns()) {
- if (!this.pathMatcher.isPattern(pattern)) {
+ if (!this.routeMatcher.isPattern(pattern)) {
result.add(pattern);
}
}
@@ -304,8 +310,9 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler message) {
- return (String) message.getHeaders().get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER);
+ protected RouteMatcher.Route getDestination(Message> message) {
+ return (RouteMatcher.Route) message.getHeaders()
+ .get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER);
}
@Override
@@ -324,13 +331,15 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler handleMatch(CompositeMessageCondition mapping, HandlerMethod handlerMethod, Message> message) {
+ protected Mono handleMatch(
+ CompositeMessageCondition mapping, HandlerMethod handlerMethod, Message> message) {
+
Set patterns = mapping.getCondition(DestinationPatternsMessageCondition.class).getPatterns();
if (!CollectionUtils.isEmpty(patterns)) {
String pattern = patterns.iterator().next();
- String destination = getDestination(message);
+ RouteMatcher.Route destination = getDestination(message);
Assert.state(destination != null, "Missing destination header");
- Map vars = getPathMatcher().extractUriTemplateVariables(pattern, destination);
+ Map vars = getRouteMatcher().matchAndExtract(pattern, destination);
if (!CollectionUtils.isEmpty(vars)) {
MessageHeaderAccessor mha = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class);
Assert.state(mha != null && mha.isMutable(), "Mutable MessageHeaderAccessor required");
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java
index 948384589c..efbe29872d 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java
@@ -53,6 +53,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
+import org.springframework.util.RouteMatcher;
/**
* Abstract base class for reactive HandlerMethod-based message handling.
@@ -393,8 +394,8 @@ public abstract class AbstractMethodMessageHandler
private Match getHandlerMethod(Message> message) {
List> matches = new ArrayList<>();
- String destination = getDestination(message);
- List mappingsByUrl = destination != null ? this.destinationLookup.get(destination) : null;
+ RouteMatcher.Route destination = getDestination(message);
+ List mappingsByUrl = destination != null ? this.destinationLookup.get(destination.value()) : null;
if (mappingsByUrl != null) {
addMatchesToCollection(mappingsByUrl, message, matches);
}
@@ -418,23 +419,21 @@ public abstract class AbstractMethodMessageHandler
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
HandlerMethod m1 = bestMatch.handlerMethod;
HandlerMethod m2 = secondBestMatch.handlerMethod;
- throw new IllegalStateException("Ambiguous handler methods mapped for destination '" +
- destination + "': {" + m1.getShortLogMessage() + ", " + m2.getShortLogMessage() + "}");
+ throw new IllegalStateException(
+ "Ambiguous handler methods mapped for destination '" +
+ destination.value() + "': {" +
+ m1.getShortLogMessage() + ", " + m2.getShortLogMessage() + "}");
}
}
return bestMatch;
}
/**
- * Extract a String-based destination, if any, that can be used to perform
- * a direct look up into the registered mappings.
- * Note: This is completely optional. The mapping
- * metadata for a sub-class may support neither direct lookups, nor String
- * based destinations.
+ * Extract the destination from the given message.
* @see #getDirectLookupMappings(Object)
*/
@Nullable
- protected abstract String getDestination(Message> message);
+ protected abstract RouteMatcher.Route getDestination(Message> message);
private void addMatchesToCollection(
Collection mappingsToCheck, Message> message, List> matches) {
@@ -470,8 +469,9 @@ public abstract class AbstractMethodMessageHandler
* @param destination the destination
* @param message the message
*/
- protected void handleNoMatch(@Nullable String destination, Message> message) {
- logger.debug("No handlers for destination '" + destination + "'");
+ protected void handleNoMatch(@Nullable RouteMatcher.Route destination, Message> message) {
+ logger.debug("No handlers for destination '" +
+ (destination != null ? destination.value() : "") + "'");
}
/**
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessageHandlerAcceptor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessageHandlerAcceptor.java
index ea5d02a65c..3a8c05fc5d 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessageHandlerAcceptor.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessageHandlerAcceptor.java
@@ -73,6 +73,7 @@ public final class MessageHandlerAcceptor extends RSocketMessageHandler
private MessagingRSocket createRSocket(RSocket rsocket) {
return new MessagingRSocket(this::handleMessage,
+ route -> getRouteMatcher().parseRoute(route),
RSocketRequester.wrap(rsocket, this.defaultDataMimeType, getRSocketStrategies()),
this.defaultDataMimeType,
getRSocketStrategies().dataBufferFactory());
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessagingRSocket.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessagingRSocket.java
index e36875966c..9a4a92bfd2 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessagingRSocket.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MessagingRSocket.java
@@ -42,6 +42,7 @@ import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
+import org.springframework.util.RouteMatcher;
import org.springframework.util.StringUtils;
/**
@@ -56,6 +57,8 @@ class MessagingRSocket extends AbstractRSocket {
private final Function, Mono> handler;
+ private final Function routeParser;
+
private final RSocketRequester requester;
@Nullable
@@ -64,10 +67,13 @@ class MessagingRSocket extends AbstractRSocket {
private final DataBufferFactory bufferFactory;
- MessagingRSocket(Function, Mono> handler, RSocketRequester requester,
+ MessagingRSocket(Function, Mono> handler,
+ Function routeParser, RSocketRequester requester,
@Nullable MimeType defaultDataMimeType, DataBufferFactory bufferFactory) {
+ this.routeParser = routeParser;
Assert.notNull(handler, "'handler' is required");
+ Assert.notNull(routeParser, "'routeParser' is required");
Assert.notNull(requester, "'requester' is required");
this.handler = handler;
this.requester = requester;
@@ -181,7 +187,8 @@ class MessagingRSocket extends AbstractRSocket {
private MessageHeaders createHeaders(String destination, @Nullable MonoProcessor> replyMono) {
MessageHeaderAccessor headers = new MessageHeaderAccessor();
headers.setLeaveMutable(true);
- headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, destination);
+ RouteMatcher.Route route = this.routeParser.apply(destination);
+ headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, route);
if (this.dataMimeType != null) {
headers.setContentType(this.dataMimeType);
}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketMessageHandler.java
index bf7481fe66..69ec6ffdc0 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketMessageHandler.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketMessageHandler.java
@@ -27,6 +27,7 @@ import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.handler.annotation.reactive.MessageMappingMessageHandler;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler;
import org.springframework.util.Assert;
+import org.springframework.util.RouteMatcher;
import org.springframework.util.StringUtils;
/**
@@ -110,16 +111,16 @@ public class RSocketMessageHandler extends MessageMappingMessageHandler {
}
@Override
- protected void handleNoMatch(@Nullable String destination, Message> message) {
+ protected void handleNoMatch(@Nullable RouteMatcher.Route destination, Message> message) {
// MessagingRSocket will raise an error anyway if reply Mono is expected
- // Here we raise a more helpful message a destination is present
+ // Here we raise a more helpful message if a destination is present
// It is OK if some messages (ConnectionSetupPayload, metadataPush) are not handled
- // We need a better way to avoid raising errors for those
+ // This works but would be better to have a more explicit way to differentiate
- if (StringUtils.hasText(destination)) {
- throw new MessageDeliveryException("No handler for destination '" + destination + "'");
+ if (destination != null && StringUtils.hasText(destination.value())) {
+ throw new MessageDeliveryException("No handler for destination '" + destination.value() + "'");
}
}
diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandlerTests.java
index 68fe4bbc4b..be99afaf2d 100644
--- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandlerTests.java
+++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandlerTests.java
@@ -47,6 +47,8 @@ import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Controller;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.SimpleRouteMatcher;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertTrue;
@@ -117,11 +119,11 @@ public class MessageMappingMessageHandlerTests {
public void unhandledExceptionShouldFlowThrough() {
GenericMessage> message = new GenericMessage<>(new Object(),
- Collections.singletonMap(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "string"));
+ Collections.singletonMap(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER,
+ new SimpleRouteMatcher(new AntPathMatcher()).parseRoute("string")));
StepVerifier.create(initMesssageHandler().handleMessage(message))
- .expectErrorSatisfies(ex -> assertTrue(
- "Actual: " + ex.getMessage(),
+ .expectErrorSatisfies(ex -> assertTrue("Actual: " + ex.getMessage(),
ex.getMessage().startsWith("Could not resolve method parameter at index 0")))
.verify(Duration.ofSeconds(5));
}
@@ -156,7 +158,8 @@ public class MessageMappingMessageHandlerTests {
Flux payload = Flux.fromIterable(Arrays.asList(content)).map(parts -> toDataBuffer(parts));
MessageHeaderAccessor headers = new MessageHeaderAccessor();
headers.setLeaveMutable(true);
- headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, destination);
+ headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER,
+ new SimpleRouteMatcher(new AntPathMatcher()).parseRoute(destination));
return MessageBuilder.createMessage(payload, headers.getMessageHeaders());
}
diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java
index c23196da56..86a6b6fb80 100644
--- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java
+++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java
@@ -44,6 +44,8 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
+import org.springframework.util.RouteMatcher;
+import org.springframework.util.SimpleRouteMatcher;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
@@ -81,7 +83,8 @@ public class MethodMessageHandlerTests {
handler.afterPropertiesSet();
Message> message = new GenericMessage<>("body", Collections.singletonMap(
- DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/bestmatch/bar/path"));
+ DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER,
+ new SimpleRouteMatcher(new AntPathMatcher()).parseRoute("/bestmatch/bar/path")));
handler.handleMessage(message).block(Duration.ofSeconds(5));
@@ -102,7 +105,8 @@ public class MethodMessageHandlerTests {
TestController.class);
Message> message = new GenericMessage<>("body", Collections.singletonMap(
- DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageWithArgument"));
+ DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER,
+ new SimpleRouteMatcher(new AntPathMatcher()).parseRoute("/handleMessageWithArgument")));
handler.handleMessage(message).block(Duration.ofSeconds(5));
@@ -118,7 +122,8 @@ public class MethodMessageHandlerTests {
TestMethodMessageHandler handler = initMethodMessageHandler(TestController.class);
Message> message = new GenericMessage<>("body", Collections.singletonMap(
- DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageWithError"));
+ DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER,
+ new SimpleRouteMatcher(new AntPathMatcher()).parseRoute("/handleMessageWithError")));
handler.handleMessage(message).block(Duration.ofSeconds(5));
@@ -238,22 +243,27 @@ public class MethodMessageHandlerTests {
@Override
@Nullable
- protected String getDestination(Message> message) {
- return (String) message.getHeaders().get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER);
+ protected RouteMatcher.Route getDestination(Message> message) {
+ return (RouteMatcher.Route) message.getHeaders().get(
+ DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER);
}
@Override
protected String getMatchingMapping(String mapping, Message> message) {
- String destination = getDestination(message);
+ RouteMatcher.Route destination = getDestination(message);
Assert.notNull(destination, "No destination");
- return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null;
+ return mapping.equals(destination.value()) ||
+ this.pathMatcher.match(mapping, destination.value()) ? mapping : null;
}
@Override
protected Comparator getMappingComparator(Message> message) {
return (info1, info2) -> {
- DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1);
- DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2);
+ SimpleRouteMatcher routeMatcher = new SimpleRouteMatcher(new AntPathMatcher());
+ DestinationPatternsMessageCondition cond1 =
+ new DestinationPatternsMessageCondition(new String[] { info1 }, routeMatcher);
+ DestinationPatternsMessageCondition cond2 =
+ new DestinationPatternsMessageCondition(new String[] { info2 }, routeMatcher);
return cond1.compareTo(cond2, message);
};
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternRouteMatcher.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternRouteMatcher.java
new file mode 100644
index 0000000000..6c5c2cc172
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternRouteMatcher.java
@@ -0,0 +1,106 @@
+/*
+ * 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.web.util.pattern;
+
+import java.util.Comparator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.http.server.PathContainer;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.RouteMatcher;
+
+/**
+ * {@code RouteMatcher} built on {@link PathPatternParser} that uses
+ * {@link PathContainer} and {@link PathPattern} as parsed representations of
+ * routes and patterns.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.2
+ */
+public class PathPatternRouteMatcher implements RouteMatcher {
+
+ private final PathPatternParser parser;
+
+ private final Map pathPatternCache = new ConcurrentHashMap<>();
+
+
+ public PathPatternRouteMatcher(PathPatternParser parser) {
+ Assert.notNull(parser, "PathPatternParser must not be null");
+ this.parser = parser;
+ }
+
+
+ @Override
+ public Route parseRoute(String routeValue) {
+ return new PathContainerRoute(PathContainer.parsePath(routeValue));
+ }
+
+ @Override
+ public boolean isPattern(String route) {
+ return getPathPattern(route).hasPatternSyntax();
+ }
+
+ @Override
+ public String combine(String pattern1, String pattern2) {
+ return getPathPattern(pattern1).combine(getPathPattern(pattern2)).getPatternString();
+ }
+
+ @Override
+ public boolean match(String pattern, Route route) {
+ return getPathPattern(pattern).matches(getPathContainer(route));
+ }
+
+ @Override
+ @Nullable
+ public Map matchAndExtract(String pattern, Route route) {
+ PathPattern.PathMatchInfo info = getPathPattern(pattern).matchAndExtract(getPathContainer(route));
+ return info != null ? info.getUriVariables() : null;
+ }
+
+ @Override
+ public Comparator getPatternComparator(Route route) {
+ return Comparator.comparing(this::getPathPattern);
+ }
+
+ private PathPattern getPathPattern(String pattern) {
+ return this.pathPatternCache.computeIfAbsent(pattern, this.parser::parse);
+ }
+
+ private PathContainer getPathContainer(Route route) {
+ Assert.isInstanceOf(PathContainerRoute.class, route);
+ return ((PathContainerRoute) route).pathContainer;
+ }
+
+
+ private static class PathContainerRoute implements Route {
+
+ private final PathContainer pathContainer;
+
+
+ PathContainerRoute(PathContainer pathContainer) {
+ this.pathContainer = pathContainer;
+ }
+
+
+ @Override
+ public String value() {
+ return this.pathContainer.value();
+ }
+ }
+
+}
\ No newline at end of file