Skip to content

Commit 0fb31c5

Browse files
committed
Refine "." separator support for STOMP messaging
After this commit DefaultUserDestinationResolves no longer looks at whether AntPathMatcher is configured with "." as separator and rather expects to be explicitly told whether to keep the leading slash in translated destinations which actually depends on what the message broker supports (e.g. RabbitMQ "/", Artemis ".") or how it is configured (simple broker could be either way). There is also a minor improvement in SimpMessagingTemplate to ensure user destinations are correctly formed based on what the DefaultUserDestinationResolver expects. When using "." as separtor it allows sending messages to "queue.q1" rather than "/queue.q1". Issue: SPR-16275
1 parent 17f9b61 commit 0fb31c5

File tree

5 files changed

+174
-21
lines changed

5 files changed

+174
-21
lines changed

spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ public void convertAndSendToUser(String user, String destination, Object payload
225225

226226
Assert.notNull(user, "User must not be null");
227227
user = StringUtils.replace(user, "/", "%2F");
228+
destination = destination.startsWith("/") ? destination : "/" + destination;
228229
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
229230
}
230231

spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.messaging.simp.config;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collection;
2021
import java.util.HashMap;
2122
import java.util.List;
2223
import java.util.Map;
@@ -291,7 +292,18 @@ protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> retu
291292
@Bean
292293
public AbstractBrokerMessageHandler simpleBrokerMessageHandler() {
293294
SimpleBrokerMessageHandler handler = getBrokerRegistry().getSimpleBroker(brokerChannel());
294-
return (handler != null ? handler : new NoOpBrokerMessageHandler());
295+
if (handler == null) {
296+
return new NoOpBrokerMessageHandler();
297+
}
298+
updateUserDestinationResolver(handler);
299+
return handler;
300+
}
301+
302+
private void updateUserDestinationResolver(AbstractBrokerMessageHandler handler) {
303+
Collection<String> prefixes = handler.getDestinationPrefixes();
304+
if (!prefixes.isEmpty() && !prefixes.iterator().next().startsWith("/")) {
305+
((DefaultUserDestinationResolver) userDestinationResolver()).setRemoveLeadingSlash(true);
306+
}
295307
}
296308

297309
@Bean
@@ -310,6 +322,7 @@ public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler() {
310322
subscriptions.put(destination, userRegistryMessageHandler());
311323
}
312324
handler.setSystemSubscriptions(subscriptions);
325+
updateUserDestinationResolver(handler);
313326
return handler;
314327
}
315328

@@ -396,7 +409,6 @@ public UserDestinationResolver userDestinationResolver() {
396409
if (prefix != null) {
397410
resolver.setUserDestinationPrefix(prefix);
398411
}
399-
resolver.setPathMatcher(getPathMatcher());
400412
return resolver;
401413
}
402414

spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver {
5959

6060
private String prefix = "/user/";
6161

62-
private boolean keepLeadingSlash = true;
62+
private boolean removeLeadingSlash = false;
6363

6464

6565
/**
@@ -98,6 +98,29 @@ public String getDestinationPrefix() {
9898
return this.prefix;
9999
}
100100

101+
/**
102+
* Use this property to indicate whether the leading slash from translated
103+
* user destinations should be removed or not. This depends on the
104+
* destination prefixes the message broker is configured with.
105+
* <p>By default this is set to {@code false}, i.e. "do not change the
106+
* target destination", although
107+
* {@link org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration
108+
* AbstractMessageBrokerConfiguration} may change that to {@code true} if
109+
* the configured destinations do not have a leading slash.
110+
* @param remove whether to remove the leading slash
111+
* @since 4.3.14
112+
*/
113+
public void setRemoveLeadingSlash(boolean remove) {
114+
this.removeLeadingSlash = remove;
115+
}
116+
117+
/**
118+
* Whether to remove the leading slash from target destinations.
119+
*/
120+
public boolean isRemoveLeadingSlash() {
121+
return this.removeLeadingSlash;
122+
}
123+
101124
/**
102125
* Provide the {@code PathMatcher} in use for working with destinations
103126
* which in turn helps to determine whether the leading slash should be
@@ -111,11 +134,14 @@ public String getDestinationPrefix() {
111134
* jms.queue.position-updates}.
112135
* @param pathMatcher the PathMatcher used to work with destinations
113136
* @since 4.3
137+
* @deprecated as of 4.3.14 this property is no longer used and is replaced
138+
* by {@link #setRemoveLeadingSlash(boolean)} that indicates more explicitly
139+
* whether to keep the leading slash which may or may not be the case
140+
* regardless of how the {@code PathMatcher} is configured.
114141
*/
142+
@Deprecated
115143
public void setPathMatcher(@Nullable PathMatcher pathMatcher) {
116-
if (pathMatcher != null) {
117-
this.keepLeadingSlash = pathMatcher.combine("1", "2").equals("1/2");
118-
}
144+
// Do nothing
119145
}
120146

121147

@@ -171,7 +197,7 @@ private ParseResult parseSubscriptionMessage(Message<?> message, String sourceDe
171197
}
172198
int prefixEnd = this.prefix.length() - 1;
173199
String actualDestination = sourceDestination.substring(prefixEnd);
174-
if (!this.keepLeadingSlash) {
200+
if (isRemoveLeadingSlash()) {
175201
actualDestination = actualDestination.substring(1);
176202
}
177203
Principal principal = SimpMessageHeaderAccessor.getUser(headers);
@@ -199,7 +225,7 @@ private ParseResult parseMessage(MessageHeaders headers, String sourceDest) {
199225
sessionIds = getSessionIdsByUser(userName, sessionId);
200226
}
201227

202-
if (!this.keepLeadingSlash) {
228+
if (isRemoveLeadingSlash()) {
203229
actualDest = actualDest.substring(1);
204230
}
205231
return new ParseResult(sourceDest, actualDest, subscribeDest, sessionIds, userName);

spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,24 @@
1717
package org.springframework.messaging.simp.config;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collections;
2021
import java.util.Iterator;
2122
import java.util.List;
23+
import java.util.Map;
2224
import java.util.Set;
2325
import java.util.concurrent.ConcurrentHashMap;
2426

2527
import org.hamcrest.Matchers;
2628
import org.junit.Test;
2729

28-
import org.springframework.beans.DirectFieldAccessor;
2930
import org.springframework.context.ApplicationContext;
3031
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3132
import org.springframework.context.annotation.Bean;
3233
import org.springframework.context.annotation.Configuration;
3334
import org.springframework.context.support.StaticApplicationContext;
3435
import org.springframework.lang.Nullable;
3536
import org.springframework.messaging.Message;
37+
import org.springframework.messaging.MessageChannel;
3638
import org.springframework.messaging.MessageHandler;
3739
import org.springframework.messaging.converter.ByteArrayMessageConverter;
3840
import org.springframework.messaging.converter.CompositeMessageConverter;
@@ -45,7 +47,9 @@
4547
import org.springframework.messaging.handler.annotation.SendTo;
4648
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
4749
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
50+
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
4851
import org.springframework.messaging.simp.SimpMessageType;
52+
import org.springframework.messaging.simp.SimpMessagingTemplate;
4953
import org.springframework.messaging.simp.annotation.SubscribeMapping;
5054
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
5155
import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
@@ -414,7 +418,7 @@ public void customPathMatcher() {
414418

415419
DefaultUserDestinationResolver resolver = context.getBean(DefaultUserDestinationResolver.class);
416420
assertNotNull(resolver);
417-
assertEquals(false, new DirectFieldAccessor(resolver).getPropertyValue("keepLeadingSlash"));
421+
assertEquals(false, resolver.isRemoveLeadingSlash());
418422
}
419423

420424
@Test
@@ -462,6 +466,67 @@ public void userBroadcastsDisabledWithSimpleBroker() {
462466
assertNotEquals(UserRegistryMessageHandler.class, messageHandler.getClass());
463467
}
464468

469+
@Test // SPR-16275
470+
public void dotSeparatorWithBrokerSlashConvention() {
471+
ApplicationContext context = loadConfig(DotSeparatorWithSlashBrokerConventionConfig.class);
472+
testDotSeparator(context, true);
473+
}
474+
475+
@Test // SPR-16275
476+
public void dotSeparatorWithBrokerDotConvention() {
477+
ApplicationContext context = loadConfig(DotSeparatorWithDotBrokerConventionConfig.class);
478+
testDotSeparator(context, false);
479+
}
480+
481+
private void testDotSeparator(ApplicationContext context, boolean expectLeadingSlash) {
482+
MessageChannel inChannel = context.getBean("clientInboundChannel", MessageChannel.class);
483+
TestChannel outChannel = context.getBean("clientOutboundChannel", TestChannel.class);
484+
MessageChannel brokerChannel = context.getBean("brokerChannel", MessageChannel.class);
485+
486+
487+
// 1. Subscribe to user destination
488+
489+
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
490+
headers.setSessionId("sess1");
491+
headers.setSubscriptionId("subs1");
492+
headers.setDestination("/user/queue.q1");
493+
Message<?> message = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
494+
inChannel.send(message);
495+
496+
// 2. Send message to user via inboundChannel
497+
498+
headers = StompHeaderAccessor.create(StompCommand.SEND);
499+
headers.setSessionId("sess1");
500+
headers.setDestination("/user/sess1/queue.q1");
501+
message = MessageBuilder.createMessage("123".getBytes(), headers.getMessageHeaders());
502+
inChannel.send(message);
503+
504+
assertEquals(1, outChannel.messages.size());
505+
Message<?> outputMessage = outChannel.messages.remove(0);
506+
headers = StompHeaderAccessor.wrap(outputMessage);
507+
508+
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
509+
assertEquals(expectLeadingSlash ? "/queue.q1-usersess1" : "queue.q1-usersess1", headers.getDestination());
510+
assertEquals("123", new String((byte[]) outputMessage.getPayload()));
511+
512+
513+
// 3. Send message via broker channel
514+
515+
SimpMessagingTemplate template = new SimpMessagingTemplate(brokerChannel);
516+
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
517+
accessor.setSessionId("sess1");
518+
template.convertAndSendToUser("sess1", "queue.q1", "456".getBytes(), accessor.getMessageHeaders());
519+
520+
assertEquals(1, outChannel.messages.size());
521+
outputMessage = outChannel.messages.remove(0);
522+
headers = StompHeaderAccessor.wrap(outputMessage);
523+
524+
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
525+
assertEquals(expectLeadingSlash ? "/queue.q1-usersess1" : "queue.q1-usersess1", headers.getDestination());
526+
assertEquals("456", new String((byte[]) outputMessage.getPayload()));
527+
528+
}
529+
465530
private AnnotationConfigApplicationContext loadConfig(Class<?> configClass) {
466531
return new AnnotationConfigApplicationContext(configClass);
467532
}
@@ -578,6 +643,60 @@ protected void configureMessageBroker(MessageBrokerRegistry registry) {
578643
}
579644

580645

646+
@Configuration
647+
static abstract class BaseDotSeparatorConfig extends BaseTestMessageBrokerConfig {
648+
649+
@Override
650+
protected void configureMessageBroker(MessageBrokerRegistry registry) {
651+
registry.setPathMatcher(new AntPathMatcher("."));
652+
}
653+
654+
@Override
655+
@Bean
656+
public AbstractSubscribableChannel clientInboundChannel() {
657+
// synchronous
658+
return new ExecutorSubscribableChannel(null);
659+
}
660+
661+
@Override
662+
@Bean
663+
public AbstractSubscribableChannel clientOutboundChannel() {
664+
return new TestChannel();
665+
}
666+
667+
@Override
668+
@Bean
669+
public AbstractSubscribableChannel brokerChannel() {
670+
// synchronous
671+
return new ExecutorSubscribableChannel(null);
672+
}
673+
}
674+
675+
@Configuration
676+
static class DotSeparatorWithSlashBrokerConventionConfig extends BaseDotSeparatorConfig {
677+
678+
// RabbitMQ-style broker convention for STOMP destinations
679+
680+
@Override
681+
protected void configureMessageBroker(MessageBrokerRegistry registry) {
682+
super.configureMessageBroker(registry);
683+
registry.enableSimpleBroker("/topic", "/queue");
684+
}
685+
}
686+
687+
@Configuration
688+
static class DotSeparatorWithDotBrokerConventionConfig extends BaseDotSeparatorConfig {
689+
690+
// Artemis-style broker convention for STOMP destinations
691+
692+
@Override
693+
protected void configureMessageBroker(MessageBrokerRegistry registry) {
694+
super.configureMessageBroker(registry);
695+
registry.enableSimpleBroker("topic.", "queue.");
696+
}
697+
}
698+
699+
581700
private static class TestChannel extends ExecutorSubscribableChannel {
582701

583702
private final List<Message<?>> messages = new ArrayList<>();

spring-messaging/src/test/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolverTests.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,6 @@
1616

1717
package org.springframework.messaging.simp.user;
1818

19-
import static org.junit.Assert.*;
20-
import static org.mockito.Mockito.*;
21-
2219
import java.security.Principal;
2320

2421
import org.junit.Before;
@@ -29,9 +26,11 @@
2926
import org.springframework.messaging.simp.SimpMessageType;
3027
import org.springframework.messaging.simp.TestPrincipal;
3128
import org.springframework.messaging.support.MessageBuilder;
32-
import org.springframework.util.AntPathMatcher;
3329
import org.springframework.util.StringUtils;
3430

31+
import static org.junit.Assert.*;
32+
import static org.mockito.Mockito.*;
33+
3534
/**
3635
* Unit tests for
3736
* {@link org.springframework.messaging.simp.user.DefaultUserDestinationResolver}.
@@ -73,9 +72,7 @@ public void handleSubscribe() {
7372

7473
@Test // SPR-14044
7574
public void handleSubscribeForDestinationWithoutLeadingSlash() {
76-
AntPathMatcher pathMatcher = new AntPathMatcher();
77-
pathMatcher.setPathSeparator(".");
78-
this.resolver.setPathMatcher(pathMatcher);
75+
this.resolver.setRemoveLeadingSlash(true);
7976

8077
TestPrincipal user = new TestPrincipal("joe");
8178
String destination = "/user/jms.queue.call";
@@ -141,9 +138,7 @@ public void handleMessage() {
141138

142139
@Test // SPR-14044
143140
public void handleMessageForDestinationWithDotSeparator() {
144-
AntPathMatcher pathMatcher = new AntPathMatcher();
145-
pathMatcher.setPathSeparator(".");
146-
this.resolver.setPathMatcher(pathMatcher);
141+
this.resolver.setRemoveLeadingSlash(true);
147142

148143
TestPrincipal user = new TestPrincipal("joe");
149144
String destination = "/user/joe/jms.queue.call";

0 commit comments

Comments
 (0)