Add tests to spring-messaging

This commit is contained in:
Rossen Stoyanchev
2013-09-02 20:35:18 -04:00
parent d202573e1a
commit e1a46bb57a
13 changed files with 597 additions and 136 deletions

View File

@@ -31,10 +31,10 @@ import org.springframework.util.Assert;
/** /**
* A {@link HandlerMethodReturnValueHandler} for replying directly to a subscription. It * A {@link HandlerMethodReturnValueHandler} for replying directly to a subscription. It
* supports methods annotated with {@link SubscribeEvent} that do not also annotated with * supports methods annotated with {@link SubscribeEvent} unless they're also annotated
* neither {@link ReplyTo} nor {@link ReplyToUser}. * with {@link ReplyTo} or {@link ReplyToUser}.
* * <p>
* <p>The value returned from the method is converted, and turned to a {@link Message} and * The value returned from the method is converted, and turned to a {@link Message} and
* then enriched with the sessionId, subscriptionId, and destination of the input message. * then enriched with the sessionId, subscriptionId, and destination of the input message.
* The message is then sent directly back to the connected client. * The message is then sent directly back to the connected client.
* *
@@ -73,8 +73,7 @@ public class SubscriptionMethodReturnValueHandler implements HandlerMethodReturn
String destination = inputHeaders.getDestination(); String destination = inputHeaders.getDestination();
Assert.state(inputHeaders.getSubscriptionId() != null, Assert.state(inputHeaders.getSubscriptionId() != null,
"No subsriptiondId in input message. Add @ReplyTo or @ReplyToUser to method: " "No subsriptiondId in input message to method " + returnType.getMethod());
+ returnType.getMethod());
MessagePostProcessor postProcessor = new SubscriptionHeaderPostProcessor(sessionId, subscriptionId); MessagePostProcessor postProcessor = new SubscriptionHeaderPostProcessor(sessionId, subscriptionId);
this.messagingTemplate.convertAndSend(destination, returnValue, postProcessor); this.messagingTemplate.convertAndSend(destination, returnValue, postProcessor);

View File

@@ -30,8 +30,7 @@ import org.springframework.web.socket.sockjs.transport.handler.WebSocketTranspor
/** /**
* A helper class for configuring STOMP protocol handling over WebSocket * An abstract base class class for configuring STOMP over WebSocket/SockJS endpoints.
* with optional SockJS fallback options.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 4.0 * @since 4.0
@@ -46,33 +45,36 @@ public abstract class AbstractStompEndpointRegistration<M> implements StompEndpo
private StompSockJsServiceRegistration sockJsServiceRegistration; private StompSockJsServiceRegistration sockJsServiceRegistration;
private final TaskScheduler defaultSockJsTaskScheduler; private final TaskScheduler sockJsTaskScheduler;
public AbstractStompEndpointRegistration(String[] paths, SubProtocolWebSocketHandler webSocketHandler, public AbstractStompEndpointRegistration(String[] paths, SubProtocolWebSocketHandler webSocketHandler,
TaskScheduler defaultSockJsTaskScheduler) { TaskScheduler sockJsTaskScheduler) {
Assert.notEmpty(paths, "No paths specified"); Assert.notEmpty(paths, "No paths specified");
this.paths = paths; this.paths = paths;
this.wsHandler = webSocketHandler; this.wsHandler = webSocketHandler;
this.defaultSockJsTaskScheduler = defaultSockJsTaskScheduler; this.sockJsTaskScheduler = sockJsTaskScheduler;
} }
protected SubProtocolWebSocketHandler getWsHandler() { /**
return this.wsHandler; * Provide a custom or pre-configured {@link HandshakeHandler}. This property is
} * optional.
*/
@Override @Override
public StompEndpointRegistration setHandshakeHandler(HandshakeHandler handshakeHandler) { public StompEndpointRegistration setHandshakeHandler(HandshakeHandler handshakeHandler) {
this.handshakeHandler = handshakeHandler; this.handshakeHandler = handshakeHandler;
return this; return this;
} }
/**
* Enable SockJS fallback options.
*/
@Override @Override
public SockJsServiceRegistration withSockJS() { public SockJsServiceRegistration withSockJS() {
this.sockJsServiceRegistration = new StompSockJsServiceRegistration(this.defaultSockJsTaskScheduler); this.sockJsServiceRegistration = new StompSockJsServiceRegistration(this.sockJsTaskScheduler);
if (this.handshakeHandler != null) { if (this.handshakeHandler != null) {
WebSocketTransportHandler transportHandler = new WebSocketTransportHandler(this.handshakeHandler); WebSocketTransportHandler transportHandler = new WebSocketTransportHandler(this.handshakeHandler);
@@ -82,7 +84,7 @@ public abstract class AbstractStompEndpointRegistration<M> implements StompEndpo
return this.sockJsServiceRegistration; return this.sockJsServiceRegistration;
} }
protected M getMappings() { protected final M getMappings() {
M mappings = createMappings(); M mappings = createMappings();

View File

@@ -33,7 +33,7 @@ import reactor.util.Assert;
*/ */
public class MessageBrokerConfigurer { public class MessageBrokerConfigurer {
private final MessageChannel webSocketReplyChannel; private final MessageChannel webSocketResponseChannel;
private SimpleBrokerRegistration simpleBroker; private SimpleBrokerRegistration simpleBroker;
@@ -42,18 +42,18 @@ public class MessageBrokerConfigurer {
private String[] annotationMethodDestinationPrefixes; private String[] annotationMethodDestinationPrefixes;
public MessageBrokerConfigurer(MessageChannel webSocketReplyChannel) { public MessageBrokerConfigurer(MessageChannel webSocketResponseChannel) {
Assert.notNull(webSocketReplyChannel); Assert.notNull(webSocketResponseChannel);
this.webSocketReplyChannel = webSocketReplyChannel; this.webSocketResponseChannel = webSocketResponseChannel;
} }
public SimpleBrokerRegistration enableSimpleBroker(String... destinationPrefixes) { public SimpleBrokerRegistration enableSimpleBroker(String... destinationPrefixes) {
this.simpleBroker = new SimpleBrokerRegistration(this.webSocketReplyChannel, destinationPrefixes); this.simpleBroker = new SimpleBrokerRegistration(this.webSocketResponseChannel, destinationPrefixes);
return this.simpleBroker; return this.simpleBroker;
} }
public StompBrokerRelayRegistration enableStompBrokerRelay(String... destinationPrefixes) { public StompBrokerRelayRegistration enableStompBrokerRelay(String... destinationPrefixes) {
this.stompRelay = new StompBrokerRelayRegistration(this.webSocketReplyChannel, destinationPrefixes); this.stompRelay = new StompBrokerRelayRegistration(this.webSocketResponseChannel, destinationPrefixes);
return this.stompRelay; return this.stompRelay;
} }
@@ -69,7 +69,7 @@ public class MessageBrokerConfigurer {
protected void initSimpleBrokerIfNecessary() { protected void initSimpleBrokerIfNecessary() {
if ((this.simpleBroker == null) && (this.stompRelay == null)) { if ((this.simpleBroker == null) && (this.stompRelay == null)) {
this.simpleBroker = new SimpleBrokerRegistration(this.webSocketReplyChannel, null); this.simpleBroker = new SimpleBrokerRegistration(this.webSocketResponseChannel, null);
} }
} }

View File

@@ -37,6 +37,8 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
private String applicationPasscode = "guest"; private String applicationPasscode = "guest";
private boolean autoStartup = true;
public StompBrokerRelayRegistration(MessageChannel webSocketReplyChannel, String[] destinationPrefixes) { public StompBrokerRelayRegistration(MessageChannel webSocketReplyChannel, String[] destinationPrefixes) {
super(webSocketReplyChannel, destinationPrefixes); super(webSocketReplyChannel, destinationPrefixes);
@@ -52,13 +54,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
return this; return this;
} }
/**
* @return the STOMP message broker host.
*/
protected String getRelayHost() {
return this.relayHost;
}
/** /**
* Set the STOMP message broker port. * Set the STOMP message broker port.
*/ */
@@ -67,13 +62,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
return this; return this;
} }
/**
* @return the STOMP message broker port.
*/
protected int getRelayPort() {
return this.relayPort;
}
/** /**
* Set the login for a "system" TCP connection used to send messages to the STOMP * Set the login for a "system" TCP connection used to send messages to the STOMP
* broker without having a client session (e.g. REST/HTTP request handling method). * broker without having a client session (e.g. REST/HTTP request handling method).
@@ -84,13 +72,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
return this; return this;
} }
/**
* @return the login for a shared, "system" connection to the STOMP message broker.
*/
protected String getApplicationLogin() {
return this.applicationLogin;
}
/** /**
* Set the passcode for a "system" TCP connection used to send messages to the STOMP * Set the passcode for a "system" TCP connection used to send messages to the STOMP
* broker without having a client session (e.g. REST/HTTP request handling method). * broker without having a client session (e.g. REST/HTTP request handling method).
@@ -102,10 +83,14 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
} }
/** /**
* @return the passcode for a shared, "system" connection to the STOMP message broker. * Configure whether the {@link StompBrokerRelayMessageHandler} should start
* automatically when the Spring ApplicationContext is refreshed.
* <p>
* The default setting is {@code true}.
*/ */
protected String getApplicationPasscode() { public StompBrokerRelayRegistration setAutoStartup(boolean autoStartup) {
return this.applicationPasscode; this.autoStartup = autoStartup;
return this;
} }
@@ -116,6 +101,7 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
handler.setRelayPort(this.relayPort); handler.setRelayPort(this.relayPort);
handler.setSystemLogin(this.applicationLogin); handler.setSystemLogin(this.applicationLogin);
handler.setSystemPasscode(this.applicationPasscode); handler.setSystemPasscode(this.applicationPasscode);
handler.setAutoStartup(this.autoStartup);
return handler; return handler;
} }

View File

@@ -67,7 +67,7 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
@Bean @Bean
public SubProtocolWebSocketHandler subProtocolWebSocketHandler() { public SubProtocolWebSocketHandler subProtocolWebSocketHandler() {
SubProtocolWebSocketHandler wsHandler = new SubProtocolWebSocketHandler(webSocketRequestChannel()); SubProtocolWebSocketHandler wsHandler = new SubProtocolWebSocketHandler(webSocketRequestChannel());
webSocketReplyChannel().subscribe(wsHandler); webSocketResponseChannel().subscribe(wsHandler);
return wsHandler; return wsHandler;
} }
@@ -109,7 +109,7 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
} }
@Bean @Bean
public SubscribableChannel webSocketReplyChannel() { public SubscribableChannel webSocketResponseChannel() {
return new ExecutorSubscribableChannel(webSocketChannelExecutor()); return new ExecutorSubscribableChannel(webSocketChannelExecutor());
} }
@@ -125,7 +125,7 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
@Bean @Bean
public AnnotationMethodMessageHandler annotationMethodMessageHandler() { public AnnotationMethodMessageHandler annotationMethodMessageHandler() {
AnnotationMethodMessageHandler handler = AnnotationMethodMessageHandler handler =
new AnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketReplyChannel()); new AnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel());
handler.setDestinationPrefixes(getMessageBrokerConfigurer().getAnnotationMethodDestinationPrefixes()); handler.setDestinationPrefixes(getMessageBrokerConfigurer().getAnnotationMethodDestinationPrefixes());
handler.setMessageConverter(brokerMessageConverter()); handler.setMessageConverter(brokerMessageConverter());
webSocketRequestChannel().subscribe(handler); webSocketRequestChannel().subscribe(handler);
@@ -140,7 +140,7 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
} }
else { else {
webSocketRequestChannel().subscribe(handler); webSocketRequestChannel().subscribe(handler);
brokerMessageChannel().subscribe(handler); brokerChannel().subscribe(handler);
return handler; return handler;
} }
} }
@@ -153,14 +153,14 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
} }
else { else {
webSocketRequestChannel().subscribe(handler); webSocketRequestChannel().subscribe(handler);
brokerMessageChannel().subscribe(handler); brokerChannel().subscribe(handler);
return handler; return handler;
} }
} }
protected final MessageBrokerConfigurer getMessageBrokerConfigurer() { protected final MessageBrokerConfigurer getMessageBrokerConfigurer() {
if (this.messageBrokerConfigurer == null) { if (this.messageBrokerConfigurer == null) {
MessageBrokerConfigurer configurer = new MessageBrokerConfigurer(webSocketReplyChannel()); MessageBrokerConfigurer configurer = new MessageBrokerConfigurer(webSocketResponseChannel());
configureMessageBroker(configurer); configureMessageBroker(configurer);
this.messageBrokerConfigurer = configurer; this.messageBrokerConfigurer = configurer;
} }
@@ -175,19 +175,19 @@ public abstract class WebSocketMessageBrokerConfigurationSupport {
UserDestinationMessageHandler handler = new UserDestinationMessageHandler( UserDestinationMessageHandler handler = new UserDestinationMessageHandler(
brokerMessagingTemplate(), userQueueSuffixResolver()); brokerMessagingTemplate(), userQueueSuffixResolver());
webSocketRequestChannel().subscribe(handler); webSocketRequestChannel().subscribe(handler);
brokerMessageChannel().subscribe(handler); brokerChannel().subscribe(handler);
return handler; return handler;
} }
@Bean @Bean
public SimpMessageSendingOperations brokerMessagingTemplate() { public SimpMessageSendingOperations brokerMessagingTemplate() {
SimpMessagingTemplate template = new SimpMessagingTemplate(webSocketRequestChannel()); SimpMessagingTemplate template = new SimpMessagingTemplate(brokerChannel());
template.setMessageConverter(brokerMessageConverter()); template.setMessageConverter(brokerMessageConverter());
return template; return template;
} }
@Bean @Bean
public SubscribableChannel brokerMessageChannel() { public SubscribableChannel brokerChannel() {
return new ExecutorSubscribableChannel(); // synchronous return new ExecutorSubscribableChannel(); // synchronous
} }

View File

@@ -47,6 +47,8 @@ public abstract class AbstractBrokerMessageHandler
private AtomicBoolean brokerAvailable = new AtomicBoolean(false); private AtomicBoolean brokerAvailable = new AtomicBoolean(false);
private boolean autoStartup = true;
private Object lifecycleMonitor = new Object(); private Object lifecycleMonitor = new Object();
private volatile boolean running = false; private volatile boolean running = false;
@@ -71,9 +73,13 @@ public abstract class AbstractBrokerMessageHandler
return this.eventPublisher; return this.eventPublisher;
} }
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
@Override @Override
public boolean isAutoStartup() { public boolean isAutoStartup() {
return true; return this.autoStartup;
} }
@Override @Override

View File

@@ -78,7 +78,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
private final SimpMessageSendingOperations brokerTemplate; private final SimpMessageSendingOperations brokerTemplate;
private final SimpMessageSendingOperations webSocketReplyTemplate; private final SimpMessageSendingOperations webSocketResponseTemplate;
private Collection<String> destinationPrefixes; private Collection<String> destinationPrefixes;
@@ -106,15 +106,15 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
/** /**
* @param brokerTemplate a messaging template to sending messages to the broker * @param brokerTemplate a messaging template to sending messages to the broker
* @param webSocketReplyChannel the channel for messages to WebSocket clients * @param webSocketResponseChannel the channel for messages to WebSocket clients
*/ */
public AnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate, public AnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
MessageChannel webSocketReplyChannel) { MessageChannel webSocketResponseChannel) {
Assert.notNull(brokerTemplate, "brokerTemplate is required"); Assert.notNull(brokerTemplate, "brokerTemplate is required");
Assert.notNull(webSocketReplyChannel, "webSocketReplyChannel is required"); Assert.notNull(webSocketResponseChannel, "webSocketReplyChannel is required");
this.brokerTemplate = brokerTemplate; this.brokerTemplate = brokerTemplate;
this.webSocketReplyTemplate = new SimpMessagingTemplate(webSocketReplyChannel); this.webSocketResponseTemplate = new SimpMessagingTemplate(webSocketResponseChannel);
} }
@@ -129,7 +129,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
public void setMessageConverter(MessageConverter<?> converter) { public void setMessageConverter(MessageConverter<?> converter) {
this.messageConverter = converter; this.messageConverter = converter;
if (converter != null) { if (converter != null) {
((AbstractMessageSendingTemplate<?>) this.webSocketReplyTemplate).setMessageConverter(converter); ((AbstractMessageSendingTemplate<?>) this.webSocketResponseTemplate).setMessageConverter(converter);
} }
} }
@@ -181,7 +181,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
// Annotation-based return value types // Annotation-based return value types
this.returnValueHandlers.addHandler(new ReplyToMethodReturnValueHandler(this.brokerTemplate)); this.returnValueHandlers.addHandler(new ReplyToMethodReturnValueHandler(this.brokerTemplate));
this.returnValueHandlers.addHandler(new SubscriptionMethodReturnValueHandler(this.webSocketReplyTemplate)); this.returnValueHandlers.addHandler(new SubscriptionMethodReturnValueHandler(this.webSocketResponseTemplate));
// custom return value types // custom return value types
this.returnValueHandlers.addHandlers(this.customReturnValueHandlers); this.returnValueHandlers.addHandlers(this.customReturnValueHandlers);
@@ -221,14 +221,14 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati
final Class<A> annotationType, MappingInfoCreator<A> mappingInfoCreator, final Class<A> annotationType, MappingInfoCreator<A> mappingInfoCreator,
Map<MappingInfo, HandlerMethod> handlerMethods) { Map<MappingInfo, HandlerMethod> handlerMethods) {
Set<Method> messageMethods = HandlerMethodSelector.selectMethods(handlerType, new MethodFilter() { Set<Method> methods = HandlerMethodSelector.selectMethods(handlerType, new MethodFilter() {
@Override @Override
public boolean matches(Method method) { public boolean matches(Method method) {
return AnnotationUtils.findAnnotation(method, annotationType) != null; return AnnotationUtils.findAnnotation(method, annotationType) != null;
} }
}); });
for (Method method : messageMethods) { for (Method method : methods) {
A annotation = AnnotationUtils.findAnnotation(method, annotationType); A annotation = AnnotationUtils.findAnnotation(method, annotationType);
HandlerMethod hm = createHandlerMethod(handler, method); HandlerMethod hm = createHandlerMethod(handler, method);
handlerMethods.put(mappingInfoCreator.create(annotation), hm); handlerMethods.put(mappingInfoCreator.create(annotation), hm);

View File

@@ -127,4 +127,10 @@ public abstract class AbstractMessageChannel implements MessageChannel, BeanName
protected abstract boolean sendInternal(Message<?> message, long timeout); protected abstract boolean sendInternal(Message<?> message, long timeout);
@Override
public String toString() {
return "MessageChannel [name=" + this.beanName + "]";
}
} }

View File

@@ -54,7 +54,7 @@ public class AbstractStompEndpointRegistrationTests {
} }
@Test @Test
public void minimal() { public void minimalRegistration() {
TestStompEndpointRegistration registration = TestStompEndpointRegistration registration =
new TestStompEndpointRegistration(new String[] {"/foo"}, this.wsHandler, this.scheduler); new TestStompEndpointRegistration(new String[] {"/foo"}, this.wsHandler, this.scheduler);
@@ -68,7 +68,7 @@ public class AbstractStompEndpointRegistrationTests {
} }
@Test @Test
public void handshakeHandler() { public void customHandshakeHandler() {
DefaultHandshakeHandler handshakeHandler = new DefaultHandshakeHandler(); DefaultHandshakeHandler handshakeHandler = new DefaultHandshakeHandler();
@@ -86,7 +86,7 @@ public class AbstractStompEndpointRegistrationTests {
} }
@Test @Test
public void handshakeHandlerPassedToSockJsService() { public void customHandshakeHandlerPassedToSockJsService() {
DefaultHandshakeHandler handshakeHandler = new DefaultHandshakeHandler(); DefaultHandshakeHandler handshakeHandler = new DefaultHandshakeHandler();

View File

@@ -0,0 +1,90 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.simp.config;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.handler.websocket.SubProtocolHandler;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.handler.MutableUserQueueSuffixResolver;
import org.springframework.messaging.simp.handler.SimpleUserQueueSuffixResolver;
import org.springframework.messaging.simp.stomp.StompProtocolHandler;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import static org.junit.Assert.*;
/**
* Test fixture for {@link ServletStompEndpointRegistry}.
*
* @author Rossen Stoyanchev
*/
public class ServletStompEndpointRegistryTests {
private ServletStompEndpointRegistry registry;
private SubProtocolWebSocketHandler webSocketHandler;
private MutableUserQueueSuffixResolver queueSuffixResolver;
@Before
public void setup() {
MessageChannel channel = Mockito.mock(MessageChannel.class);
this.webSocketHandler = new SubProtocolWebSocketHandler(channel);
this.queueSuffixResolver = new SimpleUserQueueSuffixResolver();
TaskScheduler taskScheduler = Mockito.mock(TaskScheduler.class);
this.registry = new ServletStompEndpointRegistry(webSocketHandler, queueSuffixResolver, taskScheduler);
}
@Test
public void stompProtocolHandler() {
this.registry.addEndpoint("/stomp");
Map<String, SubProtocolHandler> protocolHandlers = webSocketHandler.getProtocolHandlers();
assertEquals(3, protocolHandlers.size());
assertNotNull(protocolHandlers.get("v10.stomp"));
assertNotNull(protocolHandlers.get("v11.stomp"));
assertNotNull(protocolHandlers.get("v12.stomp"));
StompProtocolHandler stompHandler = (StompProtocolHandler) protocolHandlers.get("v10.stomp");
assertSame(this.queueSuffixResolver, stompHandler.getUserQueueSuffixResolver());
}
@Test
public void handlerMapping() {
SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();
assertEquals(0, hm.getUrlMap().size());
this.registry.addEndpoint("/stompOverWebSocket");
this.registry.addEndpoint("/stompOverSockJS").withSockJS();
hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();
assertEquals(2, hm.getUrlMap().size());
assertNotNull(hm.getUrlMap().get("/stompOverWebSocket"));
assertNotNull(hm.getUrlMap().get("/stompOverSockJS/**"));
}
}

View File

@@ -0,0 +1,347 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.simp.config;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.ReplyTo;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.SubscribeEvent;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.MutableUserQueueSuffixResolver;
import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.simp.stomp.StompTextMessageBuilder;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.support.TestWebSocketSession;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
/**
* Test fixture for {@link WebSocketMessageBrokerConfigurationSupport}.
*
* @author Rossen Stoyanchev
*/
public class WebSocketMessageBrokerConfigurationSupportTests {
private AnnotationConfigApplicationContext cxtSimpleBroker;
private AnnotationConfigApplicationContext cxtStompBroker;
@Before
public void setupOnce() {
this.cxtSimpleBroker = new AnnotationConfigApplicationContext();
this.cxtSimpleBroker.register(TestWebSocketMessageBrokerConfiguration.class, TestSimpleMessageBrokerConfig.class);
this.cxtSimpleBroker.refresh();
this.cxtStompBroker = new AnnotationConfigApplicationContext();
this.cxtStompBroker.register(TestWebSocketMessageBrokerConfiguration.class, TestStompMessageBrokerConfig.class);
this.cxtStompBroker.refresh();
}
@Test
public void handlerMapping() {
SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.cxtSimpleBroker.getBean(HandlerMapping.class);
assertEquals(1, hm.getOrder());
Map<String, Object> handlerMap = hm.getHandlerMap();
assertEquals(1, handlerMap.size());
assertNotNull(handlerMap.get("/simpleBroker"));
}
@Test
public void webSocketRequestChannel() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketRequestChannel", SubscribableChannel.class);
ArgumentCaptor<MessageHandler> captor = ArgumentCaptor.forClass(MessageHandler.class);
verify(channel, times(3)).subscribe(captor.capture());
List<MessageHandler> values = captor.getAllValues();
assertEquals(3, values.size());
assertTrue(values.contains(cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class)));
}
@Test
public void webSocketRequestChannelWithStompBroker() {
SubscribableChannel channel = this.cxtStompBroker.getBean("webSocketRequestChannel", SubscribableChannel.class);
ArgumentCaptor<MessageHandler> captor = ArgumentCaptor.forClass(MessageHandler.class);
verify(channel, times(3)).subscribe(captor.capture());
List<MessageHandler> values = captor.getAllValues();
assertEquals(3, values.size());
assertTrue(values.contains(cxtStompBroker.getBean(AnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(StompBrokerRelayMessageHandler.class)));
}
@Test
public void webSocketRequestChannelSendMessage() throws Exception {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketRequestChannel", SubscribableChannel.class);
SubProtocolWebSocketHandler webSocketHandler = this.cxtSimpleBroker.getBean(SubProtocolWebSocketHandler.class);
TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.SEND).headers("destination:/foo").build();
webSocketHandler.handleMessage(new TestWebSocketSession(), textMessage);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
verify(channel).send(captor.capture());
Message message = captor.getValue();
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/foo", headers.getDestination());
}
@Test
public void webSocketResponseChannel() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", SubscribableChannel.class);
verify(channel).subscribe(any(SubProtocolWebSocketHandler.class));
verifyNoMoreInteractions(channel);
}
@Test
public void webSocketResponseChannelUsedByAnnotatedMethod() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", SubscribableChannel.class);
AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
headers.setSessionId("sess1");
headers.setSubscriptionId("subs1");
headers.setDestination("/foo");
Message<?> message = MessageBuilder.withPayloadAndHeaders(new byte[0], headers).build();
when(channel.send(any(Message.class))).thenReturn(true);
messageHandler.handleMessage(message);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
verify(channel).send(captor.capture());
message = captor.getValue();
headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/foo", headers.getDestination());
assertEquals("\"bar\"", new String((byte[]) message.getPayload()));
}
@Test
public void webSocketResponseChannelUsedBySimpleBroker() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", SubscribableChannel.class);
SimpleBrokerMessageHandler broker = this.cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
headers.setSessionId("sess1");
headers.setSubscriptionId("subs1");
headers.setDestination("/foo");
Message<?> message = MessageBuilder.withPayloadAndHeaders(new byte[0], headers).build();
// subscribe
broker.handleMessage(message);
headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setSessionId("sess1");
headers.setDestination("/foo");
message = MessageBuilder.withPayloadAndHeaders("bar".getBytes(), headers).build();
// message
when(channel.send(any(Message.class))).thenReturn(true);
broker.handleMessage(message);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
verify(channel).send(captor.capture());
message = captor.getValue();
headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/foo", headers.getDestination());
assertEquals("bar", new String((byte[]) message.getPayload()));
}
@Test
public void brokerChannel() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("brokerChannel", SubscribableChannel.class);
ArgumentCaptor<MessageHandler> captor = ArgumentCaptor.forClass(MessageHandler.class);
verify(channel, times(2)).subscribe(captor.capture());
List<MessageHandler> values = captor.getAllValues();
assertEquals(2, values.size());
assertTrue(values.contains(cxtSimpleBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class)));
}
@Test
public void brokerChannelWithStompBroker() {
SubscribableChannel channel = this.cxtStompBroker.getBean("brokerChannel", SubscribableChannel.class);
ArgumentCaptor<MessageHandler> captor = ArgumentCaptor.forClass(MessageHandler.class);
verify(channel, times(2)).subscribe(captor.capture());
List<MessageHandler> values = captor.getAllValues();
assertEquals(2, values.size());
assertTrue(values.contains(cxtStompBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(StompBrokerRelayMessageHandler.class)));
}
@Test
public void brokerChannelUsedByAnnotatedMethod() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("brokerChannel", SubscribableChannel.class);
AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setDestination("/foo");
Message<?> message = MessageBuilder.withPayloadAndHeaders(new byte[0], headers).build();
when(channel.send(any(Message.class))).thenReturn(true);
messageHandler.handleMessage(message);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
verify(channel).send(captor.capture());
message = captor.getValue();
headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/bar", headers.getDestination());
assertEquals("\"bar\"", new String((byte[]) message.getPayload()));
}
@Test
public void brokerChannelUsedByUserDestinationMessageHandler() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("brokerChannel", SubscribableChannel.class);
UserDestinationMessageHandler messageHandler = this.cxtSimpleBroker.getBean(UserDestinationMessageHandler.class);
this.cxtSimpleBroker.getBean(MutableUserQueueSuffixResolver.class).addQueueSuffix("joe", "s1", "s1");
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setDestination("/user/joe/foo");
Message<?> message = MessageBuilder.withPayloadAndHeaders(new byte[0], headers).build();
when(channel.send(any(Message.class))).thenReturn(true);
messageHandler.handleMessage(message);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
verify(channel).send(captor.capture());
message = captor.getValue();
headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/foos1", headers.getDestination());
}
@Controller
static class TestController {
@SubscribeEvent("/foo")
public String handleSubscribe() {
return "bar";
}
@MessageMapping("/foo")
@ReplyTo("/bar")
public String handleMessage() {
return "bar";
}
}
@Configuration
static class TestSimpleMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/simpleBroker");
}
@Override
public void configureMessageBroker(MessageBrokerConfigurer configurer) {
// SimpleBroker used by default
}
@Bean
public TestController subscriptionController() {
return new TestController();
}
}
@Configuration
static class TestStompMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stompBrokerRelay");
}
@Override
public void configureMessageBroker(MessageBrokerConfigurer configurer) {
configurer.enableStompBrokerRelay("/topic", "/queue").setAutoStartup(false);
}
}
@Configuration
static class TestWebSocketMessageBrokerConfiguration extends DelegatingWebSocketMessageBrokerConfiguration {
@Override
@Bean
public SubscribableChannel webSocketRequestChannel() {
return Mockito.mock(SubscribableChannel.class);
}
@Override
@Bean
public SubscribableChannel webSocketResponseChannel() {
return Mockito.mock(SubscribableChannel.class);
}
@Override
public SubscribableChannel brokerChannel() {
return Mockito.mock(SubscribableChannel.class);
}
}
}

View File

@@ -14,9 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.messaging.simp.config; package org.springframework.messaging.simp.handler;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -26,36 +32,37 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters; import org.junit.runners.Parameterized.Parameters;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.config.DelegatingWebSocketMessageBrokerConfiguration;
import org.springframework.messaging.simp.config.MessageBrokerConfigurer;
import org.springframework.messaging.simp.config.StompEndpointRegistry;
import org.springframework.messaging.simp.config.WebSocketMessageBrokerConfigurer;
import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompTextMessageBuilder;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel; import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.socket.AbstractWebSocketIntegrationTests; import org.springframework.web.socket.AbstractWebSocketIntegrationTests;
import org.springframework.web.socket.JettyWebSocketTestServer; import org.springframework.web.socket.JettyWebSocketTestServer;
import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.TomcatWebSocketTestServer; import org.springframework.web.socket.TomcatWebSocketTestServer;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter; import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter;
import org.springframework.web.socket.client.endpoint.StandardWebSocketClient; import org.springframework.web.socket.client.endpoint.StandardWebSocketClient;
import org.springframework.web.socket.client.jetty.JettyWebSocketClient; import org.springframework.web.socket.client.jetty.JettyWebSocketClient;
import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.config.WebSocketConfigurationSupport;
import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.messaging.simp.stomp.StompTextMessageBuilder.*;
/** /**
* Test fixture for {@link WebSocketConfigurationSupport}. * Integration tests with annotated message-handling methods.
*
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
*/ */
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class WebSocketMessageBrokerConfigurationTests extends AbstractWebSocketIntegrationTests { public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
@Parameters @Parameters
public static Iterable<Object[]> arguments() { public static Iterable<Object[]> arguments() {
@@ -68,37 +75,85 @@ public class WebSocketMessageBrokerConfigurationTests extends AbstractWebSocketI
@Override @Override
protected Class<?>[] getAnnotatedConfigClasses() { protected Class<?>[] getAnnotatedConfigClasses() {
return new Class<?>[] { TestWebSocketMessageBrokerConfiguration.class, SimpleBrokerConfigurer.class }; return new Class<?>[] { TestMessageBrokerConfiguration.class, TestMessageBrokerConfigurer.class };
} }
@Test @Test
public void sendMessage() throws Exception { public void simpleController() throws Exception {
final TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.SEND) TextMessage message = create(StompCommand.SEND).headers("destination:/app/simple").build();
.headers("destination:/app/foo").build(); WebSocketSession session = doHandshake(new TestClientWebSocketHandler(message, 0), "/ws");
WebSocketHandler clientHandler = new TextWebSocketHandlerAdapter() { SimpleController controller = this.wac.getBean(SimpleController.class);
@Override assertTrue(controller.latch.await(2, TimeUnit.SECONDS));
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
session.sendMessage(textMessage);
}
};
TestController testController = this.wac.getBean(TestController.class);
WebSocketSession session = this.webSocketClient.doHandshake(clientHandler, getWsBaseUrl() + "/ws");
assertTrue(testController.latch.await(2, TimeUnit.SECONDS));
session.close();
testController.latch = new CountDownLatch(1);
session = this.webSocketClient.doHandshake(clientHandler, getWsBaseUrl() + "/sockjs/websocket");
assertTrue(testController.latch.await(2, TimeUnit.SECONDS));
session.close(); session.close();
} }
@IntegrationTestController
static class SimpleController {
private CountDownLatch latch = new CountDownLatch(1);
@MessageMapping(value="/app/simple")
public void handle() {
this.latch.countDown();
}
}
private static class TestClientWebSocketHandler extends TextWebSocketHandlerAdapter {
private final TextMessage messageToSend;
private final int expected;
private final List<TextMessage> actual = new CopyOnWriteArrayList<TextMessage>();
private final CountDownLatch latch;
public TestClientWebSocketHandler(TextMessage messageToSend, int expectedNumberOfMessages) {
this.messageToSend = messageToSend;
this.expected = expectedNumberOfMessages;
this.latch = new CountDownLatch(this.expected);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
session.sendMessage(this.messageToSend);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
this.actual.add(message);
this.latch.countDown();
}
}
@Configuration @Configuration
static class TestWebSocketMessageBrokerConfiguration extends DelegatingWebSocketMessageBrokerConfiguration { @ComponentScan(basePackageClasses=AnnotationMethodIntegrationTests.class,
includeFilters=@ComponentScan.Filter(IntegrationTestController.class))
static class TestMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {
@Autowired
private HandshakeHandler handshakeHandler; // can't rely on classpath for server detection
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setHandshakeHandler(this.handshakeHandler);
}
@Override
public void configureMessageBroker(MessageBrokerConfigurer configurer) {
configurer.setAnnotationMethodDestinationPrefixes("/app/");
configurer.enableSimpleBroker("/topic", "/queue");
}
}
@Configuration
static class TestMessageBrokerConfiguration extends DelegatingWebSocketMessageBrokerConfiguration {
@Override @Override
@Bean @Bean
@@ -108,49 +163,15 @@ public class WebSocketMessageBrokerConfigurationTests extends AbstractWebSocketI
@Override @Override
@Bean @Bean
public SubscribableChannel webSocketReplyChannel() { public SubscribableChannel webSocketResponseChannel() {
return new ExecutorSubscribableChannel(); // synchronous return new ExecutorSubscribableChannel(); // synchronous
} }
@Bean
public TestController testController() {
return new TestController();
}
}
@Configuration
static class SimpleBrokerConfigurer implements WebSocketMessageBrokerConfigurer {
@Autowired
private HandshakeHandler handshakeHandler; // can't rely on classpath for server detection
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setHandshakeHandler(this.handshakeHandler);
registry.addEndpoint("/sockjs").withSockJS()
.setTransportHandlerOverrides(new WebSocketTransportHandler(this.handshakeHandler));;
}
@Override
public void configureMessageBroker(MessageBrokerConfigurer configurer) {
configurer.setAnnotationMethodDestinationPrefixes("/app/");
configurer.enableSimpleBroker("/topic");
}
} }
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Controller @Controller
private static class TestController { private @interface IntegrationTestController {
private CountDownLatch latch = new CountDownLatch(1);
@MessageMapping(value="/app/foo")
public void handleFoo() {
this.latch.countDown();
}
} }
} }

View File

@@ -108,6 +108,10 @@ public abstract class AbstractWebSocketIntegrationTests {
return "ws://localhost:" + this.server.getPort(); return "ws://localhost:" + this.server.getPort();
} }
protected WebSocketSession doHandshake(WebSocketHandler clientHandler, String endpointPath) {
return this.webSocketClient.doHandshake(clientHandler, getWsBaseUrl() + endpointPath);
}
static abstract class AbstractRequestUpgradeStrategyConfig { static abstract class AbstractRequestUpgradeStrategyConfig {