diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java index dfbab72e00..22c2f4d888 100644 --- a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -68,30 +68,35 @@ public class AntPathMatcher implements PathMatcher { final Map stringMatcherCache = new ConcurrentHashMap(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() { } /** - * Create a new AntPathMatcher. - * @param pathSeparator the path separator to use + * A convenience alternative constructor to use with a custom path separator. + * @param pathSeparator the path separator to use, must not be {@code null}. * @since 4.1 */ public AntPathMatcher(String pathSeparator) { - if(pathSeparator != null) { - this.pathSeparator = pathSeparator; - } + Assert.notNull(pathSeparator, "'pathSeparator' is required"); + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); } + /** * Set the path separator to use for pattern parsing. * Default is "/", as in Ant. */ public void setPathSeparator(String pathSeparator) { 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 - if (pattern1.endsWith(this.pathSeparator + "*")) { - return separatorConcat(pattern1.substring(0, pattern1.length() - 2), pattern2); + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); } // /hotels/** + /booking -> /hotels/**/booking // /hotels/** + booking -> /hotels/**/booking - if (pattern1.endsWith(this.pathSeparator + "**")) { - return separatorConcat(pattern1, pattern2); + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { + return concat(pattern1, pattern2); } int starDotPos1 = pattern1.indexOf("*."); if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { // simply concatenate the two patterns - return separatorConcat(pattern1, pattern2); + return concat(pattern1, pattern2); } String extension1 = pattern1.substring(starDotPos1 + 1); int dotPos2 = pattern2.indexOf('.'); @@ -470,7 +475,7 @@ public class AntPathMatcher implements PathMatcher { 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)) { 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; + } + } + } diff --git a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java index 3f678ec3fd..03bf312c32 100644 --- a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java +++ b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java @@ -607,9 +607,10 @@ public class AntPathMatcherTests { } @Test - public void noExtensionHandlingWithDotSeparator() { + public void testExtensionMappingWithDotPathSeparator() { 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.*")); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java index a3e3fe843d..7edad46585 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -55,51 +56,38 @@ public final class DestinationPatternsMessageCondition * @param patterns 0 or more URL patterns; if 0 the condition will match to every request. */ 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 pathMatcher the customized path matcher to use with patterns + * @param pathMatcher the PathMatcher to use. */ public DestinationPatternsMessageCondition(String[] patterns, PathMatcher pathMatcher) { - this(asList(patterns), pathMatcher, true); + this(asList(patterns), 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 patterns, - PathMatcher pathMatcher, boolean prependLeadingSlash) { - this.patterns = Collections.unmodifiableSet(initializePatterns(patterns, prependLeadingSlash)); + private DestinationPatternsMessageCondition(Collection patterns, PathMatcher pathMatcher) { this.pathMatcher = (pathMatcher != null) ? pathMatcher : new AntPathMatcher(); + this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns, this.pathMatcher)); } private static List asList(String... patterns) { return patterns != null ? Arrays.asList(patterns) : Collections.emptyList(); } - private static Set initializePatterns(Collection patterns, - boolean prependLeadingSlash) { + private static Set prependLeadingSlash(Collection patterns, PathMatcher pathMatcher) { if (patterns == null) { return Collections.emptySet(); } + boolean slashSeparator = pathMatcher.combine("a", "a").equals("a/a"); Set result = new LinkedHashSet(patterns.size()); for (String pattern : patterns) { - if (StringUtils.hasLength(pattern) && !pattern.startsWith("/") && prependLeadingSlash) { - pattern = "/" + pattern; + if (slashSeparator) { + if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { + pattern = "/" + pattern; + } } result.add(pattern); } @@ -149,7 +137,7 @@ public final class DestinationPatternsMessageCondition else { 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)); - return new DestinationPatternsMessageCondition(matches, this.pathMatcher, false); + return new DestinationPatternsMessageCondition(matches, this.pathMatcher); } /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java index 89a3922779..112bbb6b80 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java @@ -90,27 +90,27 @@ public abstract class AbstractMethodMessageHandler /** - * Configure one or more prefixes to match to the destinations of handled messages. - * Messages whose destination does not start with one of the configured prefixes - * are ignored. When a destination matches one of the configured prefixes, the - * matching part is removed from destination before performing a lookup for a matching - * message handling method. Prefixes without a trailing slash will have one appended - * automatically. - *

By default the list of prefixes is empty in which case all destinations match. + * When this property is configured only messages to destinations matching + * one of the configured prefixes are eligible for handling. When there is a + * match the prefix is removed and only the remaining part of the destination + * is used for method-mapping purposes. + * + *

By default no prefixes are configured in which case all messages are + * eligible for handling. */ public void setDestinationPrefixes(Collection prefixes) { this.destinationPrefixes.clear(); if (prefixes != null) { for (String prefix : prefixes) { prefix = prefix.trim(); - if (!prefix.endsWith("/")) { - prefix += "/"; - } this.destinationPrefixes.add(prefix); } } } + /** + * Return the configured destination prefixes. + */ public Collection getDestinationPrefixes() { return this.destinationPrefixes; } @@ -346,11 +346,11 @@ public abstract class AbstractMethodMessageHandler protected abstract String getDestination(Message message); /** - * Find if the given destination matches any of the configured allowed destination - * prefixes and if a match is found return the destination with the prefix removed. - *

If no destination prefixes are configured, the destination is returned as is. - * @return the destination to use to find matching message handling methods - * or {@code null} if the destination does not match + * Check whether the given destination (of an incoming message) matches to + * one of the configured destination prefixes and if so return the remaining + * portion of the destination after the matched prefix. + *

If there are no matching prefixes, return {@code null}. + *

If there are no destination prefixes, return the destination as is. */ protected String getLookupDestination(String destination) { if (destination == null) { @@ -361,7 +361,7 @@ public abstract class AbstractMethodMessageHandler } for (String prefix : this.destinationPrefixes) { if (destination.startsWith(prefix)) { - return destination.substring(prefix.length() - 1); + return destination.substring(prefix.length()); } } return null; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java index f6a4600cfc..8fa8cd01c5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java @@ -93,7 +93,9 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan private ConversionService conversionService = new DefaultFormattingConversionService(); - private PathMatcher pathMatcher; + private PathMatcher pathMatcher = new AntPathMatcher(); + + private boolean slashPathSeparator = true; private Validator validator; @@ -112,22 +114,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan * @param brokerTemplate a messaging template to send application messages to the broker */ public SimpAnnotationMethodMessageHandler(SubscribableChannel clientInboundChannel, - 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) { + MessageChannel clientOutboundChannel, SimpMessageSendingOperations brokerTemplate) { Assert.notNull(clientInboundChannel, "clientInboundChannel 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.clientMessagingTemplate = new SimpMessagingTemplate(clientOutboundChannel); this.brokerTemplate = brokerTemplate; - this.pathMatcher = new AntPathMatcher(pathSeparator); Collection converters = new ArrayList(); converters.add(new StringMessageConverter()); @@ -144,6 +130,36 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan this.messageConverter = new CompositeMessageConverter(converters); } + + /** + * {@inheritDoc} + *

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). + * + *

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 prefixes) { + super.setDestinationPrefixes(appendSlashes(prefixes)); + } + + private static Collection appendSlashes(Collection prefixes) { + if (CollectionUtils.isEmpty(prefixes)) { + return prefixes; + } + Collection result = new ArrayList(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 * 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) { Assert.notNull(pathMatcher, "PathMatcher must not be null"); 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 messageAnnot = AnnotationUtils.findAnnotation(method, MessageMapping.class); if (messageAnnot != null) { - SimpMessageMappingInfo result = createMessageMappingCondition(messageAnnot, typeAnnotation == null); + SimpMessageMappingInfo result = createMessageMappingCondition(messageAnnot); if (typeAnnotation != null) { - result = createMessageMappingCondition(typeAnnotation, false).combine(result); + result = createMessageMappingCondition(typeAnnotation).combine(result); } return result; } SubscribeMapping subsribeAnnotation = AnnotationUtils.findAnnotation(method, SubscribeMapping.class); if (subsribeAnnotation != null) { - SimpMessageMappingInfo result = createSubscribeCondition(subsribeAnnotation, typeAnnotation == null); + SimpMessageMappingInfo result = createSubscribeCondition(subsribeAnnotation); if (typeAnnotation != null) { - result = createMessageMappingCondition(typeAnnotation, false).combine(result); + result = createMessageMappingCondition(typeAnnotation).combine(result); } return result; } return null; } - private SimpMessageMappingInfo createMessageMappingCondition(MessageMapping annotation, boolean prependLeadingSlash) { + private SimpMessageMappingInfo createMessageMappingCondition(MessageMapping annotation) { 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, - new DestinationPatternsMessageCondition(annotation.value(), this.pathMatcher, prependLeadingSlash)); + new DestinationPatternsMessageCondition(annotation.value(), this.pathMatcher)); } @Override @@ -376,6 +393,27 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan 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 protected SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message message) { return mapping.getMatchingCondition(message); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java index 92b4bb0802..269c0058a9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java @@ -51,6 +51,8 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { private SubscriptionRegistry subscriptionRegistry; + private PathMatcher pathMatcher; + private MessageHeaderInitializer headerInitializer; @@ -58,38 +60,21 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { * Create a SimpleBrokerMessageHandler instance with the given message channels * and destination prefixes. * - * @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 inChannel the channel for receiving messages from 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 */ - public SimpleBrokerMessageHandler(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, + public SimpleBrokerMessageHandler(SubscribableChannel inChannel, MessageChannel outChannel, SubscribableChannel brokerChannel, Collection 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 destinationPrefixes, PathMatcher pathMatcher) { super(destinationPrefixes); - Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); - Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); + Assert.notNull(inChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(outChannel, "'clientOutboundChannel' must not be null"); Assert.notNull(brokerChannel, "'brokerChannel' must not be null"); - this.clientInboundChannel = clientInboundChannel; - this.clientOutboundChannel = clientOutboundChannel; + this.clientInboundChannel = inChannel; + this.clientOutboundChannel = outChannel; this.brokerChannel = brokerChannel; DefaultSubscriptionRegistry subscriptionRegistry = new DefaultSubscriptionRegistry(); - if(pathMatcher != null) { - subscriptionRegistry.setPathMatcher(pathMatcher); - } this.subscriptionRegistry = subscriptionRegistry; } @@ -106,15 +91,41 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { return this.brokerChannel; } + /** + * Configure a custom SubscriptionRegistry to use for storing subscriptions. + * + *

Note 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) { Assert.notNull(subscriptionRegistry, "SubscriptionRegistry must not be null"); 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() { 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 * messages sent to the client outbound channel. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index 843b1681df..8a085da3ea 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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"); * 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.SubscribableChannel; +import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler; import org.springframework.util.Assert; /** @@ -66,4 +67,6 @@ public abstract class AbstractBrokerRegistration { return this.destinationPrefixes; } + protected abstract AbstractBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel); + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java index 9661ca4e0f..e5c0c20458 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java @@ -39,9 +39,9 @@ import org.springframework.messaging.simp.user.UserSessionRegistry; import org.springframework.messaging.support.AbstractSubscribableChannel; import org.springframework.messaging.support.ExecutorSubscribableChannel; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import org.springframework.util.AntPathMatcher; import org.springframework.util.ClassUtils; import org.springframework.util.MimeTypeUtils; +import org.springframework.util.PathMatcher; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @@ -206,17 +206,17 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC @Bean public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() { - - String defaultSeparator = this.getBrokerRegistry().getDefaultSeparator(); SimpAnnotationMethodMessageHandler handler = new SimpAnnotationMethodMessageHandler( - clientInboundChannel(), clientOutboundChannel(), brokerMessagingTemplate(), defaultSeparator); + clientInboundChannel(), clientOutboundChannel(), brokerMessagingTemplate()); handler.setDestinationPrefixes(getBrokerRegistry().getApplicationDestinationPrefixes()); handler.setMessageConverter(brokerMessageConverter()); handler.setValidator(simpValidator()); - AntPathMatcher pathMatcher = new AntPathMatcher(defaultSeparator); - handler.setPathMatcher(pathMatcher); + PathMatcher pathMatcher = this.getBrokerRegistry().getPathMatcher(); + if (pathMatcher != null) { + handler.setPathMatcher(pathMatcher); + } return handler; } @@ -234,9 +234,7 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC @Bean public UserDestinationMessageHandler userDestinationMessageHandler() { - UserDestinationMessageHandler handler = new UserDestinationMessageHandler( - clientInboundChannel(), brokerChannel(), userDestinationResolver()); - return handler; + return new UserDestinationMessageHandler(clientInboundChannel(), brokerChannel(), userDestinationResolver()); } @Bean diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java index c1d43d108a..27165c887d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java @@ -25,6 +25,7 @@ import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; /** * A registry for configuring message broker options. @@ -47,9 +48,10 @@ public class MessageBrokerRegistry { private String userDestinationPrefix; + private PathMatcher pathMatcher; + private ChannelRegistration brokerChannelRegistration = new ChannelRegistration(); - private String defaultSeparator; public MessageBrokerRegistry(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel) { Assert.notNull(clientInboundChannel); @@ -111,6 +113,31 @@ public class MessageBrokerRegistry { return this; } + /** + * Configure the PathMatcher to use to match the destinations of incoming + * messages to {@code @MessageMapping} and {@code @SubscribeMapping} methods. + * + *

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. + * + *

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. + * + * @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 * broker. By default messages from the application to the message broker are sent @@ -122,24 +149,14 @@ public class MessageBrokerRegistry { 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.**". - *

The default separator is "/". - */ - public MessageBrokerRegistry defaultSeparator(String defaultSeparator) { - this.defaultSeparator = defaultSeparator; - return this; - } - protected SimpleBrokerMessageHandler getSimpleBroker(SubscribableChannel brokerChannel) { if ((this.simpleBrokerRegistration == null) && (this.brokerRelayRegistration == null)) { enableSimpleBroker(); } if (this.simpleBrokerRegistration != null) { - AntPathMatcher pathMatcher = new AntPathMatcher(this.defaultSeparator); - return this.simpleBrokerRegistration.getMessageHandler(brokerChannel, pathMatcher); + SimpleBrokerMessageHandler handler = this.simpleBrokerRegistration.getMessageHandler(brokerChannel); + handler.setPathMatcher(this.pathMatcher); + return handler; } return null; } @@ -160,12 +177,12 @@ public class MessageBrokerRegistry { return this.userDestinationPrefix; } + protected PathMatcher getPathMatcher() { + return this.pathMatcher; + } + protected ChannelRegistration getBrokerChannelRegistration() { return this.brokerChannelRegistration; } - protected String getDefaultSeparator() { - return this.defaultSeparator; - } - } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 5784fd9022..6ff9f3d62e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -19,28 +19,25 @@ package org.springframework.messaging.simp.config; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; -import org.springframework.util.PathMatcher; /** * Registration class for configuring a {@link SimpleBrokerMessageHandler}. * * @author Rossen Stoyanchev - * @author Sebastien Deleuze * @since 4.0 */ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { - public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, - MessageChannel clientOutboundChannel, String[] destinationPrefixes) { - - super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); + public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { + super(inChannel, outChannel, prefixes); } - protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel, PathMatcher pathMatcher) { + @Override + protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) { return new SimpleBrokerMessageHandler(getClientInboundChannel(), - getClientOutboundChannel(), brokerChannel, getDestinationPrefixes(), pathMatcher); + getClientOutboundChannel(), brokerChannel, getDestinationPrefixes()); } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/DestinationPatternsMessageConditionTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/DestinationPatternsMessageConditionTests.java index c18dc4deec..c56183c06e 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/DestinationPatternsMessageConditionTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/DestinationPatternsMessageConditionTests.java @@ -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"); * 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.runner.RunWith; -import org.junit.runners.Parameterized; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; - -import org.junit.runners.Parameterized.Parameters; -import org.junit.runners.Parameterized.Parameter; import org.springframework.util.AntPathMatcher; -import java.util.Arrays; - import static org.junit.Assert.*; /** * Unit tests for {@link DestinationPatternsMessageCondition}. * * @author Rossen Stoyanchev - * @author Sebastien Deleuze */ -@RunWith(Parameterized.class) public class DestinationPatternsMessageConditionTests { - @Parameter(0) - public String pathSeparator; - - @Parameters - public static Iterable arguments() { - return Arrays.asList(new Object[][]{{"/"}, {"."}}); - } - @Test public void prependSlash() { DestinationPatternsMessageCondition c = condition("foo"); 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 @Test @@ -65,21 +57,20 @@ public class DestinationPatternsMessageConditionTests { @Test public void combineEmptySets() { DestinationPatternsMessageCondition c1 = condition(); - DestinationPatternsMessageCondition c2 = suffixCondition(); + DestinationPatternsMessageCondition c2 = condition(); assertEquals(condition(""), c1.combine(c2)); } @Test public void combineOnePatternWithEmptySet() { - DestinationPatternsMessageCondition c1 = condition("/type1", - pathSeparator + "type2"); - DestinationPatternsMessageCondition c2 = suffixCondition(); + DestinationPatternsMessageCondition c1 = condition("/type1", "/type2"); + DestinationPatternsMessageCondition c2 = condition(); - assertEquals(condition("/type1", pathSeparator + "type2"), c1.combine(c2)); + assertEquals(condition("/type1", "/type2"), c1.combine(c2)); c1 = condition(); - c2 = suffixCondition("/method1", "/method2"); + c2 = condition("/method1", "/method2"); assertEquals(condition("/method1", "/method2"), c1.combine(c2)); } @@ -87,12 +78,10 @@ public class DestinationPatternsMessageConditionTests { @Test public void combineMultiplePatterns() { DestinationPatternsMessageCondition c1 = condition("/t1", "/t2"); - DestinationPatternsMessageCondition c2 = suffixCondition(pathSeparator + "m1", - pathSeparator + "m2"); + DestinationPatternsMessageCondition c2 = condition("/m1", "/m2"); - assertEquals( - condition("/t1" + pathSeparator + "m1", "/t1" + pathSeparator + "m2", - "/t2" + pathSeparator + "m1", "/t2" + pathSeparator + "m2"), c1.combine(c2)); + assertEquals(new DestinationPatternsMessageCondition( + "/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2"), c1.combine(c2)); } @Test @@ -105,40 +94,35 @@ public class DestinationPatternsMessageConditionTests { @Test public void matchPattern() { - DestinationPatternsMessageCondition condition = condition( - "/foo" + pathSeparator + "*"); - DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo" + pathSeparator + "bar")); + DestinationPatternsMessageCondition condition = condition("/foo/*"); + DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar")); assertNotNull(match); } @Test public void matchSortPatterns() { - DestinationPatternsMessageCondition condition = suffixCondition( - pathSeparator + "**", pathSeparator + "foo" + pathSeparator + "bar", - pathSeparator + "foo" + pathSeparator + "*"); - DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo(pathSeparator + "foo" + pathSeparator + "bar")); - DestinationPatternsMessageCondition expected = suffixCondition( - pathSeparator + "foo" + pathSeparator + "bar", - pathSeparator + "foo" + pathSeparator + "*", pathSeparator + "**"); + DestinationPatternsMessageCondition condition = condition("/**", "/foo/bar", "/foo/*"); + DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar")); + DestinationPatternsMessageCondition expected = condition("/foo/bar", "/foo/*", "/**"); assertEquals(expected, match); } @Test public void compareEqualPatterns() { - DestinationPatternsMessageCondition c1 = suffixCondition(pathSeparator + "foo*"); - DestinationPatternsMessageCondition c2 = suffixCondition(pathSeparator + "foo*"); + DestinationPatternsMessageCondition c1 = condition("/foo*"); + DestinationPatternsMessageCondition c2 = condition("/foo*"); - assertEquals(0, c1.compareTo(c2, messageTo(pathSeparator + "foo"))); + assertEquals(0, c1.compareTo(c2, messageTo("/foo"))); } @Test public void comparePatternSpecificity() { - DestinationPatternsMessageCondition c1 = suffixCondition(pathSeparator + "fo*"); - DestinationPatternsMessageCondition c2 = suffixCondition(pathSeparator + "foo"); + DestinationPatternsMessageCondition c1 = condition("/fo*"); + DestinationPatternsMessageCondition c2 = condition("/foo"); - assertEquals(1, c1.compareTo(c2, messageTo(pathSeparator + "foo"))); + assertEquals(1, c1.compareTo(c2, messageTo("/foo"))); } @Test @@ -156,11 +140,7 @@ public class DestinationPatternsMessageConditionTests { private DestinationPatternsMessageCondition condition(String... patterns) { - return new DestinationPatternsMessageCondition(patterns, new AntPathMatcher(this.pathSeparator)); - } - - private DestinationPatternsMessageCondition suffixCondition(String... patterns) { - return new DestinationPatternsMessageCondition(patterns, new AntPathMatcher(this.pathSeparator), false); + return new DestinationPatternsMessageCondition(patterns); } private Message messageTo(String destination) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java index 7aca52c7bd..8f27ab32cb 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java @@ -16,6 +16,8 @@ package org.springframework.messaging.simp.annotation.support; +import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; 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.support.MessageBuilder; import org.springframework.stereotype.Controller; +import org.springframework.util.AntPathMatcher; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; @@ -148,7 +151,7 @@ public class SimpAnnotationMethodMessageHandlerTests { @Test public void simpScope() { - ConcurrentHashMap map = new ConcurrentHashMap<>(); + Map map = new ConcurrentHashMap<>(); map.put("name", "value"); SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); headers.setSessionId("session1"); @@ -160,6 +163,33 @@ public class SimpAnnotationMethodMessageHandlerTests { 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 { @@ -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 final String invalidValue; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java index c3fdb9902e..6c27b69e36 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java @@ -79,7 +79,7 @@ public class MessageBrokerConfigurationTests { private AnnotationConfigApplicationContext customChannelContext; - private AnnotationConfigApplicationContext customMatchingContext; + private AnnotationConfigApplicationContext customPathMatcherContext; @Before @@ -101,9 +101,9 @@ public class MessageBrokerConfigurationTests { this.customChannelContext.register(CustomChannelConfig.class); this.customChannelContext.refresh(); - this.customMatchingContext = new AnnotationConfigApplicationContext(); - this.customMatchingContext.register(CustomMatchingSimpleBrokerConfig.class); - this.customMatchingContext.refresh(); + this.customPathMatcherContext = new AnnotationConfigApplicationContext(); + this.customPathMatcherContext.register(CustomPathMatcherConfig.class); + this.customPathMatcherContext.refresh(); } @@ -407,17 +407,13 @@ public class MessageBrokerConfigurationTests { } @Test - public void customMatching() { - SimpleBrokerMessageHandler brokerHandler = this.customMatchingContext.getBean(SimpleBrokerMessageHandler.class); - DefaultSubscriptionRegistry subscriptionRegistry = (DefaultSubscriptionRegistry)brokerHandler.getSubscriptionRegistry(); - AntPathMatcher pathMatcher = (AntPathMatcher)subscriptionRegistry.getPathMatcher(); - DirectFieldAccessor accessor = new DirectFieldAccessor(pathMatcher); - assertEquals(".", accessor.getPropertyValue("pathSeparator")); + public void customPathMatcher() { + SimpleBrokerMessageHandler broker = this.customPathMatcherContext.getBean(SimpleBrokerMessageHandler.class); + DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) broker.getSubscriptionRegistry(); + assertEquals("a.a", registry.getPathMatcher().combine("a", "a")); - SimpAnnotationMethodMessageHandler messageHandler = customMatchingContext.getBean(SimpAnnotationMethodMessageHandler.class); - pathMatcher = (AntPathMatcher)messageHandler.getPathMatcher(); - accessor = new DirectFieldAccessor(pathMatcher); - assertEquals(".", accessor.getPropertyValue("pathSeparator")); + SimpAnnotationMethodMessageHandler handler = this.customPathMatcherContext.getBean(SimpAnnotationMethodMessageHandler.class); + assertEquals("a.a", handler.getPathMatcher().combine("a", "a")); } @@ -504,11 +500,11 @@ public class MessageBrokerConfigurationTests { } @Configuration - static class CustomMatchingSimpleBrokerConfig extends SimpleBrokerConfig { + static class CustomPathMatcherConfig extends SimpleBrokerConfig { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.defaultSeparator(".").enableSimpleBroker("/topic", "/queue"); + registry.setPathMatcher(new AntPathMatcher(".")).enableSimpleBroker("/topic", "/queue"); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java index e35e6a632e..cb9456f160 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java @@ -330,11 +330,11 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser { if (simpleBrokerElem != null) { String prefix = simpleBrokerElem.getAttribute("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); + if (messageBrokerElement.hasAttribute("path-matcher")) { + brokerDef.getPropertyValues().add("pathMatcher", + new RuntimeBeanReference(messageBrokerElement.getAttribute("path-matcher"))); + } } else if (brokerRelayElem != null) { String prefix = brokerRelayElem.getAttribute("prefix"); @@ -454,15 +454,13 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser { mpvs.add("destinationPrefixes",Arrays.asList(StringUtils.tokenizeToStringArray(appDestPrefix, ","))); mpvs.add("messageConverter", brokerMessageConverterRef); - RootBeanDefinition annotationMethodMessageHandlerDef = - new RootBeanDefinition(SimpAnnotationMethodMessageHandler.class, cavs, mpvs); - - String defaultSeparator = messageBrokerElement.getAttribute("default-separator"); - if (!defaultSeparator.isEmpty()) { - annotationMethodMessageHandlerDef.getPropertyValues().add("pathMatcher", new AntPathMatcher(defaultSeparator)); + RootBeanDefinition beanDef = new RootBeanDefinition(SimpAnnotationMethodMessageHandler.class, cavs, mpvs); + if (messageBrokerElement.hasAttribute("path-matcher")) { + beanDef.getPropertyValues().add("pathMatcher", + new RuntimeBeanReference(messageBrokerElement.getAttribute("path-matcher"))); } - registerBeanDef(annotationMethodMessageHandlerDef, parserCxt, source); + registerBeanDef(beanDef, parserCxt, source); } private RuntimeBeanReference registerUserDestinationResolver(Element messageBrokerElement, diff --git a/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd b/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd index 70edfe0dd0..43bce0b891 100644 --- a/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd +++ b/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.1.xsd @@ -1,7 +1,7 @@ @@ -22,6 +24,10 @@ + + + + diff --git a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-attributes.xml b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-attributes.xml index dd5e4ea838..bbde4d4dce 100644 --- a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-attributes.xml +++ b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-attributes.xml @@ -3,7 +3,7 @@ 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 - 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"> diff --git a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml index 0c3ac67877..f40e41f670 100644 --- a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml +++ b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml @@ -3,7 +3,7 @@ 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 - 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"> diff --git a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs.xml b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs.xml index 05b6732a29..c39d33fd36 100644 --- a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs.xml +++ b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs.xml @@ -3,7 +3,7 @@ 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 - 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"> diff --git a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers.xml b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers.xml index 9217975776..9de9921e4c 100644 --- a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers.xml +++ b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers.xml @@ -3,7 +3,7 @@ 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 - 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"> diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 41aef153d5..f1ea0d3468 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -37864,8 +37864,8 @@ options. The endpoint is available for clients to connect to at URL path `/app/p @Override public void configureMessageBroker(MessageBrokerRegistry config) { - config.setApplicationDestinationPrefixes("/app") - .enableSimpleBroker("/queue", "/topic"); + config.setApplicationDestinationPrefixes("/app"); + config.enableSimpleBroker("/queue", "/topic"); } @Override @@ -38001,7 +38001,7 @@ Below is a simple example to illustrate the flow of messages: @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); - registry.enableSimpleBroker("/topic/"); + registry.enableSimpleBroker("/topic"); } } @@ -38039,17 +38039,28 @@ kinds of arguments and return values supported. [[websocket-stomp-handle-annotations]] ==== Annotation Message Handling -The `@MessageMapping` annotation is supported on methods of `@Controller` -as well as on `@RestController`-annotated classes. -It can be used for mapping methods to path-like message destinations. It is also -possible to combine with a type-level `@MessageMapping` for expressing shared -mappings across all annotated methods within a controller. +The `@MessageMapping` annotation is supported on methods of `@Controller` classes. +It can be used for mapping methods to message destinations and can also be combined +with the type-level `@MessageMapping` for expressing shared mappings across all +annotated methods within a controller. -Destination mappings can contain Ant-style patterns (e.g. "/foo*", "/foo/**") -and template variables (e.g. "/foo/{id}"), which can then be accessed via -`@DestinationVariable` method arguments. This should be familiar to Spring MVC -users, in fact the same `AntPathMatcher` is used for matching destinations based -on patterns and for extracting template variables. +By default destination mappings are treated as Ant-style, slash-separated, path +patterns, e.g. "/foo*", "/foo/**". etc. They can also contain template variables, +e.g. "/foo/{id}" that can then be referenced via `@DestinationVariable`-annotated +method arguments. + +[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: @@ -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 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]] ==== Full-Featured Broker @@ -38168,7 +38194,7 @@ Below is example configuration that enables a full-featured broker: @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableStompBrokerRelay("/topic/", "/queue/"); + registry.enableStompBrokerRelay("/topic", "/queue"); registry.setApplicationDestinationPrefixes("/app"); }