From 088b80f4c5673e13ab6f59840e45725f491a4960 Mon Sep 17 00:00:00 2001 From: Mark Galea Date: Fri, 14 Mar 2014 21:55:03 +0100 Subject: [PATCH 1/2] Add singleSession attribute to @SendToUser Added the ability to send a message only to one user session. Given a user has two tabs open and the client sends a message to the server from tab 1, it is now possible to send a reply message to only 1 tab instead of the default mode of targetting all known user sessions. Issue: SPR-11506 --- .../messaging/simp/annotation/SendToUser.java | 7 +++ .../SendToMethodReturnValueHandler.java | 7 ++- .../user/DefaultUserDestinationResolver.java | 7 ++- .../SendToMethodReturnValueHandlerTests.java | 62 +++++++++++++++++++ .../DefaultUserDestinationResolverTests.java | 2 +- 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java index d083baabf99..85d84f050fc 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java @@ -48,4 +48,11 @@ public @interface SendToUser { */ String[] value() default {}; + /** + * A flag indicating whether the message is to be sent to a particular user session. + * + */ + boolean singleSession() default false; + + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java index 962ee17a76a..11c38daaf2b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java @@ -148,7 +148,12 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH String user = getUserName(message, headers); String[] destinations = getTargetDestinations(sendToUser, message, this.defaultUserDestinationPrefix); for (String destination : destinations) { - this.messagingTemplate.convertAndSendToUser(user, destination, returnValue, createHeaders(sessionId)); + if (sendToUser.singleSession()) { + this.messagingTemplate.convertAndSendToUser(userName, destination, returnValue, createHeaders(sessionId)); + } + else } + this.messagingTemplate.convertAndSendToUser(userName, destination, returnValue); + } } return; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java index 375b9309ba2..55f010e4e7f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java @@ -158,7 +158,12 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver { subscribeDestination = this.destinationPrefix.substring(0, startIndex-1) + destinationWithoutPrefix; user = destination.substring(startIndex, endIndex); user = StringUtils.replace(user, "%2F", "/"); - sessionIds = this.userSessionRegistry.getSessionIds(user); + if (headers.getSessionId() == null){ + sessionIds = this.userSessionRegistry.getSessionIds(user); + } else { + sessionIds = Collections.singleton(headers.getSessionId()); + } + } else { if (logger.isTraceEnabled()) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java index 0173bc1ee37..c187458e922 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java @@ -74,7 +74,9 @@ public class SendToMethodReturnValueHandlerTests { private MethodParameter sendToReturnType; private MethodParameter sendToDefaultDestReturnType; private MethodParameter sendToUserReturnType; + private MethodParameter sendToUserSingleSessionReturnType; private MethodParameter sendToUserDefaultDestReturnType; + private MethodParameter sendToUserSingleSessionDefaultDestReturnType; @Before @@ -100,9 +102,15 @@ public class SendToMethodReturnValueHandlerTests { method = this.getClass().getDeclaredMethod("handleAndSendToUser"); this.sendToUserReturnType = new MethodParameter(method, -1); + + method = this.getClass().getDeclaredMethod("handleAndSendToUserSingleSession"); + this.sendToUserSingleSessionReturnType = new MethodParameter(method, -1); method = this.getClass().getDeclaredMethod("handleAndSendToUserDefaultDestination"); this.sendToUserDefaultDestReturnType = new MethodParameter(method, -1); + + method = this.getClass().getDeclaredMethod("handleAndSendToUserSingleSessionDefaultDestination"); + this.sendToUserSingleSessionDefaultDestReturnType = new MethodParameter(method, -1); } @@ -211,6 +219,31 @@ public class SendToMethodReturnValueHandlerTests { verify(this.messageChannel, times(2)).send(this.messageCaptor.capture()); + Message message = this.messageCaptor.getAllValues().get(0); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); + assertNull(headers.getSessionId()); + assertNull(headers.getSubscriptionId()); + assertEquals("/user/" + user.getName() + "/dest1", headers.getDestination()); + + message = this.messageCaptor.getAllValues().get(1); + headers = SimpMessageHeaderAccessor.wrap(message); + assertNull(headers.getSessionId()); + assertNull(headers.getSubscriptionId()); + assertEquals("/user/" + user.getName() + "/dest2", headers.getDestination()); + } + + @Test + public void sendToUserSingleSession() throws Exception { + + when(this.messageChannel.send(any(Message.class))).thenReturn(true); + + String sessionId = "sess1"; + TestUser user = new TestUser(); + Message inputMessage = createInputMessage(sessionId, "sub1", null, user); + this.handler.handleReturnValue(payloadContent, this.sendToUserSingleSessionReturnType, inputMessage); + + verify(this.messageChannel, times(2)).send(this.messageCaptor.capture()); + Message message = this.messageCaptor.getAllValues().get(0); SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); assertEquals(sessionId, headers.getSessionId()); @@ -257,6 +290,25 @@ public class SendToMethodReturnValueHandlerTests { verify(this.messageChannel, times(1)).send(this.messageCaptor.capture()); + Message message = this.messageCaptor.getAllValues().get(0); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); + assertNull(headers.getSessionId()); + assertNull(headers.getSubscriptionId()); + assertEquals("/user/" + user.getName() + "/queue/dest", headers.getDestination()); + } + + @Test + public void sendToUserDefaultDestinationSingleSession() throws Exception { + + when(this.messageChannel.send(any(Message.class))).thenReturn(true); + + String sessionId = "sess1"; + TestUser user = new TestUser(); + Message inputMessage = createInputMessage(sessionId, "sub1", "/dest", user); + this.handler.handleReturnValue(payloadContent, this.sendToUserSingleSessionDefaultDestReturnType, inputMessage); + + verify(this.messageChannel, times(1)).send(this.messageCaptor.capture()); + Message message = this.messageCaptor.getAllValues().get(0); SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); assertEquals(sessionId, headers.getSessionId()); @@ -342,10 +394,20 @@ public class SendToMethodReturnValueHandlerTests { public String handleAndSendToUserDefaultDestination() { return PAYLOAD; } + + @SendToUser(singleSession=true) + public String handleAndSendToUserSingleSessionDefaultDestination() { + return payloadContent; + } @SendToUser({"/dest1", "/dest2"}) public String handleAndSendToUser() { return PAYLOAD; } + + @SendToUser(value={"/dest1", "/dest2"}, singleSession=true) + public String handleAndSendToUserSingleSession() { + return payloadContent; + } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolverTests.java index 6a8496f554b..34f768d405c 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolverTests.java @@ -109,7 +109,7 @@ public class DefaultUserDestinationResolverTests { String userName = "http://joe.openid.example.org/"; this.registry.registerSessionId(userName, "openid123"); String destination = "/user/" + StringUtils.replace(userName, "/", "%2F") + "/queue/foo"; - Message message = createMessage(SimpMessageType.MESSAGE, this.user, SESSION_ID, destination); + Message message = createMessage(SimpMessageType.MESSAGE, this.user, null, destination); UserDestinationResult actual = this.resolver.resolveDestination(message); assertEquals(1, actual.getTargetDestinations().size()); From 9598a1e2ef3467187eec928248df536ed8410c2a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 1 May 2014 10:31:42 -0400 Subject: [PATCH 2/2] Update @SendToUser and related code Issue: SPR-11506 --- .../messaging/simp/annotation/SendToUser.java | 30 ++++++----- .../SendToMethodReturnValueHandler.java | 14 ++--- .../user/DefaultUserDestinationResolver.java | 10 ++-- .../SendToMethodReturnValueHandlerTests.java | 51 +++++++++---------- 4 files changed, 52 insertions(+), 53 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java index 85d84f050fc..278f87f48d8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SendToUser.java @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -26,17 +26,16 @@ import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; /** - * Annotation that can be used on methods processing an input message to indicate that the - * method's return value should be converted to a {@link Message} and sent to the - * specified destination with the prefix "/user/{username}" automatically - * prepended with the user information expected to be the input message header - * {@link SimpMessageHeaderAccessor#USER_HEADER}. Such user destinations may need to be - * further resolved to actual destinations. + * Annotation that indicates the return value of a message-handling method should + * be sent as a {@link org.springframework.messaging.Message} to the specified + * destination(s) prepended with {@code "/user/{username}"} where the user + * name is extracted from the headers of the input message being handled. * * @author Rossen Stoyanchev * @since 4.0 - * @see org.springframework.messaging.handler.annotation.SendTo + * @see org.springframework.messaging.simp.annotation.support.SendToMethodReturnValueHandler * @see org.springframework.messaging.simp.user.UserDestinationMessageHandler + * @see org.springframework.messaging.simp.SimpMessageHeaderAccessor#getUser() */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @@ -44,15 +43,20 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor; public @interface SendToUser { /** - * The destination for a message based on the return value of a method. + * One or more destinations to send a message to. If left unspecified, a + * default destination is selected based on the destination of the input + * message being handled. + * @see org.springframework.messaging.simp.annotation.support.SendToMethodReturnValueHandler */ String[] value() default {}; /** - * A flag indicating whether the message is to be sent to a particular user session. - * + * Whether messages should be sent to all sessions associated with the user + * or only to the session of the input message being handled. + * + *

By default this is set to {@code true} in which case messages are + * broadcast to all sessions. */ - boolean singleSession() default false; - + boolean broadcast() default true; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java index 11c38daaf2b..0aeedf1172d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java @@ -16,9 +16,6 @@ package org.springframework.messaging.simp.annotation.support; -import java.lang.annotation.Annotation; -import java.security.Principal; - import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.messaging.Message; @@ -36,6 +33,9 @@ import org.springframework.messaging.support.MessageHeaderInitializer; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import java.lang.annotation.Annotation; +import java.security.Principal; + /** * A {@link HandlerMethodReturnValueHandler} for sending to destinations specified in a * {@link SendTo} or {@link SendToUser} method-level annotations. @@ -148,11 +148,11 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH String user = getUserName(message, headers); String[] destinations = getTargetDestinations(sendToUser, message, this.defaultUserDestinationPrefix); for (String destination : destinations) { - if (sendToUser.singleSession()) { - this.messagingTemplate.convertAndSendToUser(userName, destination, returnValue, createHeaders(sessionId)); + if (sendToUser.broadcast()) { + this.messagingTemplate.convertAndSendToUser(user, destination, returnValue); } - else } - this.messagingTemplate.convertAndSendToUser(userName, destination, returnValue); + else { + this.messagingTemplate.convertAndSendToUser(user, destination, returnValue, createHeaders(sessionId)); } } return; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java index 55f010e4e7f..a4bbce711d1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java @@ -123,6 +123,7 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver { SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(headers); String destination = SimpMessageHeaderAccessor.getDestination(headers); Principal principal = SimpMessageHeaderAccessor.getUser(headers); + String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); String destinationWithoutPrefix; String subscribeDestination; @@ -137,7 +138,6 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver { logger.error("Ignoring message, no principal info available"); return null; } - String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); if (sessionId == null) { logger.error("Ignoring message, no session id available"); return null; @@ -158,12 +158,8 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver { subscribeDestination = this.destinationPrefix.substring(0, startIndex-1) + destinationWithoutPrefix; user = destination.substring(startIndex, endIndex); user = StringUtils.replace(user, "%2F", "/"); - if (headers.getSessionId() == null){ - sessionIds = this.userSessionRegistry.getSessionIds(user); - } else { - sessionIds = Collections.singleton(headers.getSessionId()); - } - + sessionIds = (sessionId != null ? + Collections.singleton(sessionId) : this.userSessionRegistry.getSessionIds(user)); } else { if (logger.isTraceEnabled()) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java index c187458e922..fcfcbdd2a92 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java @@ -102,14 +102,14 @@ public class SendToMethodReturnValueHandlerTests { method = this.getClass().getDeclaredMethod("handleAndSendToUser"); this.sendToUserReturnType = new MethodParameter(method, -1); - + method = this.getClass().getDeclaredMethod("handleAndSendToUserSingleSession"); this.sendToUserSingleSessionReturnType = new MethodParameter(method, -1); method = this.getClass().getDeclaredMethod("handleAndSendToUserDefaultDestination"); this.sendToUserDefaultDestReturnType = new MethodParameter(method, -1); - - method = this.getClass().getDeclaredMethod("handleAndSendToUserSingleSessionDefaultDestination"); + + method = this.getClass().getDeclaredMethod("handleAndSendToUserDefaultDestinationSingleSession"); this.sendToUserSingleSessionDefaultDestReturnType = new MethodParameter(method, -1); } @@ -231,7 +231,7 @@ public class SendToMethodReturnValueHandlerTests { assertNull(headers.getSubscriptionId()); assertEquals("/user/" + user.getName() + "/dest2", headers.getDestination()); } - + @Test public void sendToUserSingleSession() throws Exception { @@ -239,8 +239,8 @@ public class SendToMethodReturnValueHandlerTests { String sessionId = "sess1"; TestUser user = new TestUser(); - Message inputMessage = createInputMessage(sessionId, "sub1", null, user); - this.handler.handleReturnValue(payloadContent, this.sendToUserSingleSessionReturnType, inputMessage); + Message inputMessage = createInputMessage(sessionId, "sub1", null, null, user); + this.handler.handleReturnValue(PAYLOAD, this.sendToUserSingleSessionReturnType, inputMessage); verify(this.messageChannel, times(2)).send(this.messageCaptor.capture()); @@ -296,7 +296,7 @@ public class SendToMethodReturnValueHandlerTests { assertNull(headers.getSubscriptionId()); assertEquals("/user/" + user.getName() + "/queue/dest", headers.getDestination()); } - + @Test public void sendToUserDefaultDestinationSingleSession() throws Exception { @@ -304,8 +304,8 @@ public class SendToMethodReturnValueHandlerTests { String sessionId = "sess1"; TestUser user = new TestUser(); - Message inputMessage = createInputMessage(sessionId, "sub1", "/dest", user); - this.handler.handleReturnValue(payloadContent, this.sendToUserSingleSessionDefaultDestReturnType, inputMessage); + Message inputMessage = createInputMessage(sessionId, "sub1", "/app", "/dest", user); + this.handler.handleReturnValue(PAYLOAD, this.sendToUserSingleSessionDefaultDestReturnType, inputMessage); verify(this.messageChannel, times(1)).send(this.messageCaptor.capture()); @@ -328,16 +328,8 @@ public class SendToMethodReturnValueHandlerTests { handler.handleReturnValue(PAYLOAD, this.sendToUserDefaultDestReturnType, inputMessage); - ArgumentCaptor captor = ArgumentCaptor.forClass(MessageHeaders.class); - verify(messagingTemplate).convertAndSendToUser(eq("joe"), eq("/queue/dest"), eq(PAYLOAD), captor.capture()); - - SimpMessageHeaderAccessor headerAccessor = - MessageHeaderAccessor.getAccessor(captor.getValue(), SimpMessageHeaderAccessor.class); - - assertNotNull(headerAccessor); - assertTrue(headerAccessor.isMutable()); - assertEquals("sess1", headerAccessor.getSessionId()); - assertNull("Subscription id should not be copied", headerAccessor.getSubscriptionId()); + verify(messagingTemplate).convertAndSendToUser(eq("joe"), eq("/queue/dest"), eq(PAYLOAD)); + verifyNoMoreInteractions(messagingTemplate); } @@ -376,38 +368,45 @@ public class SendToMethodReturnValueHandlerTests { } } + @SuppressWarnings("unused") public String handleNoAnnotations() { return PAYLOAD; } + @SuppressWarnings("unused") @SendTo public String handleAndSendToDefaultDestination() { return PAYLOAD; } + @SuppressWarnings("unused") @SendTo({"/dest1", "/dest2"}) public String handleAndSendTo() { return PAYLOAD; } + @SuppressWarnings("unused") @SendToUser public String handleAndSendToUserDefaultDestination() { return PAYLOAD; } - - @SendToUser(singleSession=true) - public String handleAndSendToUserSingleSessionDefaultDestination() { - return payloadContent; + + @SuppressWarnings("unused") + @SendToUser(broadcast=false) + public String handleAndSendToUserDefaultDestinationSingleSession() { + return PAYLOAD; } + @SuppressWarnings("unused") @SendToUser({"/dest1", "/dest2"}) public String handleAndSendToUser() { return PAYLOAD; } - - @SendToUser(value={"/dest1", "/dest2"}, singleSession=true) + + @SuppressWarnings("unused") + @SendToUser(value={"/dest1", "/dest2"}, broadcast=false) public String handleAndSendToUserSingleSession() { - return payloadContent; + return PAYLOAD; } }