Add RouteMatcher
Closes gh-22642
This commit is contained in:
@@ -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<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;
|
||||
@@ -89,6 +96,12 @@ public class DestinationPatternsMessageCondition
|
||||
return result;
|
||||
}
|
||||
|
||||
private DestinationPatternsMessageCondition(Set<String> patterns, RouteMatcher routeMatcher) {
|
||||
this.patterns = patterns;
|
||||
this.routeMatcher = routeMatcher;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Set<String> 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<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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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<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()) {
|
||||
|
||||
@@ -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 AbstractMethodMessageHandler<C
|
||||
@Nullable
|
||||
private Validator validator;
|
||||
|
||||
private PathMatcher pathMatcher;
|
||||
private RouteMatcher routeMatcher;
|
||||
|
||||
private ConversionService conversionService = new DefaultFormattingConversionService();
|
||||
|
||||
@@ -100,8 +101,9 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler<C
|
||||
|
||||
|
||||
public MessageMappingMessageHandler() {
|
||||
this.pathMatcher = new AntPathMatcher();
|
||||
((AntPathMatcher) this.pathMatcher).setPathSeparator(".");
|
||||
AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
pathMatcher.setPathSeparator(".");
|
||||
this.routeMatcher = new SimpleRouteMatcher(pathMatcher);
|
||||
}
|
||||
|
||||
|
||||
@@ -187,20 +189,23 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler<C
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the PathMatcher implementation to use for matching destinations
|
||||
* against configured destination patterns.
|
||||
* <p>By 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.
|
||||
* <p>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<C
|
||||
.map(s -> 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<String> getDirectLookupMappings(CompositeMessageCondition mapping) {
|
||||
Set<String> 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<C
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
@@ -324,13 +331,15 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler<C
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> handleMatch(CompositeMessageCondition mapping, HandlerMethod handlerMethod, Message<?> message) {
|
||||
protected Mono<Void> handleMatch(
|
||||
CompositeMessageCondition mapping, HandlerMethod handlerMethod, Message<?> message) {
|
||||
|
||||
Set<String> 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<String, String> vars = getPathMatcher().extractUriTemplateVariables(pattern, destination);
|
||||
Map<String, String> 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");
|
||||
|
||||
@@ -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<T>
|
||||
private Match<T> getHandlerMethod(Message<?> message) {
|
||||
List<Match<T>> matches = new ArrayList<>();
|
||||
|
||||
String destination = getDestination(message);
|
||||
List<T> mappingsByUrl = destination != null ? this.destinationLookup.get(destination) : null;
|
||||
RouteMatcher.Route destination = getDestination(message);
|
||||
List<T> mappingsByUrl = destination != null ? this.destinationLookup.get(destination.value()) : null;
|
||||
if (mappingsByUrl != null) {
|
||||
addMatchesToCollection(mappingsByUrl, message, matches);
|
||||
}
|
||||
@@ -418,23 +419,21 @@ public abstract class AbstractMethodMessageHandler<T>
|
||||
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.
|
||||
* <p><strong>Note:</strong> 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<T> mappingsToCheck, Message<?> message, List<Match<T>> matches) {
|
||||
@@ -470,8 +469,9 @@ public abstract class AbstractMethodMessageHandler<T>
|
||||
* @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() : "") + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<Message<?>, Mono<Void>> handler;
|
||||
|
||||
private final Function<String, RouteMatcher.Route> routeParser;
|
||||
|
||||
private final RSocketRequester requester;
|
||||
|
||||
@Nullable
|
||||
@@ -64,10 +67,13 @@ class MessagingRSocket extends AbstractRSocket {
|
||||
private final DataBufferFactory bufferFactory;
|
||||
|
||||
|
||||
MessagingRSocket(Function<Message<?>, Mono<Void>> handler, RSocketRequester requester,
|
||||
MessagingRSocket(Function<Message<?>, Mono<Void>> handler,
|
||||
Function<String, RouteMatcher.Route> 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);
|
||||
}
|
||||
|
||||
@@ -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() + "'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user