Consolidate websocket/messaging code

Before this change spring-messaging contained a few WebSocket-related
classes including WebSocket sub-protocol support for STOMP as well
as @EnableWebSocketMessageBroker and related configuration classes.

After this change those classes are located in the spring-websocket
module under org.springframework.web.socket.messaging.

This means the following classes in application configuration must
have their packages updated:

org.springframework.web.socket.messaging.config.EnableWebSocketMessageBroker
org.springframework.web.socket.messaging.config.StompEndpointRegistry
org.springframework.web.socket.messaging.config.WebSocketMessageBrokerConfigurer

MessageBrokerConfigurer has been renamed to MessageBrokerRegistry and
is also located in the above package.
This commit is contained in:
Rossen Stoyanchev
2013-11-22 13:49:38 -05:00
parent f888b8816f
commit 4de3291dc7
34 changed files with 966 additions and 870 deletions

View File

@@ -1,94 +0,0 @@
/*
* 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.handler.websocket;
import java.util.List;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
/**
* A contract for handling WebSocket messages as part of a higher level protocol, referred
* to as "sub-protocol" in the WebSocket RFC specification. Handles both
* {@link WebSocketMessage}s from a client as well as {@link Message}s to a client.
* <p>
* Implementations of this interface can be configured on a
* {@link SubProtocolWebSocketHandler} which selects a sub-protocol handler to delegate
* messages to based on the sub-protocol requested by the client through the
* {@code Sec-WebSocket-Protocol} request header.
*
* @author Andy Wilkinson
* @author Rossen Stoyanchev
*
* @since 4.0
*/
public interface SubProtocolHandler {
/**
* Return the list of sub-protocols supported by this handler, never {@code null}.
*/
List<String> getSupportedProtocols();
/**
* Handle the given {@link WebSocketMessage} received from a client.
*
* @param session the client session
* @param message the client message
* @param outputChannel an output channel to send messages to
*/
void handleMessageFromClient(WebSocketSession session, WebSocketMessage message,
MessageChannel outputChannel) throws Exception;
/**
* Handle the given {@link Message} to the client associated with the given WebSocket
* session.
*
* @param session the client session
* @param message the client message
*/
void handleMessageToClient(WebSocketSession session, Message<?> message) throws Exception;
/**
* Resolve the session id from the given message or return {@code null}.
*
* @param message the message to resolve the session id from
*/
String resolveSessionId(Message<?> message);
/**
* Invoked after a {@link WebSocketSession} has started.
*
* @param session the client session
* @param outputChannel a channel
*/
void afterSessionStarted(WebSocketSession session, MessageChannel outputChannel) throws Exception;
/**
* Invoked after a {@link WebSocketSession} has ended.
*
* @param session the client session
* @param closeStatus the reason why the session was closed
* @param outputChannel a channel
*/
void afterSessionEnded(WebSocketSession session, CloseStatus closeStatus,
MessageChannel outputChannel) throws Exception;
}

View File

@@ -1,232 +0,0 @@
/*
* 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.handler.websocket;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
/**
* A {@link WebSocketHandler} that delegates messages to a {@link SubProtocolHandler}
* based on the sub-protocol value requested by the client through the
* {@code Sec-WebSocket-Protocol} request header A default handler can also be configured
* to use if the client does not request a specific sub-protocol.
*
* @author Rossen Stoyanchev
* @author Andy Wilkinson
*
* @since 4.0
*/
public class SubProtocolWebSocketHandler implements WebSocketHandler, MessageHandler {
private final Log logger = LogFactory.getLog(SubProtocolWebSocketHandler.class);
private final MessageChannel outputChannel;
private final Map<String, SubProtocolHandler> protocolHandlers =
new TreeMap<String, SubProtocolHandler>(String.CASE_INSENSITIVE_ORDER);
private SubProtocolHandler defaultProtocolHandler;
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<String, WebSocketSession>();
/**
* @param outputChannel
*/
public SubProtocolWebSocketHandler(MessageChannel outputChannel) {
Assert.notNull(outputChannel, "outputChannel is required");
this.outputChannel = outputChannel;
}
/**
* Configure one or more handlers to use depending on the sub-protocol requested by
* the client in the WebSocket handshake request.
*
* @param protocolHandlers the sub-protocol handlers to use
*/
public void setProtocolHandlers(List<SubProtocolHandler> protocolHandlers) {
this.protocolHandlers.clear();
for (SubProtocolHandler handler: protocolHandlers) {
addProtocolHandler(handler);
}
}
/**
* Register a sub-protocol handler.
*/
public void addProtocolHandler(SubProtocolHandler handler) {
List<String> protocols = handler.getSupportedProtocols();
if (CollectionUtils.isEmpty(protocols)) {
logger.warn("No sub-protocols, ignoring handler " + handler);
return;
}
for (String protocol: protocols) {
SubProtocolHandler replaced = this.protocolHandlers.put(protocol, handler);
if ((replaced != null) && (replaced != handler) ) {
throw new IllegalStateException("Failed to map handler " + handler
+ " to protocol '" + protocol + "', it is already mapped to handler " + replaced);
}
}
}
/**
* @return the configured sub-protocol handlers
*/
public Map<String, SubProtocolHandler> getProtocolHandlers() {
return this.protocolHandlers;
}
/**
* Set the {@link SubProtocolHandler} to use when the client did not request a
* sub-protocol.
*
* @param defaultProtocolHandler the default handler
*/
public void setDefaultProtocolHandler(SubProtocolHandler defaultProtocolHandler) {
this.defaultProtocolHandler = defaultProtocolHandler;
if (this.protocolHandlers.isEmpty()) {
setProtocolHandlers(Arrays.asList(defaultProtocolHandler));
}
}
/**
* @return the default sub-protocol handler to use
*/
public SubProtocolHandler getDefaultProtocolHandler() {
return this.defaultProtocolHandler;
}
/**
* Return all supported protocols.
*/
public Set<String> getSupportedProtocols() {
return this.protocolHandlers.keySet();
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
this.sessions.put(session.getId(), session);
findProtocolHandler(session).afterSessionStarted(session, this.outputChannel);
}
protected final SubProtocolHandler findProtocolHandler(WebSocketSession session) {
SubProtocolHandler handler;
String protocol = session.getAcceptedProtocol();
if (!StringUtils.isEmpty(protocol)) {
handler = this.protocolHandlers.get(protocol);
Assert.state(handler != null,
"No handler for sub-protocol '" + protocol + "', handlers=" + this.protocolHandlers);
}
else {
if (this.defaultProtocolHandler != null) {
handler = this.defaultProtocolHandler;
}
else {
Set<SubProtocolHandler> handlers = new HashSet<SubProtocolHandler>(this.protocolHandlers.values());
if (handlers.size() == 1) {
handler = handlers.iterator().next();
}
else {
throw new IllegalStateException(
"No sub-protocol was requested and a default sub-protocol handler was not configured");
}
}
}
return handler;
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
findProtocolHandler(session).handleMessageFromClient(session, message, this.outputChannel);
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
String sessionId = resolveSessionId(message);
if (sessionId == null) {
logger.error("sessionId not found in message " + message);
return;
}
WebSocketSession session = this.sessions.get(sessionId);
if (session == null) {
logger.error("Session not found for session with id " + sessionId);
return;
}
try {
findProtocolHandler(session).handleMessageToClient(session, message);
}
catch (Exception e) {
logger.error("Failed to send message to client " + message, e);
}
}
private String resolveSessionId(Message<?> message) {
for (SubProtocolHandler handler : this.protocolHandlers.values()) {
String sessionId = handler.resolveSessionId(message);
if (sessionId != null) {
return sessionId;
}
}
if (this.defaultProtocolHandler != null) {
String sessionId = this.defaultProtocolHandler.resolveSessionId(message);
if (sessionId != null) {
return sessionId;
}
}
return null;
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
this.sessions.remove(session.getId());
findProtocolHandler(session).afterSessionEnded(session, closeStatus, this.outputChannel);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}

View File

@@ -47,6 +47,10 @@ public class SubscriptionMethodReturnValueHandler implements HandlerMethodReturn
private final MessageSendingOperations<String> messagingTemplate;
/**
* @param messagingTemplate a messaging template for sending messages directly
* to clients, e.g. in response to a subscription
*/
public SubscriptionMethodReturnValueHandler(MessageSendingOperations<String> messagingTemplate) {
Assert.notNull(messagingTemplate, "messagingTemplate is required");
this.messagingTemplate = messagingTemplate;

View File

@@ -19,6 +19,7 @@ package org.springframework.messaging.simp.config;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.handler.AbstractBrokerMessageHandler;
@@ -32,25 +33,25 @@ import org.springframework.util.Assert;
*/
public abstract class AbstractBrokerRegistration {
private final MessageChannel webSocketReplyChannel;
private final MessageChannel clientOutboundChannel;
private final String[] destinationPrefixes;
private final List<String> destinationPrefixes;
public AbstractBrokerRegistration(MessageChannel webSocketReplyChannel, String[] destinationPrefixes) {
Assert.notNull(webSocketReplyChannel, "");
this.webSocketReplyChannel = webSocketReplyChannel;
this.destinationPrefixes = destinationPrefixes;
public AbstractBrokerRegistration(MessageChannel clientOutboundChannel, String[] destinationPrefixes) {
Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' is required");
this.clientOutboundChannel = clientOutboundChannel;
this.destinationPrefixes = (destinationPrefixes != null)
? Arrays.<String>asList(destinationPrefixes) : Collections.<String>emptyList();
}
protected MessageChannel getWebSocketReplyChannel() {
return this.webSocketReplyChannel;
protected MessageChannel getClientOutboundChannel() {
return this.clientOutboundChannel;
}
protected Collection<String> getDestinationPrefixes() {
return (this.destinationPrefixes != null)
? Arrays.<String>asList(this.destinationPrefixes) : Collections.<String>emptyList();
return this.destinationPrefixes;
}
protected abstract AbstractBrokerMessageHandler getMessageHandler();

View File

@@ -0,0 +1,223 @@
/*
* 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 org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.handler.*;
import org.springframework.messaging.support.channel.AbstractSubscribableChannel;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
import org.springframework.messaging.support.converter.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeTypeUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Provides essential configuration for handling messages with simple messaging
* protocols such as STOMP.
* <p>
* {@link #clientInboundChannel()} and {@link #clientOutboundChannel()} deliver messages
* to and from remote clients to several message handlers such as
* <ul>
* <li>{@link #simpAnnotationMethodMessageHandler()}</li>
* <li>{@link #simpleBrokerMessageHandler()}</li>
* <li>{@link #stompBrokerRelayMessageHandler()}</li>
* <li>{@link #userDestinationMessageHandler()}</li>
* </ul>
* while {@link #brokerChannel()} delivers messages from within the application to the
* the respective message handlers. {@link #brokerMessagingTemplate()} can be injected
* into any application component to send messages.
* <p>
* Sub-classes are responsible for the part of the configuration that feed messages
* to and from the client inbound/outbound channels (e.g. STOMP over WebSokcet).
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractMessageBrokerConfiguration {
private static final boolean jackson2Present= ClassUtils.isPresent(
"com.fasterxml.jackson.databind.ObjectMapper", AbstractMessageBrokerConfiguration.class.getClassLoader());
private MessageBrokerRegistry brokerRegistry;
/**
* Protected constructor.
*/
protected AbstractMessageBrokerConfiguration() {
}
/**
* An accessor for the {@link MessageBrokerRegistry} that ensures its one-time creation
* and initialization through {@link #configureMessageBroker(MessageBrokerRegistry)}.
*/
protected final MessageBrokerRegistry getBrokerRegistry() {
if (this.brokerRegistry == null) {
MessageBrokerRegistry registry = new MessageBrokerRegistry(clientOutboundChannel());
configureMessageBroker(registry);
this.brokerRegistry = registry;
}
return this.brokerRegistry;
}
/**
* A hook for sub-classes to customize message broker configuration through the
* provided {@link MessageBrokerRegistry} instance.
*/
protected abstract void configureMessageBroker(MessageBrokerRegistry registry);
@Bean
public AbstractSubscribableChannel clientInboundChannel() {
return new ExecutorSubscribableChannel(clientInboundChannelExecutor());
}
@Bean
public ThreadPoolTaskExecutor clientInboundChannelExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("ClientInboundChannel-");
return executor;
}
@Bean
public AbstractSubscribableChannel clientOutboundChannel() {
return new ExecutorSubscribableChannel(clientOutboundChannelExecutor());
}
@Bean
public ThreadPoolTaskExecutor clientOutboundChannelExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("ClientOutboundChannel-");
return executor;
}
@Bean
public AbstractSubscribableChannel brokerChannel() {
return new ExecutorSubscribableChannel(); // synchronous
}
@Bean
public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() {
SimpAnnotationMethodMessageHandler handler =
new SimpAnnotationMethodMessageHandler(brokerMessagingTemplate(), clientOutboundChannel());
handler.setDestinationPrefixes(getBrokerRegistry().getApplicationDestinationPrefixes());
handler.setMessageConverter(brokerMessageConverter());
clientInboundChannel().subscribe(handler);
return handler;
}
@Bean
public AbstractBrokerMessageHandler simpleBrokerMessageHandler() {
SimpleBrokerMessageHandler handler = getBrokerRegistry().getSimpleBroker();
if (handler != null) {
clientInboundChannel().subscribe(handler);
brokerChannel().subscribe(handler);
return handler;
}
return noopBroker;
}
@Bean
public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler() {
AbstractBrokerMessageHandler handler = getBrokerRegistry().getStompBrokerRelay();
if (handler != null) {
clientInboundChannel().subscribe(handler);
brokerChannel().subscribe(handler);
return handler;
}
return noopBroker;
}
@Bean
public UserDestinationMessageHandler userDestinationMessageHandler() {
UserDestinationMessageHandler handler = new UserDestinationMessageHandler(
brokerMessagingTemplate(), userDestinationResolver());
clientInboundChannel().subscribe(handler);
brokerChannel().subscribe(handler);
return handler;
}
@Bean
public SimpMessagingTemplate brokerMessagingTemplate() {
SimpMessagingTemplate template = new SimpMessagingTemplate(brokerChannel());
String prefix = getBrokerRegistry().getUserDestinationPrefix();
if (prefix != null) {
template.setUserDestinationPrefix(prefix);
}
template.setMessageConverter(brokerMessageConverter());
return template;
}
@Bean
public CompositeMessageConverter brokerMessageConverter() {
DefaultContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver();
List<MessageConverter> converters = new ArrayList<MessageConverter>();
if (jackson2Present) {
converters.add(new MappingJackson2MessageConverter());
contentTypeResolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
}
converters.add(new StringMessageConverter());
converters.add(new ByteArrayMessageConverter());
return new CompositeMessageConverter(converters, contentTypeResolver);
}
@Bean
public UserDestinationResolver userDestinationResolver() {
DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userSessionRegistry());
String prefix = getBrokerRegistry().getUserDestinationPrefix();
if (prefix != null) {
resolver.setUserDestinationPrefix(prefix);
}
return resolver;
}
@Bean
public UserSessionRegistry userSessionRegistry() {
return new DefaultUserSessionRegistry();
}
private static final AbstractBrokerMessageHandler noopBroker = new AbstractBrokerMessageHandler(null) {
@Override
protected void startInternal() {
}
@Override
protected void stopInternal() {
}
@Override
protected void handleMessageInternal(Message<?> message) {
}
};
}

View File

@@ -1,154 +0,0 @@
/*
* 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.Set;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.DefaultHandshakeHandler;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.config.SockJsServiceRegistration;
import org.springframework.web.socket.sockjs.SockJsService;
import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler;
import org.springframework.web.socket.support.WebSocketHandlerDecorator;
/**
* An abstract base class class for configuring STOMP over WebSocket/SockJS endpoints.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractStompEndpointRegistration<M> implements StompEndpointRegistration {
private final String[] paths;
private final WebSocketHandler wsHandler;
private HandshakeHandler handshakeHandler;
private StompSockJsServiceRegistration sockJsServiceRegistration;
private final TaskScheduler sockJsTaskScheduler;
public AbstractStompEndpointRegistration(String[] paths, WebSocketHandler webSocketHandler,
TaskScheduler sockJsTaskScheduler) {
Assert.notEmpty(paths, "No paths specified");
this.paths = paths;
this.wsHandler = webSocketHandler;
this.sockJsTaskScheduler = sockJsTaskScheduler;
}
/**
* Provide a custom or pre-configured {@link HandshakeHandler}. This property is
* optional.
*/
@Override
public StompEndpointRegistration setHandshakeHandler(HandshakeHandler handshakeHandler) {
this.handshakeHandler = handshakeHandler;
return this;
}
/**
* Enable SockJS fallback options.
*/
@Override
public SockJsServiceRegistration withSockJS() {
this.sockJsServiceRegistration = new StompSockJsServiceRegistration(this.sockJsTaskScheduler);
if (this.handshakeHandler != null) {
WebSocketTransportHandler transportHandler = new WebSocketTransportHandler(this.handshakeHandler);
this.sockJsServiceRegistration.setTransportHandlerOverrides(transportHandler);
}
return this.sockJsServiceRegistration;
}
protected final M getMappings() {
M mappings = createMappings();
if (this.sockJsServiceRegistration != null) {
SockJsService sockJsService = this.sockJsServiceRegistration.getSockJsService();
for (String path : this.paths) {
String pathPattern = path.endsWith("/") ? path + "**" : path + "/**";
addSockJsServiceMapping(mappings, sockJsService, this.wsHandler, pathPattern);
}
}
else {
HandshakeHandler handshakeHandler = getOrCreateHandshakeHandler();
for (String path : this.paths) {
addWebSocketHandlerMapping(mappings, this.wsHandler, handshakeHandler, path);
}
}
return mappings;
}
protected abstract M createMappings();
private HandshakeHandler getOrCreateHandshakeHandler() {
HandshakeHandler handler = (this.handshakeHandler != null)
? this.handshakeHandler : new DefaultHandshakeHandler();
if (handler instanceof DefaultHandshakeHandler) {
DefaultHandshakeHandler defaultHandshakeHandler = (DefaultHandshakeHandler) handler;
if (ObjectUtils.isEmpty(defaultHandshakeHandler.getSupportedProtocols())) {
Set<String> protocols = findSubProtocolWebSocketHandler(this.wsHandler).getSupportedProtocols();
defaultHandshakeHandler.setSupportedProtocols(protocols.toArray(new String[protocols.size()]));
}
}
return handler;
}
private static SubProtocolWebSocketHandler findSubProtocolWebSocketHandler(WebSocketHandler webSocketHandler) {
WebSocketHandler actual = (webSocketHandler instanceof WebSocketHandlerDecorator) ?
((WebSocketHandlerDecorator) webSocketHandler).getLastHandler() : webSocketHandler;
Assert.isInstanceOf(SubProtocolWebSocketHandler.class, actual,
"No SubProtocolWebSocketHandler found: " + webSocketHandler);
return (SubProtocolWebSocketHandler) actual;
}
protected abstract void addSockJsServiceMapping(M mappings, SockJsService sockJsService,
WebSocketHandler wsHandler, String pathPattern);
protected abstract void addWebSocketHandlerMapping(M mappings,
WebSocketHandler wsHandler, HandshakeHandler handshakeHandler, String path);
private class StompSockJsServiceRegistration extends SockJsServiceRegistration {
public StompSockJsServiceRegistration(TaskScheduler defaultTaskScheduler) {
super(defaultTaskScheduler);
}
protected SockJsService getSockJsService() {
return super.getSockJsService();
}
}
}

View File

@@ -1,66 +0,0 @@
/*
* 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.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;
/**
* A {@link WebSocketMessageBrokerConfiguration} extension that detects beans of type
* {@link WebSocketMessageBrokerConfigurer} and delegates to all of them allowing callback
* style customization of the configuration provided in
* {@link WebSocketMessageBrokerConfigurationSupport}.
*
* <p>This class is typically imported via {@link EnableWebSocketMessageBroker}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
@Configuration
public class DelegatingWebSocketMessageBrokerConfiguration extends WebSocketMessageBrokerConfigurationSupport {
private List<WebSocketMessageBrokerConfigurer> configurers = new ArrayList<WebSocketMessageBrokerConfigurer>();
@Autowired(required=false)
public void setConfigurers(List<WebSocketMessageBrokerConfigurer> configurers) {
if (CollectionUtils.isEmpty(configurers)) {
return;
}
this.configurers.addAll(configurers);
}
@Override
protected void registerStompEndpoints(StompEndpointRegistry registry) {
for (WebSocketMessageBrokerConfigurer c : this.configurers) {
c.registerStompEndpoints(registry);
}
}
@Override
protected void configureMessageBroker(MessageBrokerConfigurer configurer) {
for (WebSocketMessageBrokerConfigurer c : this.configurers) {
c.configureMessageBroker(configurer);
}
}
}

View File

@@ -1,65 +0,0 @@
/*
* 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.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* Add this annotation to an {@code @Configuration} class to enable broker-backed
* messaging over WebSocket using a higher-level messaging sub-protocol.
*
* <pre class="code">
* &#064;Configuration
* &#064;EnableWebSocketMessageBroker
* public class MyWebSocketConfig {
*
* }
* </pre>
* <p>
* Customize the imported configuration by implementing the
* {@link WebSocketMessageBrokerConfigurer} interface:
*
* <pre class="code">
* &#064;Configuration
* &#064;EnableWebSocketMessageBroker
* public class MyConfiguration implements implements WebSocketMessageBrokerConfigurer {
*
* &#064;Override
* public void registerStompEndpoints(StompEndpointRegistry registry) {
* registry.addEndpoint("/portfolio").withSockJS();
* }
*
* &#064;Bean
* public void configureMessageBroker(MessageBrokerConfigurer configurer) {
* configurer.enableStompBrokerRelay("/queue/", "/topic/");
* configurer.setApplicationDestinationPrefixes("/app/");
* }
* }
* </pre>
*
* @author Rossen Stoyanchev
* @since 4.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebSocketMessageBrokerConfiguration.class)
public @interface EnableWebSocketMessageBroker {
}

View File

@@ -20,18 +20,19 @@ import java.util.Arrays;
import java.util.Collection;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.handler.AbstractBrokerMessageHandler;
import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.util.Assert;
/**
* A helper class for configuring message broker options.
* A registry for configuring message broker options.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class MessageBrokerConfigurer {
public class MessageBrokerRegistry {
private final MessageChannel webSocketResponseChannel;
private final MessageChannel clientOutboundChannel;
private SimpleBrokerRegistration simpleBroker;
@@ -42,9 +43,9 @@ public class MessageBrokerConfigurer {
private String userDestinationPrefix;
public MessageBrokerConfigurer(MessageChannel webSocketResponseChannel) {
Assert.notNull(webSocketResponseChannel);
this.webSocketResponseChannel = webSocketResponseChannel;
public MessageBrokerRegistry(MessageChannel clientOutboundChannel) {
Assert.notNull(clientOutboundChannel);
this.clientOutboundChannel = clientOutboundChannel;
}
/**
@@ -52,7 +53,7 @@ public class MessageBrokerConfigurer {
* destinations targeting the broker (e.g. destinations prefixed with "/topic").
*/
public SimpleBrokerRegistration enableSimpleBroker(String... destinationPrefixes) {
this.simpleBroker = new SimpleBrokerRegistration(this.webSocketResponseChannel, destinationPrefixes);
this.simpleBroker = new SimpleBrokerRegistration(this.clientOutboundChannel, destinationPrefixes);
return this.simpleBroker;
}
@@ -62,7 +63,7 @@ public class MessageBrokerConfigurer {
* destinations.
*/
public StompBrokerRelayRegistration enableStompBrokerRelay(String... destinationPrefixes) {
this.stompRelay = new StompBrokerRelayRegistration(this.webSocketResponseChannel, destinationPrefixes);
this.stompRelay = new StompBrokerRelayRegistration(this.clientOutboundChannel, destinationPrefixes);
return this.stompRelay;
}
@@ -78,7 +79,7 @@ public class MessageBrokerConfigurer {
* <p>
* Prefixes that do not have a trailing slash will have one automatically appended.
*/
public MessageBrokerConfigurer setApplicationDestinationPrefixes(String... prefixes) {
public MessageBrokerRegistry setApplicationDestinationPrefixes(String... prefixes) {
this.applicationDestinationPrefixes = prefixes;
return this;
}
@@ -97,24 +98,24 @@ public class MessageBrokerConfigurer {
* <p>
* The default prefix used to identify such destinations is "/user/".
*/
public MessageBrokerConfigurer setUserDestinationPrefix(String destinationPrefix) {
public MessageBrokerRegistry setUserDestinationPrefix(String destinationPrefix) {
this.userDestinationPrefix = destinationPrefix;
return this;
}
protected AbstractBrokerMessageHandler getSimpleBroker() {
protected SimpleBrokerMessageHandler getSimpleBroker() {
initSimpleBrokerIfNecessary();
return (this.simpleBroker != null) ? this.simpleBroker.getMessageHandler() : null;
}
protected void initSimpleBrokerIfNecessary() {
if ((this.simpleBroker == null) && (this.stompRelay == null)) {
this.simpleBroker = new SimpleBrokerRegistration(this.webSocketResponseChannel, null);
this.simpleBroker = new SimpleBrokerRegistration(this.clientOutboundChannel, null);
}
}
protected AbstractBrokerMessageHandler getStompBrokerRelay() {
protected StompBrokerRelayMessageHandler getStompBrokerRelay() {
return (this.stompRelay != null) ? this.stompRelay.getMessageHandler() : null;
}

View File

@@ -1,68 +0,0 @@
/*
* 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 org.springframework.scheduling.TaskScheduler;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsService;
/**
* A helper class for configuring STOMP protocol handling over WebSocket
* with optional SockJS fallback options.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ServletStompEndpointRegistration
extends AbstractStompEndpointRegistration<MultiValueMap<HttpRequestHandler, String>> {
public ServletStompEndpointRegistration(String[] paths,
WebSocketHandler wsHandler, TaskScheduler sockJsTaskScheduler) {
super(paths, wsHandler, sockJsTaskScheduler);
}
@Override
protected MultiValueMap<HttpRequestHandler, String> createMappings() {
return new LinkedMultiValueMap<HttpRequestHandler, String>();
}
@Override
protected void addSockJsServiceMapping(MultiValueMap<HttpRequestHandler, String> mappings,
SockJsService sockJsService, WebSocketHandler wsHandler, String pathPattern) {
SockJsHttpRequestHandler httpHandler = new SockJsHttpRequestHandler(sockJsService, wsHandler);
mappings.add(httpHandler, pathPattern);
}
@Override
protected void addWebSocketHandlerMapping(MultiValueMap<HttpRequestHandler, String> mappings,
WebSocketHandler wsHandler, HandshakeHandler handshakeHandler, String path) {
WebSocketHttpRequestHandler handler = new WebSocketHttpRequestHandler(wsHandler, handshakeHandler);
mappings.add(handler, path);
}
}

View File

@@ -1,108 +0,0 @@
/*
* 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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.handler.UserSessionRegistry;
import org.springframework.messaging.simp.stomp.StompProtocolHandler;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.support.WebSocketHandlerDecorator;
/**
* A helper class for configuring STOMP protocol handling over WebSocket.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ServletStompEndpointRegistry implements StompEndpointRegistry {
private final WebSocketHandler webSocketHandler;
private final SubProtocolWebSocketHandler subProtocolWebSocketHandler;
private final StompProtocolHandler stompHandler;
private final List<ServletStompEndpointRegistration> registrations = new ArrayList<ServletStompEndpointRegistration>();
private final TaskScheduler sockJsScheduler;
public ServletStompEndpointRegistry(WebSocketHandler webSocketHandler,
UserSessionRegistry userSessionRegistry, TaskScheduler defaultSockJsTaskScheduler) {
Assert.notNull(webSocketHandler);
Assert.notNull(userSessionRegistry);
this.webSocketHandler = webSocketHandler;
this.subProtocolWebSocketHandler = findSubProtocolWebSocketHandler(webSocketHandler);
this.stompHandler = new StompProtocolHandler();
this.stompHandler.setUserSessionRegistry(userSessionRegistry);
this.sockJsScheduler = defaultSockJsTaskScheduler;
}
private static SubProtocolWebSocketHandler findSubProtocolWebSocketHandler(WebSocketHandler webSocketHandler) {
WebSocketHandler actual = (webSocketHandler instanceof WebSocketHandlerDecorator) ?
((WebSocketHandlerDecorator) webSocketHandler).getLastHandler() : webSocketHandler;
Assert.isInstanceOf(SubProtocolWebSocketHandler.class, actual,
"No SubProtocolWebSocketHandler found: " + webSocketHandler);
return (SubProtocolWebSocketHandler) actual;
}
@Override
public StompEndpointRegistration addEndpoint(String... paths) {
this.subProtocolWebSocketHandler.addProtocolHandler(this.stompHandler);
ServletStompEndpointRegistration r = new ServletStompEndpointRegistration(
paths, this.webSocketHandler, this.sockJsScheduler);
this.registrations.add(r);
return r;
}
/**
* Returns a handler mapping with the mapped ViewControllers; or {@code null} in case of no registrations.
*/
protected AbstractHandlerMapping getHandlerMapping() {
Map<String, Object> urlMap = new LinkedHashMap<String, Object>();
for (ServletStompEndpointRegistration registration : this.registrations) {
MultiValueMap<HttpRequestHandler, String> mappings = registration.getMappings();
for (HttpRequestHandler httpHandler : mappings.keySet()) {
for (String pattern : mappings.get(httpHandler)) {
urlMap.put(pattern, httpHandler);
}
}
}
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
hm.setUrlMap(urlMap);
return hm;
}
}

View File

@@ -21,7 +21,7 @@ import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
/**
* A simple message broker alternative providing a simple getting started option.
* Registration class for configuring a {@link SimpleBrokerMessageHandler}.
*
* @author Rossen Stoyanchev
* @since 4.0
@@ -29,15 +29,13 @@ import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
public SimpleBrokerRegistration(MessageChannel webSocketReplyChannel, String[] destinationPrefixes) {
super(webSocketReplyChannel, destinationPrefixes);
public SimpleBrokerRegistration(MessageChannel clientOutboundChannel, String[] destinationPrefixes) {
super(clientOutboundChannel, destinationPrefixes);
}
@Override
protected SimpleBrokerMessageHandler getMessageHandler() {
SimpleBrokerMessageHandler handler =
new SimpleBrokerMessageHandler(getWebSocketReplyChannel(), getDestinationPrefixes());
return handler;
return new SimpleBrokerMessageHandler(getClientOutboundChannel(), getDestinationPrefixes());
}
}

View File

@@ -22,7 +22,7 @@ import org.springframework.util.Assert;
/**
* A helper class for configuring a relay to an external STOMP message broker.
* Registration class for configuring a {@link StompBrokerRelayMessageHandler}.
*
* @author Rossen Stoyanchev
* @since 4.0
@@ -44,8 +44,8 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
private boolean autoStartup = true;
public StompBrokerRelayRegistration(MessageChannel webSocketReplyChannel, String[] destinationPrefixes) {
super(webSocketReplyChannel, destinationPrefixes);
public StompBrokerRelayRegistration(MessageChannel clientOutboundChannel, String[] destinationPrefixes) {
super(clientOutboundChannel, destinationPrefixes);
}
@@ -124,18 +124,23 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
protected StompBrokerRelayMessageHandler getMessageHandler() {
StompBrokerRelayMessageHandler handler =
new StompBrokerRelayMessageHandler(getWebSocketReplyChannel(), getDestinationPrefixes());
new StompBrokerRelayMessageHandler(getClientOutboundChannel(), getDestinationPrefixes());
handler.setRelayHost(this.relayHost);
handler.setRelayPort(this.relayPort);
handler.setSystemLogin(this.applicationLogin);
handler.setSystemPasscode(this.applicationPasscode);
if (this.systemHeartbeatSendInterval != null) {
handler.setSystemHeartbeatSendInterval(this.systemHeartbeatSendInterval);
}
if (this.systemHeartbeatReceiveInterval != null) {
handler.setSystemHeartbeatReceiveInterval(this.systemHeartbeatReceiveInterval);
}
handler.setAutoStartup(this.autoStartup);
return handler;
}

View File

@@ -1,41 +0,0 @@
/*
* 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 org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.config.SockJsServiceRegistration;
/**
* Provides methods for configuring a STOMP protocol handler including enabling SockJS
* fallback options.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface StompEndpointRegistration {
/**
* Configure the HandshakeHandler to use.
*/
StompEndpointRegistration setHandshakeHandler(HandshakeHandler handshakeHandler);
/**
* Enable SockJS fallback options.
*/
SockJsServiceRegistration withSockJS();
}

View File

@@ -1,33 +0,0 @@
/*
* 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;
/**
* Provides methods for configuring STOMP protocol handlers at specific URL paths.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface StompEndpointRegistry {
/**
* Expose a STOMP endpoint at the specified URL path (or paths_.
*/
StompEndpointRegistration addEndpoint(String... paths);
}

View File

@@ -1,257 +0,0 @@
/*
* 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 org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.handler.*;
import org.springframework.messaging.support.channel.AbstractSubscribableChannel;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
import org.springframework.messaging.support.converter.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.config.SockJsServiceRegistration;
import java.util.ArrayList;
import java.util.List;
/**
* Configuration support for broker-backed messaging over WebSocket using a higher-level
* messaging sub-protocol such as STOMP. This class can either be extended directly
* or its configuration can also be customized in a callback style via
* {@link EnableWebSocketMessageBroker @EnableWebSocketMessageBroker} and
* {@link WebSocketMessageBrokerConfigurer}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class WebSocketMessageBrokerConfigurationSupport {
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", WebMvcConfigurationSupport.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", WebMvcConfigurationSupport.class.getClassLoader());
private MessageBrokerConfigurer messageBrokerConfigurer;
// WebSocket configuration including message channels to/from the application
@Bean
public HandlerMapping brokerWebSocketHandlerMapping() {
ServletStompEndpointRegistry registry = new ServletStompEndpointRegistry(
subProtocolWebSocketHandler(), userSessionRegistry(), brokerDefaultSockJsTaskScheduler());
registerStompEndpoints(registry);
AbstractHandlerMapping hm = registry.getHandlerMapping();
hm.setOrder(1);
return hm;
}
@Bean
public WebSocketHandler subProtocolWebSocketHandler() {
SubProtocolWebSocketHandler wsHandler = new SubProtocolWebSocketHandler(webSocketRequestChannel());
webSocketResponseChannel().subscribe(wsHandler);
return wsHandler;
}
@Bean
public UserSessionRegistry userSessionRegistry() {
return new DefaultUserSessionRegistry();
}
/**
* The default TaskScheduler to use if none is configured via
* {@link SockJsServiceRegistration#setTaskScheduler(org.springframework.scheduling.TaskScheduler)}, i.e.
* <pre class="code">
* &#064;Configuration
* &#064;EnableWebSocketMessageBroker
* public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
*
* public void registerStompEndpoints(StompEndpointRegistry registry) {
* registry.addEndpoint("/stomp").withSockJS().setTaskScheduler(myScheduler());
* }
*
* // ...
*
* }
* </pre>
*/
@Bean
public ThreadPoolTaskScheduler brokerDefaultSockJsTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("BrokerSockJS-");
return scheduler;
}
protected void registerStompEndpoints(StompEndpointRegistry registry) {
}
@Bean
public AbstractSubscribableChannel webSocketRequestChannel() {
return new ExecutorSubscribableChannel(webSocketRequestChannelExecutor());
}
@Bean
public ThreadPoolTaskExecutor webSocketRequestChannelExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("WebSocketRequestChannel-");
return executor;
}
@Bean
public AbstractSubscribableChannel webSocketResponseChannel() {
return new ExecutorSubscribableChannel(webSocketResponseChannelExecutor());
}
@Bean
public ThreadPoolTaskExecutor webSocketResponseChannelExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("WebSocketResponseChannel-");
return executor;
}
// Handling of messages by the application
@Bean
public SimpAnnotationMethodMessageHandler annotationMethodMessageHandler() {
SimpAnnotationMethodMessageHandler handler =
new SimpAnnotationMethodMessageHandler(brokerMessagingTemplate(), webSocketResponseChannel());
handler.setDestinationPrefixes(getMessageBrokerConfigurer().getApplicationDestinationPrefixes());
handler.setMessageConverter(simpMessageConverter());
webSocketRequestChannel().subscribe(handler);
return handler;
}
@Bean
public AbstractBrokerMessageHandler simpleBrokerMessageHandler() {
AbstractBrokerMessageHandler handler = getMessageBrokerConfigurer().getSimpleBroker();
if (handler == null) {
return noopBroker;
}
else {
webSocketRequestChannel().subscribe(handler);
brokerChannel().subscribe(handler);
return handler;
}
}
@Bean
public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler() {
AbstractBrokerMessageHandler handler = getMessageBrokerConfigurer().getStompBrokerRelay();
if (handler == null) {
return noopBroker;
}
else {
webSocketRequestChannel().subscribe(handler);
brokerChannel().subscribe(handler);
return handler;
}
}
protected final MessageBrokerConfigurer getMessageBrokerConfigurer() {
if (this.messageBrokerConfigurer == null) {
MessageBrokerConfigurer configurer = new MessageBrokerConfigurer(webSocketResponseChannel());
configureMessageBroker(configurer);
this.messageBrokerConfigurer = configurer;
}
return this.messageBrokerConfigurer;
}
protected void configureMessageBroker(MessageBrokerConfigurer configurer) {
}
@Bean
public UserDestinationMessageHandler userDestinationMessageHandler() {
UserDestinationMessageHandler handler = new UserDestinationMessageHandler(
brokerMessagingTemplate(), userDestinationResolver());
webSocketRequestChannel().subscribe(handler);
brokerChannel().subscribe(handler);
return handler;
}
@Bean
public SimpMessageSendingOperations brokerMessagingTemplate() {
SimpMessagingTemplate template = new SimpMessagingTemplate(brokerChannel());
String userDestinationPrefix = getMessageBrokerConfigurer().getUserDestinationPrefix();
if (userDestinationPrefix != null) {
template.setUserDestinationPrefix(userDestinationPrefix);
}
template.setMessageConverter(simpMessageConverter());
return template;
}
@Bean
public AbstractSubscribableChannel brokerChannel() {
return new ExecutorSubscribableChannel(); // synchronous
}
@Bean
public CompositeMessageConverter simpMessageConverter() {
DefaultContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver();
List<MessageConverter> converters = new ArrayList<MessageConverter>();
if (jackson2Present) {
converters.add(new MappingJackson2MessageConverter());
contentTypeResolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
}
converters.add(new StringMessageConverter());
converters.add(new ByteArrayMessageConverter());
return new CompositeMessageConverter(converters, contentTypeResolver);
}
@Bean
public UserDestinationResolver userDestinationResolver() {
DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userSessionRegistry());
String prefix = getMessageBrokerConfigurer().getUserDestinationPrefix();
if (prefix != null) {
resolver.setUserDestinationPrefix(prefix);
}
return resolver;
}
private static final AbstractBrokerMessageHandler noopBroker = new AbstractBrokerMessageHandler(null) {
@Override
protected void startInternal() {
}
@Override
protected void stopInternal() {
}
@Override
protected void handleMessageInternal(Message<?> message) {
}
};
}

View File

@@ -1,40 +0,0 @@
/*
* 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;
/**
* Defines callback methods to configure broker-backed messaging over WebSocket via
* {@link EnableWebSocketMessageBroker @EnableWebSocketMessageBroker}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface WebSocketMessageBrokerConfigurer {
/**
* Configure STOMP protocol handling over WebSocket at a specific URL.
*/
void registerStompEndpoints(StompEndpointRegistry registry);
/**
* Configure message broker options.
*/
void configureMessageBroker(MessageBrokerConfigurer configurer);
}

View File

@@ -79,7 +79,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
private final SimpMessageSendingOperations brokerTemplate;
private final SimpMessageSendingOperations webSocketResponseTemplate;
private final SimpMessageSendingOperations clientMessagingTemplate;
private MessageConverter messageConverter;
@@ -90,15 +90,15 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
/**
* @param brokerTemplate a messaging template to send application messages to the broker
* @param webSocketResponseChannel the channel for messages to WebSocket clients
* @param clientOutboundChannel the channel for messages to clients (e.g. WebSocket clients)
*/
public SimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
MessageChannel webSocketResponseChannel) {
MessageChannel clientOutboundChannel) {
Assert.notNull(brokerTemplate, "brokerTemplate is required");
Assert.notNull(webSocketResponseChannel, "webSocketReplyChannel is required");
Assert.notNull(clientOutboundChannel, "clientOutboundChannel is required");
this.brokerTemplate = brokerTemplate;
this.webSocketResponseTemplate = new SimpMessagingTemplate(webSocketResponseChannel);
this.clientMessagingTemplate = new SimpMessagingTemplate(clientOutboundChannel);
Collection<MessageConverter> converters = new ArrayList<MessageConverter>();
converters.add(new StringMessageConverter());
@@ -117,7 +117,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
public void setMessageConverter(MessageConverter converter) {
this.messageConverter = converter;
if (converter != null) {
((AbstractMessageSendingTemplate<?>) this.webSocketResponseTemplate).setMessageConverter(converter);
((AbstractMessageSendingTemplate<?>) this.clientMessagingTemplate).setMessageConverter(converter);
}
}
@@ -194,7 +194,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
// Annotation-based return value types
handlers.add(new SendToMethodReturnValueHandler(this.brokerTemplate, true));
handlers.add(new SubscriptionMethodReturnValueHandler(this.webSocketResponseTemplate));
handlers.add(new SubscriptionMethodReturnValueHandler(this.clientMessagingTemplate));
// custom return value types
handlers.addAll(getCustomReturnValueHandlers());

View File

@@ -1,251 +0,0 @@
/*
* 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.stomp;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.handler.websocket.SubProtocolHandler;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.handler.UserSessionRegistry;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
/**
* A {@link SubProtocolHandler} for STOMP that supports versions 1.0, 1.1, and 1.2 of the
* STOMP specification.
*
* @author Rossen Stoyanchev
* @author Andy Wilkinson
* @since 4.0
*/
public class StompProtocolHandler implements SubProtocolHandler {
/**
* The name of the header set on the CONNECTED frame indicating the name of the user
* authenticated on the WebSocket session.
*/
public static final String CONNECTED_USER_HEADER = "user-name";
private static final Log logger = LogFactory.getLog(StompProtocolHandler.class);
private final StompDecoder stompDecoder = new StompDecoder();
private final StompEncoder stompEncoder = new StompEncoder();
private UserSessionRegistry userSessionRegistry;
/**
* Provide a registry with which to register active user session ids.
*
* @see {@link org.springframework.messaging.simp.handler.UserDestinationMessageHandler}
*/
public void setUserSessionRegistry(UserSessionRegistry registry) {
this.userSessionRegistry = registry;
}
/**
* @return the configured UserSessionRegistry.
*/
public UserSessionRegistry getUserSessionRegistry() {
return this.userSessionRegistry;
}
@Override
public List<String> getSupportedProtocols() {
return Arrays.asList("v10.stomp", "v11.stomp", "v12.stomp");
}
/**
* Handle incoming WebSocket messages from clients.
*/
public void handleMessageFromClient(WebSocketSession session, WebSocketMessage webSocketMessage,
MessageChannel outputChannel) {
Message<?> message;
try {
Assert.isInstanceOf(TextMessage.class, webSocketMessage);
String payload = ((TextMessage)webSocketMessage).getPayload();
ByteBuffer byteBuffer = ByteBuffer.wrap(payload.getBytes(Charset.forName("UTF-8")));
message = this.stompDecoder.decode(byteBuffer);
}
catch (Throwable error) {
logger.error("Failed to parse STOMP frame, WebSocket message payload: ", error);
sendErrorMessage(session, error);
return;
}
try {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
if (SimpMessageType.HEARTBEAT.equals(headers.getMessageType())) {
logger.trace("Received heartbeat from client session=" + session.getId());
}
else {
logger.trace("Received message from client session=" + session.getId());
}
headers.setSessionId(session.getId());
headers.setUser(session.getPrincipal());
message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build();
outputChannel.send(message);
}
catch (Throwable t) {
logger.error("Terminating STOMP session due to failure to send message: ", t);
sendErrorMessage(session, t);
}
}
protected void sendErrorMessage(WebSocketSession session, Throwable error) {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.ERROR);
headers.setMessage(error.getMessage());
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
String payload = new String(this.stompEncoder.encode(message), Charset.forName("UTF-8"));
try {
session.sendMessage(new TextMessage(payload));
}
catch (Throwable t) {
// ignore
}
}
/**
* Handle STOMP messages going back out to WebSocket clients.
*/
@SuppressWarnings("unchecked")
@Override
public void handleMessageToClient(WebSocketSession session, Message<?> message) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
if (headers.getMessageType() == SimpMessageType.CONNECT_ACK) {
StompHeaderAccessor connectedHeaders = StompHeaderAccessor.create(StompCommand.CONNECTED);
connectedHeaders.setVersion(getVersion(headers));
connectedHeaders.setHeartbeat(0, 0); // no heart-beat support with simple broker
headers = connectedHeaders;
}
else if (SimpMessageType.MESSAGE.equals(headers.getMessageType())) {
headers.updateStompCommandAsServerMessage();
}
if (headers.getCommand() == StompCommand.CONNECTED) {
afterStompSessionConnected(headers, session);
}
if (StompCommand.MESSAGE.equals(headers.getCommand()) && (headers.getSubscriptionId() == null)) {
logger.error("Ignoring message, no subscriptionId header: " + message);
return;
}
if (!(message.getPayload() instanceof byte[])) {
logger.error("Ignoring message, expected byte[] content: " + message);
return;
}
try {
message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build();
byte[] bytes = this.stompEncoder.encode((Message<byte[]>)message);
session.sendMessage(new TextMessage(new String(bytes, Charset.forName("UTF-8"))));
}
catch (Throwable t) {
sendErrorMessage(session, t);
}
finally {
if (StompCommand.ERROR.equals(headers.getCommand())) {
try {
session.close(CloseStatus.PROTOCOL_ERROR);
}
catch (IOException e) {
// Ignore
}
}
}
}
private String getVersion(StompHeaderAccessor connectAckHeaders) {
String name = StompHeaderAccessor.CONNECT_MESSAGE_HEADER;
Message<?> connectMessage = (Message<?>) connectAckHeaders.getHeader(name);
StompHeaderAccessor connectHeaders = StompHeaderAccessor.wrap(connectMessage);
Assert.notNull(connectMessage, "CONNECT_ACK does not contain original CONNECT " + connectAckHeaders);
Set<String> acceptVersions = connectHeaders.getAcceptVersion();
if (acceptVersions.contains("1.2")) {
return "1.2";
}
else if (acceptVersions.contains("1.1")) {
return "1.1";
}
else if (acceptVersions.isEmpty()) {
return null;
}
else {
throw new StompConversionException("Unsupported version '" + acceptVersions + "'");
}
}
private void afterStompSessionConnected(StompHeaderAccessor headers, WebSocketSession session) {
Principal principal = session.getPrincipal();
if (principal != null) {
headers.setNativeHeader(CONNECTED_USER_HEADER, principal.getName());
if (this.userSessionRegistry != null) {
String suffix = session.getId();
this.userSessionRegistry.registerSessionId(principal.getName(), session.getId());
}
}
}
@Override
public String resolveSessionId(Message<?> message) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
return headers.getSessionId();
}
@Override
public void afterSessionStarted(WebSocketSession session, MessageChannel outputChannel) {
}
@Override
public void afterSessionEnded(WebSocketSession session, CloseStatus closeStatus, MessageChannel outputChannel) {
if ((this.userSessionRegistry != null) && (session.getPrincipal() != null)) {
this.userSessionRegistry.unregisterSessionId(session.getPrincipal().getName(), session.getId());
}
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
headers.setSessionId(session.getId());
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
outputChannel.send(message);
}
}

View File

@@ -1,134 +0,0 @@
/*
* 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 CONDITIOsNS 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.handler.websocket;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.messaging.MessageChannel;
import org.springframework.web.socket.support.TestWebSocketSession;
import static org.mockito.Mockito.*;
/**
* Test fixture for {@link SubProtocolWebSocketHandler}.
*
* @author Rossen Stoyanchev
* @author Andy Wilkinson
*/
public class SubProtocolWebSocketHandlerTests {
private SubProtocolWebSocketHandler webSocketHandler;
private TestWebSocketSession session;
@Mock SubProtocolHandler stompHandler;
@Mock SubProtocolHandler mqttHandler;
@Mock SubProtocolHandler defaultHandler;
@Mock MessageChannel channel;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.webSocketHandler = new SubProtocolWebSocketHandler(this.channel);
when(stompHandler.getSupportedProtocols()).thenReturn(Arrays.asList("v10.stomp", "v11.stomp", "v12.stomp"));
when(mqttHandler.getSupportedProtocols()).thenReturn(Arrays.asList("MQTT"));
this.session = new TestWebSocketSession();
this.session.setId("1");
}
@Test
public void subProtocolMatch() throws Exception {
this.webSocketHandler.setProtocolHandlers(Arrays.asList(stompHandler, mqttHandler));
this.session.setAcceptedProtocol("v12.sToMp");
this.webSocketHandler.afterConnectionEstablished(session);
verify(this.stompHandler).afterSessionStarted(session, this.channel);
verify(this.mqttHandler, times(0)).afterSessionStarted(session, this.channel);
}
@Test
public void subProtocolDefaultHandlerOnly() throws Exception {
this.webSocketHandler.setDefaultProtocolHandler(stompHandler);
this.session.setAcceptedProtocol("v12.sToMp");
this.webSocketHandler.afterConnectionEstablished(session);
verify(this.stompHandler).afterSessionStarted(session, this.channel);
}
@Test(expected=IllegalStateException.class)
public void subProtocolNoMatch() throws Exception {
this.webSocketHandler.setDefaultProtocolHandler(defaultHandler);
this.webSocketHandler.setProtocolHandlers(Arrays.asList(stompHandler, mqttHandler));
this.session.setAcceptedProtocol("wamp");
this.webSocketHandler.afterConnectionEstablished(session);
}
@Test
public void nullSubProtocol() throws Exception {
this.webSocketHandler.setDefaultProtocolHandler(defaultHandler);
this.webSocketHandler.afterConnectionEstablished(session);
verify(this.defaultHandler).afterSessionStarted(session, this.channel);
verify(this.stompHandler, times(0)).afterSessionStarted(session, this.channel);
verify(this.mqttHandler, times(0)).afterSessionStarted(session, this.channel);
}
@Test
public void emptySubProtocol() throws Exception {
this.session.setAcceptedProtocol("");
this.webSocketHandler.setDefaultProtocolHandler(defaultHandler);
this.webSocketHandler.afterConnectionEstablished(session);
verify(this.defaultHandler).afterSessionStarted(session, this.channel);
verify(this.stompHandler, times(0)).afterSessionStarted(session, this.channel);
verify(this.mqttHandler, times(0)).afterSessionStarted(session, this.channel);
}
@Test
public void noSubProtocolOneHandler() throws Exception {
this.webSocketHandler.setProtocolHandlers(Arrays.asList(stompHandler));
this.webSocketHandler.afterConnectionEstablished(session);
verify(this.stompHandler).afterSessionStarted(session, this.channel);
}
@Test(expected=IllegalStateException.class)
public void noSubProtocolTwoHandlers() throws Exception {
this.webSocketHandler.setProtocolHandlers(Arrays.asList(stompHandler, mqttHandler));
this.webSocketHandler.afterConnectionEstablished(session);
}
@Test(expected=IllegalStateException.class)
public void noSubProtocolNoDefaultHandler() throws Exception {
this.webSocketHandler.setProtocolHandlers(Arrays.asList(stompHandler, mqttHandler));
this.webSocketHandler.afterConnectionEstablished(session);
}
}

View File

@@ -1,164 +0,0 @@
/*
* 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.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.DefaultHandshakeHandler;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.sockjs.SockJsService;
import org.springframework.web.socket.sockjs.transport.TransportType;
import org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService;
import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler;
import static org.junit.Assert.*;
/**
* Test fixture for {@link AbstractStompEndpointRegistration}.
*
* @author Rossen Stoyanchev
*/
public class AbstractStompEndpointRegistrationTests {
private SubProtocolWebSocketHandler wsHandler;
private TaskScheduler scheduler;
@Before
public void setup() {
this.wsHandler = new SubProtocolWebSocketHandler(new ExecutorSubscribableChannel());
this.scheduler = Mockito.mock(TaskScheduler.class);
}
@Test
public void minimalRegistration() {
TestStompEndpointRegistration registration =
new TestStompEndpointRegistration(new String[] {"/foo"}, this.wsHandler, this.scheduler);
List<Mapping> mappings = registration.getMappings();
assertEquals(1, mappings.size());
Mapping m1 = mappings.get(0);
assertSame(this.wsHandler, m1.webSocketHandler);
assertEquals("/foo", m1.path);
}
@Test
public void customHandshakeHandler() {
DefaultHandshakeHandler handshakeHandler = new DefaultHandshakeHandler();
TestStompEndpointRegistration registration =
new TestStompEndpointRegistration(new String[] {"/foo"}, this.wsHandler, this.scheduler);
registration.setHandshakeHandler(handshakeHandler);
List<Mapping> mappings = registration.getMappings();
assertEquals(1, mappings.size());
Mapping m1 = mappings.get(0);
assertSame(this.wsHandler, m1.webSocketHandler);
assertEquals("/foo", m1.path);
assertSame(handshakeHandler, m1.handshakeHandler);
}
@Test
public void customHandshakeHandlerPassedToSockJsService() {
DefaultHandshakeHandler handshakeHandler = new DefaultHandshakeHandler();
TestStompEndpointRegistration registration =
new TestStompEndpointRegistration(new String[] {"/foo"}, this.wsHandler, this.scheduler);
registration.setHandshakeHandler(handshakeHandler);
registration.withSockJS();
List<Mapping> mappings = registration.getMappings();
assertEquals(1, mappings.size());
Mapping m1 = mappings.get(0);
assertSame(this.wsHandler, m1.webSocketHandler);
assertEquals("/foo/**", m1.path);
assertNotNull(m1.sockJsService);
WebSocketTransportHandler transportHandler =
(WebSocketTransportHandler) m1.sockJsService.getTransportHandlers().get(TransportType.WEBSOCKET);
assertSame(handshakeHandler, transportHandler.getHandshakeHandler());
}
private static class TestStompEndpointRegistration extends AbstractStompEndpointRegistration<List<Mapping>> {
public TestStompEndpointRegistration(String[] paths, SubProtocolWebSocketHandler wsh, TaskScheduler scheduler) {
super(paths, wsh, scheduler);
}
@Override
protected List<Mapping> createMappings() {
return new ArrayList<>();
}
@Override
protected void addSockJsServiceMapping(List<Mapping> mappings, SockJsService sockJsService,
WebSocketHandler wsHandler, String pathPattern) {
mappings.add(new Mapping(wsHandler, pathPattern, sockJsService));
}
@Override
protected void addWebSocketHandlerMapping(List<Mapping> mappings, WebSocketHandler wsHandler,
HandshakeHandler handshakeHandler, String path) {
mappings.add(new Mapping(wsHandler, path, handshakeHandler));
}
}
private static class Mapping {
private final WebSocketHandler webSocketHandler;
private final String path;
private final HandshakeHandler handshakeHandler;
private final DefaultSockJsService sockJsService;
public Mapping(WebSocketHandler handler, String path, SockJsService sockJsService) {
this.webSocketHandler = handler;
this.path = path;
this.handshakeHandler = null;
this.sockJsService = (DefaultSockJsService) sockJsService;
}
public Mapping(WebSocketHandler h, String path, HandshakeHandler hh) {
this.webSocketHandler = h;
this.path = path;
this.handshakeHandler = hh;
this.sockJsService = null;
}
}
}

View File

@@ -25,7 +25,6 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler;
@@ -35,7 +34,6 @@ import org.springframework.messaging.simp.handler.UserSessionRegistry;
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.messaging.support.channel.AbstractSubscribableChannel;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
@@ -43,24 +41,19 @@ import org.springframework.messaging.support.converter.CompositeMessageConverter
import org.springframework.messaging.support.converter.DefaultContentTypeResolver;
import org.springframework.stereotype.Controller;
import org.springframework.util.MimeTypeUtils;
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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
/**
* Test fixture for {@link WebSocketMessageBrokerConfigurationSupport}.
* Test fixture for {@link AbstractMessageBrokerConfiguration}.
*
* @author Rossen Stoyanchev
*/
public class WebSocketMessageBrokerConfigurationSupportTests {
public class MessageBrokerConfigurationTests {
private AnnotationConfigApplicationContext cxtSimpleBroker;
@@ -71,29 +64,19 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
public void setupOnce() {
this.cxtSimpleBroker = new AnnotationConfigApplicationContext();
this.cxtSimpleBroker.register(TestWebSocketMessageBrokerConfiguration.class, TestSimpleMessageBrokerConfig.class);
this.cxtSimpleBroker.register(TestMessageBrokerConfiguration.class);
this.cxtSimpleBroker.refresh();
this.cxtStompBroker = new AnnotationConfigApplicationContext();
this.cxtStompBroker.register(TestWebSocketMessageBrokerConfiguration.class, TestStompMessageBrokerConfig.class);
this.cxtStompBroker.register(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() {
public void clientInboundChannel() {
TestChannel channel = this.cxtSimpleBroker.getBean("webSocketRequestChannel", TestChannel.class);
TestChannel channel = this.cxtSimpleBroker.getBean("clientInboundChannel", TestChannel.class);
List<MessageHandler> handlers = channel.handlers;
assertEquals(3, handlers.size());
@@ -103,8 +86,8 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
@Test
public void webSocketRequestChannelWithStompBroker() {
TestChannel channel = this.cxtStompBroker.getBean("webSocketRequestChannel", TestChannel.class);
public void clientInboundChannelWithStompBroker() {
TestChannel channel = this.cxtStompBroker.getBean("clientInboundChannel", TestChannel.class);
List<MessageHandler> values = channel.handlers;
assertEquals(3, values.size());
@@ -114,34 +97,9 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
@Test
public void webSocketRequestChannelSendMessage() throws Exception {
public void clientOutboundChannelUsedByAnnotatedMethod() {
TestChannel channel = this.cxtSimpleBroker.getBean("webSocketRequestChannel", TestChannel.class);
SubProtocolWebSocketHandler webSocketHandler = this.cxtSimpleBroker.getBean(SubProtocolWebSocketHandler.class);
TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.SEND).headers("destination:/foo").build();
webSocketHandler.handleMessage(new TestWebSocketSession(), textMessage);
Message<?> message = channel.messages.get(0);
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
assertEquals("/foo", headers.getDestination());
}
@Test
public void webSocketResponseChannel() {
TestChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", TestChannel.class);
List<MessageHandler> values = channel.handlers;
assertEquals(1, values.size());
assertTrue(values.get(0) instanceof SubProtocolWebSocketHandler);
}
@Test
public void webSocketResponseChannelUsedByAnnotatedMethod() {
TestChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", TestChannel.class);
TestChannel channel = this.cxtSimpleBroker.getBean("clientOutboundChannel", TestChannel.class);
SimpAnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
@@ -161,8 +119,8 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
@Test
public void webSocketResponseChannelUsedBySimpleBroker() {
TestChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", TestChannel.class);
public void clientOutboundChannelUsedBySimpleBroker() {
TestChannel channel = this.cxtSimpleBroker.getBean("clientOutboundChannel", TestChannel.class);
SimpleBrokerMessageHandler broker = this.cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
@@ -252,7 +210,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
@Test
public void messageConverter() {
CompositeMessageConverter messageConverter = this.cxtStompBroker.getBean(
"simpMessageConverter", CompositeMessageConverter.class);
"brokerMessageConverter", CompositeMessageConverter.class);
DefaultContentTypeResolver resolver = (DefaultContentTypeResolver) messageConverter.getContentTypeResolver();
assertEquals(MimeTypeUtils.APPLICATION_JSON, resolver.getDefaultMimeType());
@@ -275,50 +233,26 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
@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
}
static class TestMessageBrokerConfiguration extends AbstractMessageBrokerConfiguration {
@Bean
public TestController subscriptionController() {
return new TestController();
}
}
@Configuration
static class TestStompMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stompBrokerRelay");
protected void configureMessageBroker(MessageBrokerRegistry registry) {
}
@Override
public void configureMessageBroker(MessageBrokerConfigurer configurer) {
configurer.enableStompBrokerRelay("/topic", "/queue").setAutoStartup(false);
}
}
@Configuration
static class TestWebSocketMessageBrokerConfiguration extends DelegatingWebSocketMessageBrokerConfiguration {
@Override
@Bean
public AbstractSubscribableChannel webSocketRequestChannel() {
public AbstractSubscribableChannel clientInboundChannel() {
return new TestChannel();
}
@Override
@Bean
public AbstractSubscribableChannel webSocketResponseChannel() {
public AbstractSubscribableChannel clientOutboundChannel() {
return new TestChannel();
}
@@ -328,6 +262,16 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
}
@Configuration
static class TestStompMessageBrokerConfig extends TestMessageBrokerConfiguration {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue").setAutoStartup(false);
}
}
private static class TestChannel extends ExecutorSubscribableChannel {
private final List<MessageHandler> handlers = new ArrayList<>();

View File

@@ -1,90 +0,0 @@
/*
* 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.DefaultUserSessionRegistry;
import org.springframework.messaging.simp.handler.UserSessionRegistry;
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 UserSessionRegistry userSessionRegistry;
@Before
public void setup() {
MessageChannel channel = Mockito.mock(MessageChannel.class);
this.webSocketHandler = new SubProtocolWebSocketHandler(channel);
this.userSessionRegistry = new DefaultUserSessionRegistry();
TaskScheduler taskScheduler = Mockito.mock(TaskScheduler.class);
this.registry = new ServletStompEndpointRegistry(webSocketHandler, userSessionRegistry, 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.userSessionRegistry, stompHandler.getUserSessionRegistry());
}
@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

@@ -1,247 +0,0 @@
/*
* 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.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.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
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.support.channel.AbstractSubscribableChannel;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
import org.springframework.stereotype.Controller;
import org.springframework.web.socket.AbstractWebSocketIntegrationTests;
import org.springframework.web.socket.JettyWebSocketTestServer;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.TomcatWebSocketTestServer;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter;
import org.springframework.web.socket.client.endpoint.StandardWebSocketClient;
import org.springframework.web.socket.client.jetty.JettyWebSocketClient;
import org.springframework.web.socket.server.HandshakeHandler;
import static org.junit.Assert.*;
import static org.springframework.messaging.simp.stomp.StompTextMessageBuilder.*;
/**
* Integration tests with annotated message-handling methods.
* @author Rossen Stoyanchev
*/
@RunWith(Parameterized.class)
public class SimpAnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
@Parameters
public static Iterable<Object[]> arguments() {
return Arrays.asList(new Object[][] {
{new JettyWebSocketTestServer(), new JettyWebSocketClient()},
{new TomcatWebSocketTestServer(), new StandardWebSocketClient()}
});
}
@Override
protected Class<?>[] getAnnotatedConfigClasses() {
return new Class<?>[] { TestMessageBrokerConfiguration.class, TestMessageBrokerConfigurer.class };
}
@Test
public void sendMessageToController() throws Exception {
TextMessage message = create(StompCommand.SEND).headers("destination:/app/simple").build();
WebSocketSession session = doHandshake(new TestClientWebSocketHandler(0, message), "/ws").get();
SimpleController controller = this.wac.getBean(SimpleController.class);
try {
assertTrue(controller.latch.await(2, TimeUnit.SECONDS));
}
finally {
session.close();
}
}
@Test
public void sendMessageToControllerAndReceiveReplyViaTopic() throws Exception {
TextMessage message1 = create(StompCommand.SUBSCRIBE).headers(
"id:subs1", "destination:/topic/increment").build();
TextMessage message2 = create(StompCommand.SEND).headers(
"destination:/app/increment").body("5").build();
TestClientWebSocketHandler clientHandler = new TestClientWebSocketHandler(1, message1, message2);
WebSocketSession session = doHandshake(clientHandler, "/ws").get();
try {
assertTrue(clientHandler.latch.await(2, TimeUnit.SECONDS));
}
finally {
session.close();
}
}
// SPR-10930
@Test
public void sendMessageToBrokerAndReceiveReplyViaTopic() throws Exception {
TextMessage message1 = create(StompCommand.SUBSCRIBE).headers("id:subs1", "destination:/topic/foo").build();
TextMessage message2 = create(StompCommand.SEND).headers("destination:/topic/foo").body("5").build();
TestClientWebSocketHandler clientHandler = new TestClientWebSocketHandler(1, message1, message2);
WebSocketSession session = doHandshake(clientHandler, "/ws").get();
try {
assertTrue(clientHandler.latch.await(2, TimeUnit.SECONDS));
String payload = clientHandler.actual.get(0).getPayload();
assertTrue("Expected STOMP Command=MESSAGE, got " + payload, payload.startsWith("MESSAGE\n"));
}
finally {
session.close();
}
}
@IntegrationTestController
static class SimpleController {
private CountDownLatch latch = new CountDownLatch(1);
@MessageMapping(value="/simple")
public void handle() {
this.latch.countDown();
}
@MessageMapping(value="/exception")
public void handleWithError() {
throw new IllegalArgumentException("Bad input");
}
@MessageExceptionHandler
public void handleException(IllegalArgumentException ex) {
}
}
@IntegrationTestController
static class IncrementController {
@MessageMapping(value="/increment")
public int handle(int i) {
return i + 1;
}
}
private static class TestClientWebSocketHandler extends TextWebSocketHandlerAdapter {
private final TextMessage[] messagesToSend;
private final int expected;
private final List<TextMessage> actual = new CopyOnWriteArrayList<>();
private final CountDownLatch latch;
public TestClientWebSocketHandler(int expectedNumberOfMessages, TextMessage... messagesToSend) {
this.messagesToSend = messagesToSend;
this.expected = expectedNumberOfMessages;
this.latch = new CountDownLatch(this.expected);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
for (TextMessage message : this.messagesToSend) {
session.sendMessage(message);
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
this.actual.add(message);
this.latch.countDown();
}
}
@Configuration
@ComponentScan(basePackageClasses=SimpAnnotationMethodIntegrationTests.class,
useDefaultFilters=false,
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.setApplicationDestinationPrefixes("/app");
configurer.enableSimpleBroker("/topic", "/queue");
}
}
@Configuration
static class TestMessageBrokerConfiguration extends DelegatingWebSocketMessageBrokerConfiguration {
@Override
@Bean
public AbstractSubscribableChannel webSocketRequestChannel() {
return new ExecutorSubscribableChannel(); // synchronous
}
@Override
@Bean
public AbstractSubscribableChannel webSocketResponseChannel() {
return new ExecutorSubscribableChannel(); // synchronous
}
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Controller
private @interface IntegrationTestController {
}
}

View File

@@ -146,9 +146,9 @@ public class SimpAnnotationMethodMessageHandlerTests {
private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler {
public TestSimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
MessageChannel webSocketResponseChannel) {
MessageChannel clientOutboundChannel) {
super(brokerTemplate, webSocketResponseChannel);
super(brokerTemplate, clientOutboundChannel);
}
public void registerHandler(Object handler) {

View File

@@ -20,7 +20,6 @@ import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageType;
@@ -146,7 +145,7 @@ public class StompHeaderAccessorTests {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.MESSAGE);
headers.setSubscriptionId("s1");
headers.setDestination("/d");
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setContentType(MimeTypeUtils.APPLICATION_JSON);
Map<String, List<String>> actual = headers.toNativeHeaderMap();

View File

@@ -1,119 +0,0 @@
/*
* 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.stomp;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashSet;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.TestPrincipal;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.support.TestWebSocketSession;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Test fixture for {@link StompProtocolHandler} tests.
*
* @author Rossen Stoyanchev
*/
public class StompProtocolHandlerTests {
private StompProtocolHandler stompHandler;
private TestWebSocketSession session;
private MessageChannel channel;
private ArgumentCaptor<Message> messageCaptor;
@Before
public void setup() {
this.stompHandler = new StompProtocolHandler();
this.channel = Mockito.mock(MessageChannel.class);
this.messageCaptor = ArgumentCaptor.forClass(Message.class);
this.session = new TestWebSocketSession();
this.session.setId("s1");
this.session.setPrincipal(new TestPrincipal("joe"));
}
@Test
public void connectedResponseIsSentWhenConnectAckIsToBeSentToClient() {
StompHeaderAccessor connectHeaders = StompHeaderAccessor.create(StompCommand.CONNECT);
connectHeaders.setHeartbeat(10000, 10000);
connectHeaders.setNativeHeader(StompHeaderAccessor.STOMP_ACCEPT_VERSION_HEADER, "1.0,1.1");
Message<?> connectMessage = MessageBuilder.withPayload(new byte[0]).setHeaders(connectHeaders).build();
SimpMessageHeaderAccessor connectAckHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
connectAckHeaders.setHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER, connectMessage);
Message<byte[]> connectAck = MessageBuilder.withPayload(new byte[0]).setHeaders(connectAckHeaders).build();
this.stompHandler.handleMessageToClient(this.session, connectAck);
verifyNoMoreInteractions(this.channel);
// Check CONNECTED reply
assertEquals(1, this.session.getSentMessages().size());
TextMessage textMessage = (TextMessage) this.session.getSentMessages().get(0);
Message<?> message = new StompDecoder().decode(ByteBuffer.wrap(textMessage.getPayload().getBytes()));
StompHeaderAccessor replyHeaders = StompHeaderAccessor.wrap(message);
assertEquals(StompCommand.CONNECTED, replyHeaders.getCommand());
assertEquals("1.1", replyHeaders.getVersion());
assertArrayEquals(new long[] {0, 0}, replyHeaders.getHeartbeat());
assertEquals("joe", replyHeaders.getNativeHeader("user-name").get(0));
}
@Test
public void messagesAreAugmentedAndForwarded() {
TextMessage textMessage = StompTextMessageBuilder.create(StompCommand.CONNECT).headers(
"login:guest", "passcode:guest", "accept-version:1.1,1.0", "heart-beat:10000,10000").build();
this.stompHandler.handleMessageFromClient(this.session, textMessage, this.channel);
verify(this.channel).send(this.messageCaptor.capture());
Message<?> actual = this.messageCaptor.getValue();
assertNotNull(actual);
StompHeaderAccessor headers = StompHeaderAccessor.wrap(actual);
assertEquals(StompCommand.CONNECT, headers.getCommand());
assertEquals("s1", headers.getSessionId());
assertEquals("joe", headers.getUser().getName());
assertEquals("guest", headers.getLogin());
assertEquals("PROTECTED", headers.getPasscode());
assertArrayEquals(new long[] {10000, 10000}, headers.getHeartbeat());
assertEquals(new HashSet<>(Arrays.asList("1.1","1.0")), headers.getAcceptVersion());
assertEquals(0, this.session.getSentMessages().size());
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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.stomp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.web.socket.TextMessage;
/**
* A builder for creating WebSocket messages with STOMP frame content.
*
* @author Rossen Stoyanchev
*/
public class StompTextMessageBuilder {
private StompCommand command;
private final List<String> headerLines = new ArrayList<String>();
private String body;
private StompTextMessageBuilder(StompCommand command) {
this.command = command;
}
public static StompTextMessageBuilder create(StompCommand command) {
return new StompTextMessageBuilder(command);
}
public StompTextMessageBuilder headers(String... headerLines) {
this.headerLines.addAll(Arrays.asList(headerLines));
return this;
}
public StompTextMessageBuilder body(String body) {
this.body = body;
return this;
}
public TextMessage build() {
StringBuilder sb = new StringBuilder(this.command.name()).append("\n");
for (String line : this.headerLines) {
sb.append(line).append("\n");
}
sb.append("\n");
if (this.body != null) {
sb.append(this.body);
}
sb.append("\u0000");
return new TextMessage(sb.toString());
}
}