From 97c2de915afb34aad64e6a63a448e6e66a6040de Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 14 May 2019 21:44:26 -0400 Subject: [PATCH] Add RouteMatcher Closes gh-22642 --- .../springframework/util/RouteMatcher.java | 100 +++++++++++++++++ .../util/SimpleRouteMatcher.java | 101 +++++++++++++++++ .../DestinationPatternsMessageCondition.java | 90 ++++++++++----- .../MessageMappingMessageHandler.java | 49 ++++---- .../AbstractMethodMessageHandler.java | 24 ++-- .../rsocket/MessageHandlerAcceptor.java | 1 + .../messaging/rsocket/MessagingRSocket.java | 11 +- .../rsocket/RSocketMessageHandler.java | 11 +- .../MessageMappingMessageHandlerTests.java | 11 +- .../reactive/MethodMessageHandlerTests.java | 28 +++-- .../util/pattern/PathPatternRouteMatcher.java | 106 ++++++++++++++++++ 11 files changed, 449 insertions(+), 83 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/util/RouteMatcher.java create mode 100644 spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternRouteMatcher.java 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