Update support for using "." as path separator

Issue: SPR-11660
This commit is contained in:
Rossen Stoyanchev 2014-07-14 18:47:51 -04:00
parent 928a466b5d
commit ab2526a586
26 changed files with 405 additions and 254 deletions

View File

@ -68,23 +68,27 @@ public class AntPathMatcher implements PathMatcher {
final Map<String, AntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<String, AntPathStringMatcher>(256); final Map<String, AntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<String, AntPathStringMatcher>(256);
private PathSeparatorPatternCache pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR);
/** /**
* Create a new AntPathMatcher with the default Ant path separator "/". * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}.
*/ */
public AntPathMatcher() { public AntPathMatcher() {
} }
/** /**
* Create a new AntPathMatcher. * A convenience alternative constructor to use with a custom path separator.
* @param pathSeparator the path separator to use * @param pathSeparator the path separator to use, must not be {@code null}.
* @since 4.1 * @since 4.1
*/ */
public AntPathMatcher(String pathSeparator) { public AntPathMatcher(String pathSeparator) {
if(pathSeparator != null) { Assert.notNull(pathSeparator, "'pathSeparator' is required");
this.pathSeparator = pathSeparator; this.pathSeparator = pathSeparator;
this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator);
} }
}
/** /**
* Set the path separator to use for pattern parsing. * Set the path separator to use for pattern parsing.
@ -92,6 +96,7 @@ public class AntPathMatcher implements PathMatcher {
*/ */
public void setPathSeparator(String pathSeparator) { public void setPathSeparator(String pathSeparator) {
this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator);
} }
/** /**
@ -447,20 +452,20 @@ public class AntPathMatcher implements PathMatcher {
// /hotels/* + /booking -> /hotels/booking // /hotels/* + /booking -> /hotels/booking
// /hotels/* + booking -> /hotels/booking // /hotels/* + booking -> /hotels/booking
if (pattern1.endsWith(this.pathSeparator + "*")) { if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) {
return separatorConcat(pattern1.substring(0, pattern1.length() - 2), pattern2); return concat(pattern1.substring(0, pattern1.length() - 2), pattern2);
} }
// /hotels/** + /booking -> /hotels/**/booking // /hotels/** + /booking -> /hotels/**/booking
// /hotels/** + booking -> /hotels/**/booking // /hotels/** + booking -> /hotels/**/booking
if (pattern1.endsWith(this.pathSeparator + "**")) { if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) {
return separatorConcat(pattern1, pattern2); return concat(pattern1, pattern2);
} }
int starDotPos1 = pattern1.indexOf("*."); int starDotPos1 = pattern1.indexOf("*.");
if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) {
// simply concatenate the two patterns // simply concatenate the two patterns
return separatorConcat(pattern1, pattern2); return concat(pattern1, pattern2);
} }
String extension1 = pattern1.substring(starDotPos1 + 1); String extension1 = pattern1.substring(starDotPos1 + 1);
int dotPos2 = pattern2.indexOf('.'); int dotPos2 = pattern2.indexOf('.');
@ -470,7 +475,7 @@ public class AntPathMatcher implements PathMatcher {
return fileName2 + extension; return fileName2 + extension;
} }
private String separatorConcat(String path1, String path2) { private String concat(String path1, String path2) {
if (path1.endsWith(this.pathSeparator) || path2.startsWith(this.pathSeparator)) { if (path1.endsWith(this.pathSeparator) || path2.startsWith(this.pathSeparator)) {
return path1 + path2; return path1 + path2;
} }
@ -763,4 +768,29 @@ public class AntPathMatcher implements PathMatcher {
} }
} }
/**
* A simple cache for patterns that depend on the configured path separator.
*/
private static class PathSeparatorPatternCache {
private final String endsOnWildCard;
private final String endsOnDoubleWildCard;
private PathSeparatorPatternCache(String pathSeparator) {
this.endsOnWildCard = pathSeparator + "*";
this.endsOnDoubleWildCard = pathSeparator + "**";
}
public String getEndsOnWildCard() {
return this.endsOnWildCard;
}
public String getEndsOnDoubleWildCard() {
return this.endsOnDoubleWildCard;
}
}
} }

View File

@ -607,9 +607,10 @@ public class AntPathMatcherTests {
} }
@Test @Test
public void noExtensionHandlingWithDotSeparator() { public void testExtensionMappingWithDotPathSeparator() {
pathMatcher.setPathSeparator("."); pathMatcher.setPathSeparator(".");
assertEquals("/*.html.hotel.*", pathMatcher.combine("/*.html", "hotel.*")); assertEquals("Extension mapping should be disabled with \".\" as path separator",
"/*.html.hotel.*", pathMatcher.combine("/*.html", "hotel.*"));
} }
} }

View File

@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
@ -55,52 +56,39 @@ public final class DestinationPatternsMessageCondition
* @param patterns 0 or more URL patterns; if 0 the condition will match to every request. * @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
*/ */
public DestinationPatternsMessageCondition(String... patterns) { public DestinationPatternsMessageCondition(String... patterns) {
this(patterns, null, true); this(patterns, null);
} }
/** /**
* Additional constructor with a customized path matcher. * Alternative constructor accepting a custom PathMatcher.
* @param patterns the URL patterns to use; if 0, the condition will match to every request. * @param patterns the URL patterns to use; if 0, the condition will match to every request.
* @param pathMatcher the customized path matcher to use with patterns * @param pathMatcher the PathMatcher to use.
*/ */
public DestinationPatternsMessageCondition(String[] patterns, PathMatcher pathMatcher) { public DestinationPatternsMessageCondition(String[] patterns, PathMatcher pathMatcher) {
this(asList(patterns), pathMatcher, true); this(asList(patterns), pathMatcher);
} }
/** private DestinationPatternsMessageCondition(Collection<String> patterns, PathMatcher pathMatcher) {
* Additional constructor with a customized path matcher and a flag specifying if
* the destination patterns should be prepended with "/".
* @param patterns the URL patterns to use; if 0, the condition will match to every request.
* @param pathMatcher the customized path matcher to use with patterns
* @param prependLeadingSlash to specify whether each pattern that is not empty and does not
* start with "/" will be prepended with "/" or not
* @since 4.1
*/
public DestinationPatternsMessageCondition(String[] patterns, PathMatcher pathMatcher,
boolean prependLeadingSlash) {
this(asList(patterns), pathMatcher, prependLeadingSlash);
}
private DestinationPatternsMessageCondition(Collection<String> patterns,
PathMatcher pathMatcher, boolean prependLeadingSlash) {
this.patterns = Collections.unmodifiableSet(initializePatterns(patterns, prependLeadingSlash));
this.pathMatcher = (pathMatcher != null) ? pathMatcher : new AntPathMatcher(); this.pathMatcher = (pathMatcher != null) ? pathMatcher : new AntPathMatcher();
this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns, this.pathMatcher));
} }
private static List<String> asList(String... patterns) { private static List<String> asList(String... patterns) {
return patterns != null ? Arrays.asList(patterns) : Collections.<String>emptyList(); return patterns != null ? Arrays.asList(patterns) : Collections.<String>emptyList();
} }
private static Set<String> initializePatterns(Collection<String> patterns, private static Set<String> prependLeadingSlash(Collection<String> patterns, PathMatcher pathMatcher) {
boolean prependLeadingSlash) {
if (patterns == null) { if (patterns == null) {
return Collections.emptySet(); return Collections.emptySet();
} }
boolean slashSeparator = pathMatcher.combine("a", "a").equals("a/a");
Set<String> result = new LinkedHashSet<String>(patterns.size()); Set<String> result = new LinkedHashSet<String>(patterns.size());
for (String pattern : patterns) { for (String pattern : patterns) {
if (StringUtils.hasLength(pattern) && !pattern.startsWith("/") && prependLeadingSlash) { if (slashSeparator) {
if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
pattern = "/" + pattern; pattern = "/" + pattern;
} }
}
result.add(pattern); result.add(pattern);
} }
return result; return result;
@ -149,7 +137,7 @@ public final class DestinationPatternsMessageCondition
else { else {
result.add(""); result.add("");
} }
return new DestinationPatternsMessageCondition(result, this.pathMatcher, false); return new DestinationPatternsMessageCondition(result, this.pathMatcher);
} }
/** /**
@ -184,7 +172,7 @@ public final class DestinationPatternsMessageCondition
} }
Collections.sort(matches, this.pathMatcher.getPatternComparator(destination)); Collections.sort(matches, this.pathMatcher.getPatternComparator(destination));
return new DestinationPatternsMessageCondition(matches, this.pathMatcher, false); return new DestinationPatternsMessageCondition(matches, this.pathMatcher);
} }
/** /**

View File

@ -90,27 +90,27 @@ public abstract class AbstractMethodMessageHandler<T>
/** /**
* Configure one or more prefixes to match to the destinations of handled messages. * When this property is configured only messages to destinations matching
* Messages whose destination does not start with one of the configured prefixes * one of the configured prefixes are eligible for handling. When there is a
* are ignored. When a destination matches one of the configured prefixes, the * match the prefix is removed and only the remaining part of the destination
* matching part is removed from destination before performing a lookup for a matching * is used for method-mapping purposes.
* message handling method. Prefixes without a trailing slash will have one appended *
* automatically. * <p>By default no prefixes are configured in which case all messages are
* <p>By default the list of prefixes is empty in which case all destinations match. * eligible for handling.
*/ */
public void setDestinationPrefixes(Collection<String> prefixes) { public void setDestinationPrefixes(Collection<String> prefixes) {
this.destinationPrefixes.clear(); this.destinationPrefixes.clear();
if (prefixes != null) { if (prefixes != null) {
for (String prefix : prefixes) { for (String prefix : prefixes) {
prefix = prefix.trim(); prefix = prefix.trim();
if (!prefix.endsWith("/")) {
prefix += "/";
}
this.destinationPrefixes.add(prefix); this.destinationPrefixes.add(prefix);
} }
} }
} }
/**
* Return the configured destination prefixes.
*/
public Collection<String> getDestinationPrefixes() { public Collection<String> getDestinationPrefixes() {
return this.destinationPrefixes; return this.destinationPrefixes;
} }
@ -346,11 +346,11 @@ public abstract class AbstractMethodMessageHandler<T>
protected abstract String getDestination(Message<?> message); protected abstract String getDestination(Message<?> message);
/** /**
* Find if the given destination matches any of the configured allowed destination * Check whether the given destination (of an incoming message) matches to
* prefixes and if a match is found return the destination with the prefix removed. * one of the configured destination prefixes and if so return the remaining
* <p>If no destination prefixes are configured, the destination is returned as is. * portion of the destination after the matched prefix.
* @return the destination to use to find matching message handling methods * <p>If there are no matching prefixes, return {@code null}.
* or {@code null} if the destination does not match * <p>If there are no destination prefixes, return the destination as is.
*/ */
protected String getLookupDestination(String destination) { protected String getLookupDestination(String destination) {
if (destination == null) { if (destination == null) {
@ -361,7 +361,7 @@ public abstract class AbstractMethodMessageHandler<T>
} }
for (String prefix : this.destinationPrefixes) { for (String prefix : this.destinationPrefixes) {
if (destination.startsWith(prefix)) { if (destination.startsWith(prefix)) {
return destination.substring(prefix.length() - 1); return destination.substring(prefix.length());
} }
} }
return null; return null;

View File

@ -93,7 +93,9 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
private ConversionService conversionService = new DefaultFormattingConversionService(); private ConversionService conversionService = new DefaultFormattingConversionService();
private PathMatcher pathMatcher; private PathMatcher pathMatcher = new AntPathMatcher();
private boolean slashPathSeparator = true;
private Validator validator; private Validator validator;
@ -113,21 +115,6 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
*/ */
public SimpAnnotationMethodMessageHandler(SubscribableChannel clientInboundChannel, public SimpAnnotationMethodMessageHandler(SubscribableChannel clientInboundChannel,
MessageChannel clientOutboundChannel, SimpMessageSendingOperations brokerTemplate) { MessageChannel clientOutboundChannel, SimpMessageSendingOperations brokerTemplate) {
this(clientInboundChannel, clientOutboundChannel, brokerTemplate, null);
}
/**
* Create an instance of SimpAnnotationMethodMessageHandler with the given
* message channels and broker messaging template.
* @param clientInboundChannel the channel for receiving messages from clients (e.g. WebSocket clients)
* @param clientOutboundChannel the channel for messages to clients (e.g. WebSocket clients)
* @param brokerTemplate a messaging template to send application messages to the broker
* @param pathSeparator the path separator to use with the destination patterns
* @since 4.1
*/
public SimpAnnotationMethodMessageHandler(SubscribableChannel clientInboundChannel,
MessageChannel clientOutboundChannel, SimpMessageSendingOperations brokerTemplate,
String pathSeparator) {
Assert.notNull(clientInboundChannel, "clientInboundChannel must not be null"); Assert.notNull(clientInboundChannel, "clientInboundChannel must not be null");
Assert.notNull(clientOutboundChannel, "clientOutboundChannel must not be null"); Assert.notNull(clientOutboundChannel, "clientOutboundChannel must not be null");
@ -136,7 +123,6 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
this.clientInboundChannel = clientInboundChannel; this.clientInboundChannel = clientInboundChannel;
this.clientMessagingTemplate = new SimpMessagingTemplate(clientOutboundChannel); this.clientMessagingTemplate = new SimpMessagingTemplate(clientOutboundChannel);
this.brokerTemplate = brokerTemplate; this.brokerTemplate = brokerTemplate;
this.pathMatcher = new AntPathMatcher(pathSeparator);
Collection<MessageConverter> converters = new ArrayList<MessageConverter>(); Collection<MessageConverter> converters = new ArrayList<MessageConverter>();
converters.add(new StringMessageConverter()); converters.add(new StringMessageConverter());
@ -144,6 +130,36 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
this.messageConverter = new CompositeMessageConverter(converters); this.messageConverter = new CompositeMessageConverter(converters);
} }
/**
* {@inheritDoc}
* <p>Destination prefixes are expected to be slash-separated Strings and
* therefore a slash is automatically appended where missing to ensure a
* proper prefix-based match (i.e. matching complete segments).
*
* <p>Note however that the remaining portion of a destination after the
* prefix may use a different separator (e.g. commonly "." in messaging)
* depending on the configured {@code PathMatcher}.
*/
@Override
public void setDestinationPrefixes(Collection<String> prefixes) {
super.setDestinationPrefixes(appendSlashes(prefixes));
}
private static Collection<String> appendSlashes(Collection<String> prefixes) {
if (CollectionUtils.isEmpty(prefixes)) {
return prefixes;
}
Collection<String> result = new ArrayList<String>(prefixes.size());
for (String prefix : prefixes) {
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
result.add(prefix);
}
return result;
}
/** /**
* Configure a {@link MessageConverter} to use to convert the payload of a message * Configure a {@link MessageConverter} to use to convert the payload of a message
* from serialize form with a specific MIME type to an Object matching the target * from serialize form with a specific MIME type to an Object matching the target
@ -189,6 +205,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
public void setPathMatcher(PathMatcher pathMatcher) { public void setPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher must not be null"); Assert.notNull(pathMatcher, "PathMatcher must not be null");
this.pathMatcher = pathMatcher; this.pathMatcher = pathMatcher;
this.slashPathSeparator = this.pathMatcher.combine("a", "a").equals("a/a");
} }
/** /**
@ -333,31 +350,31 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
MessageMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, MessageMapping.class); MessageMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, MessageMapping.class);
MessageMapping messageAnnot = AnnotationUtils.findAnnotation(method, MessageMapping.class); MessageMapping messageAnnot = AnnotationUtils.findAnnotation(method, MessageMapping.class);
if (messageAnnot != null) { if (messageAnnot != null) {
SimpMessageMappingInfo result = createMessageMappingCondition(messageAnnot, typeAnnotation == null); SimpMessageMappingInfo result = createMessageMappingCondition(messageAnnot);
if (typeAnnotation != null) { if (typeAnnotation != null) {
result = createMessageMappingCondition(typeAnnotation, false).combine(result); result = createMessageMappingCondition(typeAnnotation).combine(result);
} }
return result; return result;
} }
SubscribeMapping subsribeAnnotation = AnnotationUtils.findAnnotation(method, SubscribeMapping.class); SubscribeMapping subsribeAnnotation = AnnotationUtils.findAnnotation(method, SubscribeMapping.class);
if (subsribeAnnotation != null) { if (subsribeAnnotation != null) {
SimpMessageMappingInfo result = createSubscribeCondition(subsribeAnnotation, typeAnnotation == null); SimpMessageMappingInfo result = createSubscribeCondition(subsribeAnnotation);
if (typeAnnotation != null) { if (typeAnnotation != null) {
result = createMessageMappingCondition(typeAnnotation, false).combine(result); result = createMessageMappingCondition(typeAnnotation).combine(result);
} }
return result; return result;
} }
return null; return null;
} }
private SimpMessageMappingInfo createMessageMappingCondition(MessageMapping annotation, boolean prependLeadingSlash) { private SimpMessageMappingInfo createMessageMappingCondition(MessageMapping annotation) {
return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.MESSAGE, return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.MESSAGE,
new DestinationPatternsMessageCondition(annotation.value(), this.pathMatcher, prependLeadingSlash)); new DestinationPatternsMessageCondition(annotation.value(), this.pathMatcher));
} }
private SimpMessageMappingInfo createSubscribeCondition(SubscribeMapping annotation, boolean prependLeadingSlash) { private SimpMessageMappingInfo createSubscribeCondition(SubscribeMapping annotation) {
return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.SUBSCRIBE, return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.SUBSCRIBE,
new DestinationPatternsMessageCondition(annotation.value(), this.pathMatcher, prependLeadingSlash)); new DestinationPatternsMessageCondition(annotation.value(), this.pathMatcher));
} }
@Override @Override
@ -376,6 +393,27 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
return SimpMessageHeaderAccessor.getDestination(message.getHeaders()); return SimpMessageHeaderAccessor.getDestination(message.getHeaders());
} }
@Override
protected String getLookupDestination(String destination) {
if (destination == null) {
return null;
}
if (CollectionUtils.isEmpty(getDestinationPrefixes())) {
return destination;
}
for (String prefix : getDestinationPrefixes()) {
if (destination.startsWith(prefix)) {
if (this.slashPathSeparator) {
return destination.substring(prefix.length() - 1);
}
else {
return destination.substring(prefix.length());
}
}
}
return null;
}
@Override @Override
protected SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message<?> message) { protected SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message<?> message) {
return mapping.getMatchingCondition(message); return mapping.getMatchingCondition(message);

View File

@ -51,6 +51,8 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
private SubscriptionRegistry subscriptionRegistry; private SubscriptionRegistry subscriptionRegistry;
private PathMatcher pathMatcher;
private MessageHeaderInitializer headerInitializer; private MessageHeaderInitializer headerInitializer;
@ -58,38 +60,21 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
* Create a SimpleBrokerMessageHandler instance with the given message channels * Create a SimpleBrokerMessageHandler instance with the given message channels
* and destination prefixes. * and destination prefixes.
* *
* @param clientInboundChannel the channel for receiving messages from clients (e.g. WebSocket clients) * @param inChannel the channel for receiving messages from clients (e.g. WebSocket clients)
* @param clientOutboundChannel the channel for sending messages to clients (e.g. WebSocket clients) * @param outChannel the channel for sending messages to clients (e.g. WebSocket clients)
* @param brokerChannel the channel for the application to send messages to the broker * @param brokerChannel the channel for the application to send messages to the broker
*/ */
public SimpleBrokerMessageHandler(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, public SimpleBrokerMessageHandler(SubscribableChannel inChannel, MessageChannel outChannel,
SubscribableChannel brokerChannel, Collection<String> destinationPrefixes) { SubscribableChannel brokerChannel, Collection<String> destinationPrefixes) {
this(clientInboundChannel, clientOutboundChannel, brokerChannel, destinationPrefixes, null);
}
/**
* Additional constructor with a customized path matcher.
*
* @param clientInboundChannel the channel for receiving messages from clients (e.g. WebSocket clients)
* @param clientOutboundChannel the channel for sending messages to clients (e.g. WebSocket clients)
* @param brokerChannel the channel for the application to send messages to the broker
* @param pathMatcher the path matcher to use
* @since 4.1
*/
public SimpleBrokerMessageHandler(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel,
SubscribableChannel brokerChannel, Collection<String> destinationPrefixes, PathMatcher pathMatcher) {
super(destinationPrefixes); super(destinationPrefixes);
Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(inChannel, "'clientInboundChannel' must not be null");
Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); Assert.notNull(outChannel, "'clientOutboundChannel' must not be null");
Assert.notNull(brokerChannel, "'brokerChannel' must not be null"); Assert.notNull(brokerChannel, "'brokerChannel' must not be null");
this.clientInboundChannel = clientInboundChannel; this.clientInboundChannel = inChannel;
this.clientOutboundChannel = clientOutboundChannel; this.clientOutboundChannel = outChannel;
this.brokerChannel = brokerChannel; this.brokerChannel = brokerChannel;
DefaultSubscriptionRegistry subscriptionRegistry = new DefaultSubscriptionRegistry(); DefaultSubscriptionRegistry subscriptionRegistry = new DefaultSubscriptionRegistry();
if(pathMatcher != null) {
subscriptionRegistry.setPathMatcher(pathMatcher);
}
this.subscriptionRegistry = subscriptionRegistry; this.subscriptionRegistry = subscriptionRegistry;
} }
@ -106,15 +91,41 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
return this.brokerChannel; return this.brokerChannel;
} }
/**
* Configure a custom SubscriptionRegistry to use for storing subscriptions.
*
* <p><strong>Note</strong> that when a custom PathMatcher is configured via
* {@link #setPathMatcher}, if the custom registry is not an instance of
* {@link DefaultSubscriptionRegistry}, the provided PathMatcher is not used
* and must be configured directly on the custom registry.
*/
public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) { public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
Assert.notNull(subscriptionRegistry, "SubscriptionRegistry must not be null"); Assert.notNull(subscriptionRegistry, "SubscriptionRegistry must not be null");
this.subscriptionRegistry = subscriptionRegistry; this.subscriptionRegistry = subscriptionRegistry;
initPathMatcherToUse();
}
private void initPathMatcherToUse() {
if (this.pathMatcher != null) {
if (this.subscriptionRegistry instanceof DefaultSubscriptionRegistry) {
((DefaultSubscriptionRegistry) this.subscriptionRegistry).setPathMatcher(this.pathMatcher);
}
}
} }
public SubscriptionRegistry getSubscriptionRegistry() { public SubscriptionRegistry getSubscriptionRegistry() {
return this.subscriptionRegistry; return this.subscriptionRegistry;
} }
/**
* When configured, the given PathMatcher is passed down to the
* SubscriptionRegistry to use for matching destination to subscriptions.
*/
public void setPathMatcher(PathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
initPathMatcherToUse();
}
/** /**
* Configure a {@link MessageHeaderInitializer} to apply to the headers of all * Configure a {@link MessageHeaderInitializer} to apply to the headers of all
* messages sent to the client outbound channel. * messages sent to the client outbound channel.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,6 +23,7 @@ import java.util.List;
import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -66,4 +67,6 @@ public abstract class AbstractBrokerRegistration {
return this.destinationPrefixes; return this.destinationPrefixes;
} }
protected abstract AbstractBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel);
} }

View File

@ -39,9 +39,9 @@ import org.springframework.messaging.simp.user.UserSessionRegistry;
import org.springframework.messaging.support.AbstractSubscribableChannel; import org.springframework.messaging.support.AbstractSubscribableChannel;
import org.springframework.messaging.support.ExecutorSubscribableChannel; import org.springframework.messaging.support.ExecutorSubscribableChannel;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
import org.springframework.util.PathMatcher;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
@ -206,17 +206,17 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
@Bean @Bean
public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() { public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() {
String defaultSeparator = this.getBrokerRegistry().getDefaultSeparator();
SimpAnnotationMethodMessageHandler handler = new SimpAnnotationMethodMessageHandler( SimpAnnotationMethodMessageHandler handler = new SimpAnnotationMethodMessageHandler(
clientInboundChannel(), clientOutboundChannel(), brokerMessagingTemplate(), defaultSeparator); clientInboundChannel(), clientOutboundChannel(), brokerMessagingTemplate());
handler.setDestinationPrefixes(getBrokerRegistry().getApplicationDestinationPrefixes()); handler.setDestinationPrefixes(getBrokerRegistry().getApplicationDestinationPrefixes());
handler.setMessageConverter(brokerMessageConverter()); handler.setMessageConverter(brokerMessageConverter());
handler.setValidator(simpValidator()); handler.setValidator(simpValidator());
AntPathMatcher pathMatcher = new AntPathMatcher(defaultSeparator); PathMatcher pathMatcher = this.getBrokerRegistry().getPathMatcher();
if (pathMatcher != null) {
handler.setPathMatcher(pathMatcher); handler.setPathMatcher(pathMatcher);
}
return handler; return handler;
} }
@ -234,9 +234,7 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
@Bean @Bean
public UserDestinationMessageHandler userDestinationMessageHandler() { public UserDestinationMessageHandler userDestinationMessageHandler() {
UserDestinationMessageHandler handler = new UserDestinationMessageHandler( return new UserDestinationMessageHandler(clientInboundChannel(), brokerChannel(), userDestinationResolver());
clientInboundChannel(), brokerChannel(), userDestinationResolver());
return handler;
} }
@Bean @Bean

View File

@ -25,6 +25,7 @@ import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
/** /**
* A registry for configuring message broker options. * A registry for configuring message broker options.
@ -47,9 +48,10 @@ public class MessageBrokerRegistry {
private String userDestinationPrefix; private String userDestinationPrefix;
private PathMatcher pathMatcher;
private ChannelRegistration brokerChannelRegistration = new ChannelRegistration(); private ChannelRegistration brokerChannelRegistration = new ChannelRegistration();
private String defaultSeparator;
public MessageBrokerRegistry(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel) { public MessageBrokerRegistry(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel) {
Assert.notNull(clientInboundChannel); Assert.notNull(clientInboundChannel);
@ -111,6 +113,31 @@ public class MessageBrokerRegistry {
return this; return this;
} }
/**
* Configure the PathMatcher to use to match the destinations of incoming
* messages to {@code @MessageMapping} and {@code @SubscribeMapping} methods.
*
* <p>By default {@link org.springframework.util.AntPathMatcher} is configured.
* However applications may provide an {@code AntPathMatcher} instance
* customized to use "." (commonly used in messaging) instead of "/" as path
* separator or provide a completely different PathMatcher implementation.
*
* <p>Note that the configured PathMatcher is only used for matching the
* portion of the destination after the configured prefix. For example given
* application destination prefix "/app" and destination "/app/price.stock.**",
* the message might be mapped to a controller with "price" and "stock.**"
* as its type and method-level mappings respectively.
*
* <p>When the simple broker is enabled, the PathMatcher configured here is
* also used to match message destinations when brokering messages.
*
* @since 4.1
*/
public MessageBrokerRegistry setPathMatcher(PathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
return this;
}
/** /**
* Customize the channel used to send messages from the application to the message * Customize the channel used to send messages from the application to the message
* broker. By default messages from the application to the message broker are sent * broker. By default messages from the application to the message broker are sent
@ -122,24 +149,14 @@ public class MessageBrokerRegistry {
return this.brokerChannelRegistration; return this.brokerChannelRegistration;
} }
/**
* Customize the default separator used for destination patterns matching/combining.
* It can be used to configure "." as the default separator, since it is used in most
* STOMP broker relay, enabling destination patterns like "/topic/PRICE.STOCK.**".
* <p>The default separator is "/".
*/
public MessageBrokerRegistry defaultSeparator(String defaultSeparator) {
this.defaultSeparator = defaultSeparator;
return this;
}
protected SimpleBrokerMessageHandler getSimpleBroker(SubscribableChannel brokerChannel) { protected SimpleBrokerMessageHandler getSimpleBroker(SubscribableChannel brokerChannel) {
if ((this.simpleBrokerRegistration == null) && (this.brokerRelayRegistration == null)) { if ((this.simpleBrokerRegistration == null) && (this.brokerRelayRegistration == null)) {
enableSimpleBroker(); enableSimpleBroker();
} }
if (this.simpleBrokerRegistration != null) { if (this.simpleBrokerRegistration != null) {
AntPathMatcher pathMatcher = new AntPathMatcher(this.defaultSeparator); SimpleBrokerMessageHandler handler = this.simpleBrokerRegistration.getMessageHandler(brokerChannel);
return this.simpleBrokerRegistration.getMessageHandler(brokerChannel, pathMatcher); handler.setPathMatcher(this.pathMatcher);
return handler;
} }
return null; return null;
} }
@ -160,12 +177,12 @@ public class MessageBrokerRegistry {
return this.userDestinationPrefix; return this.userDestinationPrefix;
} }
protected PathMatcher getPathMatcher() {
return this.pathMatcher;
}
protected ChannelRegistration getBrokerChannelRegistration() { protected ChannelRegistration getBrokerChannelRegistration() {
return this.brokerChannelRegistration; return this.brokerChannelRegistration;
} }
protected String getDefaultSeparator() {
return this.defaultSeparator;
}
} }

View File

@ -19,28 +19,25 @@ package org.springframework.messaging.simp.config;
import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.util.PathMatcher;
/** /**
* Registration class for configuring a {@link SimpleBrokerMessageHandler}. * Registration class for configuring a {@link SimpleBrokerMessageHandler}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 4.0 * @since 4.0
*/ */
public class SimpleBrokerRegistration extends AbstractBrokerRegistration { public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
MessageChannel clientOutboundChannel, String[] destinationPrefixes) { super(inChannel, outChannel, prefixes);
super(clientInboundChannel, clientOutboundChannel, destinationPrefixes);
} }
protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel, PathMatcher pathMatcher) { @Override
protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
return new SimpleBrokerMessageHandler(getClientInboundChannel(), return new SimpleBrokerMessageHandler(getClientInboundChannel(),
getClientOutboundChannel(), brokerChannel, getDestinationPrefixes(), pathMatcher); getClientOutboundChannel(), brokerChannel, getDestinationPrefixes());
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2012 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,42 +18,34 @@ package org.springframework.messaging.handler;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder; import org.springframework.messaging.support.MessageBuilder;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runners.Parameterized.Parameter;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import java.util.Arrays;
import static org.junit.Assert.*; import static org.junit.Assert.*;
/** /**
* Unit tests for {@link DestinationPatternsMessageCondition}. * Unit tests for {@link DestinationPatternsMessageCondition}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/ */
@RunWith(Parameterized.class)
public class DestinationPatternsMessageConditionTests { public class DestinationPatternsMessageConditionTests {
@Parameter(0)
public String pathSeparator;
@Parameters
public static Iterable<Object[]> arguments() {
return Arrays.asList(new Object[][]{{"/"}, {"."}});
}
@Test @Test
public void prependSlash() { public void prependSlash() {
DestinationPatternsMessageCondition c = condition("foo"); DestinationPatternsMessageCondition c = condition("foo");
assertEquals("/foo", c.getPatterns().iterator().next()); assertEquals("/foo", c.getPatterns().iterator().next());
} }
@Test
public void prependSlashWithCustomPathSeparator() {
DestinationPatternsMessageCondition c =
new DestinationPatternsMessageCondition(new String[] {"foo"}, new AntPathMatcher("."));
assertEquals("Pre-pending should be disabled when not using '/' as path separator",
"foo", c.getPatterns().iterator().next());
}
// SPR-8255 // SPR-8255
@Test @Test
@ -65,21 +57,20 @@ public class DestinationPatternsMessageConditionTests {
@Test @Test
public void combineEmptySets() { public void combineEmptySets() {
DestinationPatternsMessageCondition c1 = condition(); DestinationPatternsMessageCondition c1 = condition();
DestinationPatternsMessageCondition c2 = suffixCondition(); DestinationPatternsMessageCondition c2 = condition();
assertEquals(condition(""), c1.combine(c2)); assertEquals(condition(""), c1.combine(c2));
} }
@Test @Test
public void combineOnePatternWithEmptySet() { public void combineOnePatternWithEmptySet() {
DestinationPatternsMessageCondition c1 = condition("/type1", DestinationPatternsMessageCondition c1 = condition("/type1", "/type2");
pathSeparator + "type2"); DestinationPatternsMessageCondition c2 = condition();
DestinationPatternsMessageCondition c2 = suffixCondition();
assertEquals(condition("/type1", pathSeparator + "type2"), c1.combine(c2)); assertEquals(condition("/type1", "/type2"), c1.combine(c2));
c1 = condition(); c1 = condition();
c2 = suffixCondition("/method1", "/method2"); c2 = condition("/method1", "/method2");
assertEquals(condition("/method1", "/method2"), c1.combine(c2)); assertEquals(condition("/method1", "/method2"), c1.combine(c2));
} }
@ -87,12 +78,10 @@ public class DestinationPatternsMessageConditionTests {
@Test @Test
public void combineMultiplePatterns() { public void combineMultiplePatterns() {
DestinationPatternsMessageCondition c1 = condition("/t1", "/t2"); DestinationPatternsMessageCondition c1 = condition("/t1", "/t2");
DestinationPatternsMessageCondition c2 = suffixCondition(pathSeparator + "m1", DestinationPatternsMessageCondition c2 = condition("/m1", "/m2");
pathSeparator + "m2");
assertEquals( assertEquals(new DestinationPatternsMessageCondition(
condition("/t1" + pathSeparator + "m1", "/t1" + pathSeparator + "m2", "/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2"), c1.combine(c2));
"/t2" + pathSeparator + "m1", "/t2" + pathSeparator + "m2"), c1.combine(c2));
} }
@Test @Test
@ -105,40 +94,35 @@ public class DestinationPatternsMessageConditionTests {
@Test @Test
public void matchPattern() { public void matchPattern() {
DestinationPatternsMessageCondition condition = condition( DestinationPatternsMessageCondition condition = condition("/foo/*");
"/foo" + pathSeparator + "*"); DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar"));
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo" + pathSeparator + "bar"));
assertNotNull(match); assertNotNull(match);
} }
@Test @Test
public void matchSortPatterns() { public void matchSortPatterns() {
DestinationPatternsMessageCondition condition = suffixCondition( DestinationPatternsMessageCondition condition = condition("/**", "/foo/bar", "/foo/*");
pathSeparator + "**", pathSeparator + "foo" + pathSeparator + "bar", DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar"));
pathSeparator + "foo" + pathSeparator + "*"); DestinationPatternsMessageCondition expected = condition("/foo/bar", "/foo/*", "/**");
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo(pathSeparator + "foo" + pathSeparator + "bar"));
DestinationPatternsMessageCondition expected = suffixCondition(
pathSeparator + "foo" + pathSeparator + "bar",
pathSeparator + "foo" + pathSeparator + "*", pathSeparator + "**");
assertEquals(expected, match); assertEquals(expected, match);
} }
@Test @Test
public void compareEqualPatterns() { public void compareEqualPatterns() {
DestinationPatternsMessageCondition c1 = suffixCondition(pathSeparator + "foo*"); DestinationPatternsMessageCondition c1 = condition("/foo*");
DestinationPatternsMessageCondition c2 = suffixCondition(pathSeparator + "foo*"); DestinationPatternsMessageCondition c2 = condition("/foo*");
assertEquals(0, c1.compareTo(c2, messageTo(pathSeparator + "foo"))); assertEquals(0, c1.compareTo(c2, messageTo("/foo")));
} }
@Test @Test
public void comparePatternSpecificity() { public void comparePatternSpecificity() {
DestinationPatternsMessageCondition c1 = suffixCondition(pathSeparator + "fo*"); DestinationPatternsMessageCondition c1 = condition("/fo*");
DestinationPatternsMessageCondition c2 = suffixCondition(pathSeparator + "foo"); DestinationPatternsMessageCondition c2 = condition("/foo");
assertEquals(1, c1.compareTo(c2, messageTo(pathSeparator + "foo"))); assertEquals(1, c1.compareTo(c2, messageTo("/foo")));
} }
@Test @Test
@ -156,11 +140,7 @@ public class DestinationPatternsMessageConditionTests {
private DestinationPatternsMessageCondition condition(String... patterns) { private DestinationPatternsMessageCondition condition(String... patterns) {
return new DestinationPatternsMessageCondition(patterns, new AntPathMatcher(this.pathSeparator)); return new DestinationPatternsMessageCondition(patterns);
}
private DestinationPatternsMessageCondition suffixCondition(String... patterns) {
return new DestinationPatternsMessageCondition(patterns, new AntPathMatcher(this.pathSeparator), false);
} }
private Message<?> messageTo(String destination) { private Message<?> messageTo(String destination) {

View File

@ -16,6 +16,8 @@
package org.springframework.messaging.simp.annotation.support; package org.springframework.messaging.simp.annotation.support;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -38,6 +40,7 @@ import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.support.MessageBuilder; import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -148,7 +151,7 @@ public class SimpAnnotationMethodMessageHandlerTests {
@Test @Test
public void simpScope() { public void simpScope() {
ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>(); Map<String, Object> map = new ConcurrentHashMap<>();
map.put("name", "value"); map.put("name", "value");
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setSessionId("session1"); headers.setSessionId("session1");
@ -160,6 +163,33 @@ public class SimpAnnotationMethodMessageHandlerTests {
assertEquals("scope", this.testController.method); assertEquals("scope", this.testController.method);
} }
@Test
public void dotPathSeparator() {
DotPathSeparatorController controller = new DotPathSeparatorController();
this.messageHandler.setPathMatcher(new AntPathMatcher("."));
this.messageHandler.registerHandler(controller);
this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/"));
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setSessionId("session1");
headers.setSessionAttributes(new HashMap<>());
headers.setDestination("/app1/pre.foo");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
assertEquals("handleFoo", controller.method);
headers = SimpMessageHeaderAccessor.create();
headers.setSessionId("session1");
headers.setSessionAttributes(new HashMap<>());
headers.setDestination("/app2/pre.foo");
message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
assertEquals("handleFoo", controller.method);
}
private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler { private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler {
@ -232,6 +262,20 @@ public class SimpAnnotationMethodMessageHandlerTests {
} }
} }
@Controller
@MessageMapping("pre")
private static class DotPathSeparatorController {
private String method;
@MessageMapping("foo")
public void handleFoo() {
this.method = "handleFoo";
}
}
private static class StringTestValidator implements Validator { private static class StringTestValidator implements Validator {
private final String invalidValue; private final String invalidValue;

View File

@ -79,7 +79,7 @@ public class MessageBrokerConfigurationTests {
private AnnotationConfigApplicationContext customChannelContext; private AnnotationConfigApplicationContext customChannelContext;
private AnnotationConfigApplicationContext customMatchingContext; private AnnotationConfigApplicationContext customPathMatcherContext;
@Before @Before
@ -101,9 +101,9 @@ public class MessageBrokerConfigurationTests {
this.customChannelContext.register(CustomChannelConfig.class); this.customChannelContext.register(CustomChannelConfig.class);
this.customChannelContext.refresh(); this.customChannelContext.refresh();
this.customMatchingContext = new AnnotationConfigApplicationContext(); this.customPathMatcherContext = new AnnotationConfigApplicationContext();
this.customMatchingContext.register(CustomMatchingSimpleBrokerConfig.class); this.customPathMatcherContext.register(CustomPathMatcherConfig.class);
this.customMatchingContext.refresh(); this.customPathMatcherContext.refresh();
} }
@ -407,17 +407,13 @@ public class MessageBrokerConfigurationTests {
} }
@Test @Test
public void customMatching() { public void customPathMatcher() {
SimpleBrokerMessageHandler brokerHandler = this.customMatchingContext.getBean(SimpleBrokerMessageHandler.class); SimpleBrokerMessageHandler broker = this.customPathMatcherContext.getBean(SimpleBrokerMessageHandler.class);
DefaultSubscriptionRegistry subscriptionRegistry = (DefaultSubscriptionRegistry)brokerHandler.getSubscriptionRegistry(); DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) broker.getSubscriptionRegistry();
AntPathMatcher pathMatcher = (AntPathMatcher)subscriptionRegistry.getPathMatcher(); assertEquals("a.a", registry.getPathMatcher().combine("a", "a"));
DirectFieldAccessor accessor = new DirectFieldAccessor(pathMatcher);
assertEquals(".", accessor.getPropertyValue("pathSeparator"));
SimpAnnotationMethodMessageHandler messageHandler = customMatchingContext.getBean(SimpAnnotationMethodMessageHandler.class); SimpAnnotationMethodMessageHandler handler = this.customPathMatcherContext.getBean(SimpAnnotationMethodMessageHandler.class);
pathMatcher = (AntPathMatcher)messageHandler.getPathMatcher(); assertEquals("a.a", handler.getPathMatcher().combine("a", "a"));
accessor = new DirectFieldAccessor(pathMatcher);
assertEquals(".", accessor.getPropertyValue("pathSeparator"));
} }
@ -504,11 +500,11 @@ public class MessageBrokerConfigurationTests {
} }
@Configuration @Configuration
static class CustomMatchingSimpleBrokerConfig extends SimpleBrokerConfig { static class CustomPathMatcherConfig extends SimpleBrokerConfig {
@Override @Override
public void configureMessageBroker(MessageBrokerRegistry registry) { public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.defaultSeparator(".").enableSimpleBroker("/topic", "/queue"); registry.setPathMatcher(new AntPathMatcher(".")).enableSimpleBroker("/topic", "/queue");
} }
} }

View File

@ -330,11 +330,11 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
if (simpleBrokerElem != null) { if (simpleBrokerElem != null) {
String prefix = simpleBrokerElem.getAttribute("prefix"); String prefix = simpleBrokerElem.getAttribute("prefix");
cavs.addIndexedArgumentValue(3, Arrays.asList(StringUtils.tokenizeToStringArray(prefix, ","))); cavs.addIndexedArgumentValue(3, Arrays.asList(StringUtils.tokenizeToStringArray(prefix, ",")));
String defaultSeparator = messageBrokerElement.getAttribute("default-separator");
if (!defaultSeparator.isEmpty()) {
cavs.addIndexedArgumentValue(4, new AntPathMatcher(defaultSeparator));
}
brokerDef = new RootBeanDefinition(SimpleBrokerMessageHandler.class, cavs, null); brokerDef = new RootBeanDefinition(SimpleBrokerMessageHandler.class, cavs, null);
if (messageBrokerElement.hasAttribute("path-matcher")) {
brokerDef.getPropertyValues().add("pathMatcher",
new RuntimeBeanReference(messageBrokerElement.getAttribute("path-matcher")));
}
} }
else if (brokerRelayElem != null) { else if (brokerRelayElem != null) {
String prefix = brokerRelayElem.getAttribute("prefix"); String prefix = brokerRelayElem.getAttribute("prefix");
@ -454,15 +454,13 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
mpvs.add("destinationPrefixes",Arrays.asList(StringUtils.tokenizeToStringArray(appDestPrefix, ","))); mpvs.add("destinationPrefixes",Arrays.asList(StringUtils.tokenizeToStringArray(appDestPrefix, ",")));
mpvs.add("messageConverter", brokerMessageConverterRef); mpvs.add("messageConverter", brokerMessageConverterRef);
RootBeanDefinition annotationMethodMessageHandlerDef = RootBeanDefinition beanDef = new RootBeanDefinition(SimpAnnotationMethodMessageHandler.class, cavs, mpvs);
new RootBeanDefinition(SimpAnnotationMethodMessageHandler.class, cavs, mpvs); if (messageBrokerElement.hasAttribute("path-matcher")) {
beanDef.getPropertyValues().add("pathMatcher",
String defaultSeparator = messageBrokerElement.getAttribute("default-separator"); new RuntimeBeanReference(messageBrokerElement.getAttribute("path-matcher")));
if (!defaultSeparator.isEmpty()) {
annotationMethodMessageHandlerDef.getPropertyValues().add("pathMatcher", new AntPathMatcher(defaultSeparator));
} }
registerBeanDef(annotationMethodMessageHandlerDef, parserCxt, source); registerBeanDef(beanDef, parserCxt, source);
} }
private RuntimeBeanReference registerUserDestinationResolver(Element messageBrokerElement, private RuntimeBeanReference registerUserDestinationResolver(Element messageBrokerElement,

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<!-- <!--
~ Copyright 2002-2013 the original author or authors. ~ Copyright 2002-2014 the original author or authors.
~ ~
~ Licensed under the Apache License, Version 2.0 (the "License"); ~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License. ~ you may not use this file except in compliance with the License.
@ -19,12 +19,13 @@
<xsd:schema xmlns="http://www.springframework.org/schema/websocket" <xsd:schema xmlns="http://www.springframework.org/schema/websocket"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
targetNamespace="http://www.springframework.org/schema/websocket" targetNamespace="http://www.springframework.org/schema/websocket"
elementFormDefault="qualified" elementFormDefault="qualified"
attributeFormDefault="unqualified"> attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans" <xsd:import namespace="http://www.springframework.org/schema/beans" schemaLocation="http://www.springframework.org/schema/beans/spring-beans-4.1.xsd"/>
schemaLocation="http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"/> <xsd:import namespace="http://www.springframework.org/schema/tool" schemaLocation="http://www.springframework.org/schema/tool/spring-tool-4.1.xsd" />
<xsd:complexType name="mapping"> <xsd:complexType name="mapping">
<xsd:annotation> <xsd:annotation>
@ -687,14 +688,31 @@
]]></xsd:documentation> ]]></xsd:documentation>
</xsd:annotation> </xsd:annotation>
</xsd:attribute> </xsd:attribute>
<xsd:attribute name="default-separator" type="xsd:string"> <xsd:attribute name="path-matcher" type="xsd:string">
<xsd:annotation> <xsd:annotation>
<xsd:documentation><![CDATA[ <xsd:documentation><![CDATA[
Customize the default separator used for destination patterns matching/combining. A reference to the PathMatcher to use to match the destinations of incoming
It could be used to configure "." as the default separator, since it is used in most messages to @MessageMapping and @SubscribeMapping methods.
STOMP broker relay, enabling destination patterns like "/topic/PRICE.STOCK.**".
The default separator is "/". By default AntPathMatcher is configured.
However applications may provide an AntPathMatcher instance
customized to use "." (commonly used in messaging) instead of "/" as path
separator or provide a completely different PathMatcher implementation.
Note that the configured PathMatcher is only used for matching the
portion of the destination after the configured prefix. For example given
application destination prefix "/app" and destination "/app/price.stock.**",
the message might be mapped to a controller with "price" and "stock.**"
as its type and method-level mappings respectively.
When the simple broker is enabled, the PathMatcher configured here is
also used to match message destinations when brokering messages.
]]></xsd:documentation> ]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java:org.springframework.util.PathMatcher" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation> </xsd:annotation>
</xsd:attribute> </xsd:attribute>
<xsd:attribute name="order" type="xsd:token"> <xsd:attribute name="order" type="xsd:token">

View File

@ -2,7 +2,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker> <websocket:message-broker>

View File

@ -2,7 +2,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker> <websocket:message-broker>

View File

@ -3,7 +3,7 @@
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation=" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker> <websocket:message-broker>
<websocket:stomp-endpoint path="/foo"/> <websocket:stomp-endpoint path="/foo"/>

View File

@ -2,7 +2,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app" user-destination-prefix="/personal"> <websocket:message-broker application-destination-prefix="/app" user-destination-prefix="/personal">
<websocket:stomp-endpoint path="/foo,/bar"> <websocket:stomp-endpoint path="/foo,/bar">

View File

@ -2,7 +2,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker order="2"> <websocket:message-broker order="2">
<websocket:stomp-endpoint path="/foo"> <websocket:stomp-endpoint path="/foo">

View File

@ -2,9 +2,11 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app" user-destination-prefix="/personal" default-separator="."> <websocket:message-broker application-destination-prefix="/app"
user-destination-prefix="/personal"
path-matcher="pathMatcher">
<!-- message-size=128*1024, send-buffer-size=1024*1024 --> <!-- message-size=128*1024, send-buffer-size=1024*1024 -->
<websocket:transport message-size="131072" send-timeout="25000" send-buffer-size="1048576" /> <websocket:transport message-size="131072" send-timeout="25000" send-buffer-size="1048576" />
@ -22,6 +24,10 @@
</websocket:message-broker> </websocket:message-broker>
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
<property name="pathSeparator" value="." />
</bean>
<bean id="myHandler" class="org.springframework.web.socket.config.TestHandshakeHandler"/> <bean id="myHandler" class="org.springframework.web.socket.config.TestHandshakeHandler"/>
</beans> </beans>

View File

@ -3,7 +3,7 @@
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation=" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers order="2"> <websocket:handlers order="2">
<websocket:mapping path="/foo" handler="fooHandler"/> <websocket:mapping path="/foo" handler="fooHandler"/>

View File

@ -3,7 +3,7 @@
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation=" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers > <websocket:handlers >
<websocket:mapping path="/test" handler="testHandler"/> <websocket:mapping path="/test" handler="testHandler"/>

View File

@ -3,7 +3,7 @@
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation=" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers> <websocket:handlers>
<websocket:mapping path="/test" handler="testHandler"/> <websocket:mapping path="/test" handler="testHandler"/>

View File

@ -3,7 +3,7 @@
xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation=" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd"> http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers> <websocket:handlers>
<websocket:mapping path="/foo,/bar" handler="fooHandler"/> <websocket:mapping path="/foo,/bar" handler="fooHandler"/>

View File

@ -37864,8 +37864,8 @@ options. The endpoint is available for clients to connect to at URL path `/app/p
@Override @Override
public void configureMessageBroker(MessageBrokerRegistry config) { public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app") config.setApplicationDestinationPrefixes("/app");
.enableSimpleBroker("/queue", "/topic"); config.enableSimpleBroker("/queue", "/topic");
} }
@Override @Override
@ -38001,7 +38001,7 @@ Below is a simple example to illustrate the flow of messages:
@Override @Override
public void configureMessageBroker(MessageBrokerRegistry registry) { public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app"); registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic/"); registry.enableSimpleBroker("/topic");
} }
} }
@ -38039,17 +38039,28 @@ kinds of arguments and return values supported.
[[websocket-stomp-handle-annotations]] [[websocket-stomp-handle-annotations]]
==== Annotation Message Handling ==== Annotation Message Handling
The `@MessageMapping` annotation is supported on methods of `@Controller` The `@MessageMapping` annotation is supported on methods of `@Controller` classes.
as well as on `@RestController`-annotated classes. It can be used for mapping methods to message destinations and can also be combined
It can be used for mapping methods to path-like message destinations. It is also with the type-level `@MessageMapping` for expressing shared mappings across all
possible to combine with a type-level `@MessageMapping` for expressing shared annotated methods within a controller.
mappings across all annotated methods within a controller.
Destination mappings can contain Ant-style patterns (e.g. "/foo*", "/foo/**") By default destination mappings are treated as Ant-style, slash-separated, path
and template variables (e.g. "/foo/{id}"), which can then be accessed via patterns, e.g. "/foo*", "/foo/**". etc. They can also contain template variables,
`@DestinationVariable` method arguments. This should be familiar to Spring MVC e.g. "/foo/{id}" that can then be referenced via `@DestinationVariable`-annotated
users, in fact the same `AntPathMatcher` is used for matching destinations based method arguments.
on patterns and for extracting template variables.
[NOTE]
====
Although Ant-style, slash-separated, path patterns should feel familiar to web
developers, in message brokers and in messaging it is common to use "." as the
separator, for example in the names of destinations such as topics, queues,
exchanges, etc.
Applications can switch to using "." (dot) instead of "/" (slash) as the separator
for destinations mapped to `@MessageMapping` methods simply by configuring an `AntPathMatcher`
with a customized path separator property. This can be done easily through
the provided Java config and XML namespace.
====
The following method arguments are supported for `@MessageMapping` methods: The following method arguments are supported for `@MessageMapping` methods:
@ -38137,6 +38148,21 @@ stores them in memory, and broadcasts messages to connected clients with matchin
destinations. The broker supports path-like destinations, including subscriptions destinations. The broker supports path-like destinations, including subscriptions
to Ant-style destination patterns. to Ant-style destination patterns.
[NOTE]
====
Although Ant-style, slash-separated, path patterns should feel familiar to web
developers, in message brokers and in messaging it is common to use "." as the
separator, for example in the names of destinations such as topics, queues,
exchanges, etc.
Applications can switch to using "." (dot) instead of "/" (slash) as the separator
for destinations handled by the broker simply by configuring an `AntPathMatcher`
with a customized path separator property. This can be done easily through
the provided Java config and XML namespace.
====
[[websocket-stomp-handle-broker-relay]] [[websocket-stomp-handle-broker-relay]]
==== Full-Featured Broker ==== Full-Featured Broker
@ -38168,7 +38194,7 @@ Below is example configuration that enables a full-featured broker:
@Override @Override
public void configureMessageBroker(MessageBrokerRegistry registry) { public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic/", "/queue/"); registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app"); registry.setApplicationDestinationPrefixes("/app");
} }