Browse Source

Support heartbeat in SimpleBrokerMessageHandler

Issue: SPR-10954
pull/770/merge
Rossen Stoyanchev 10 years ago
parent
commit
de9675bf5a
  1. 11
      spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java
  2. 4
      spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageType.java
  3. 213
      spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java
  4. 43
      spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java
  5. 24
      spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java
  6. 179
      spring-messaging/src/test/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandlerTests.java
  7. 8
      spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java
  8. 36
      spring-websocket/src/main/java/org/springframework/web/socket/messaging/StompSubProtocolHandler.java
  9. 2
      spring-websocket/src/main/resources/META-INF/spring.schemas
  10. 896
      spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.2.xsd
  11. 9
      spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java
  12. 22
      spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java
  13. 284
      spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java
  14. 8
      spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java
  15. 3
      spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml

11
spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java

@ -51,8 +51,6 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { @@ -51,8 +51,6 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
// SiMP header names
public static final String CONNECT_MESSAGE_HEADER = "simpConnectMessage";
public static final String DESTINATION_HEADER = "simpDestination";
public static final String MESSAGE_TYPE_HEADER = "simpMessageType";
@ -65,6 +63,11 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { @@ -65,6 +63,11 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
public static final String USER_HEADER = "simpUser";
public static final String CONNECT_MESSAGE_HEADER = "simpConnectMessage";
public static final String HEART_BEAT_HEADER = "simpHeartbeat";
/**
* For internal use.
* <p>The original destination used by a client when subscribing. Such a
@ -262,4 +265,8 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { @@ -262,4 +265,8 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
return (Principal) headers.get(USER_HEADER);
}
public static long[] getHeartbeat(Map<String, Object> headers) {
return (long[]) headers.get(HEART_BEAT_HEADER);
}
}

4
spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageType.java

@ -29,14 +29,14 @@ public enum SimpMessageType { @@ -29,14 +29,14 @@ public enum SimpMessageType {
CONNECT_ACK,
HEARTBEAT,
MESSAGE,
SUBSCRIBE,
UNSUBSCRIBE,
HEARTBEAT,
DISCONNECT,
DISCONNECT_ACK,

213
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java

@ -16,7 +16,11 @@ @@ -16,7 +16,11 @@
package org.springframework.messaging.simp.broker;
import java.security.Principal;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
@ -27,6 +31,7 @@ import org.springframework.messaging.simp.SimpMessageType; @@ -27,6 +31,7 @@ import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.support.MessageHeaderInitializer;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.PathMatcher;
@ -43,10 +48,18 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -43,10 +48,18 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
private static final byte[] EMPTY_PAYLOAD = new byte[0];
private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
private SubscriptionRegistry subscriptionRegistry;
private PathMatcher pathMatcher;
private TaskScheduler taskScheduler;
private long[] heartbeatValue;
private ScheduledFuture<?> heartbeatFuture;
private MessageHeaderInitializer headerInitializer;
@ -100,6 +113,49 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -100,6 +113,49 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
initPathMatcherToUse();
}
/**
* Configure the {@link org.springframework.scheduling.TaskScheduler} to
* use for providing heartbeat support. Setting this property also sets the
* {@link #setHeartbeatValue heartbeatValue} to "10000, 10000".
* <p>By default this is not set.
* @since 4.2
*/
public void setTaskScheduler(TaskScheduler taskScheduler) {
Assert.notNull(taskScheduler);
this.taskScheduler = taskScheduler;
if (this.heartbeatValue == null) {
this.heartbeatValue = new long[] {10000, 10000};
}
}
/**
* Return the configured TaskScheduler.
*/
public TaskScheduler getTaskScheduler() {
return this.taskScheduler;
}
/**
* Configure the value for the heart-beat settings. The first number
* represents how often the server will write or send a heartbeat.
* The second is how often the client should write. 0 means no heartbeats.
* <p>By default this is set to "0, 0" unless the {@link #setTaskScheduler
* taskScheduler} in which case the default becomes "10000,10000"
* (in milliseconds).
* @since 4.2
*/
public void setHeartbeatValue(long[] heartbeat) {
Assert.notNull(heartbeat);
this.heartbeatValue = heartbeat;
}
/**
* The configured value for the heart-beat settings.
*/
public long[] getHeartbeatValue() {
return this.heartbeatValue;
}
/**
* Configure a {@link MessageHeaderInitializer} to apply to the headers
* of all messages sent to the client outbound channel.
@ -120,11 +176,37 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -120,11 +176,37 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
@Override
public void startInternal() {
publishBrokerAvailableEvent();
if (getTaskScheduler() != null) {
long interval = initHeartbeatTaskDelay();
if (interval > 0) {
this.heartbeatFuture = this.taskScheduler.scheduleWithFixedDelay(new HeartbeatTask(), interval);
}
}
else {
Assert.isTrue(getHeartbeatValue() == null ||
(getHeartbeatValue()[0] == 0 && getHeartbeatValue()[1] == 0),
"Heartbeat values configured but no TaskScheduler is provided.");
}
}
private long initHeartbeatTaskDelay() {
if (getHeartbeatValue() == null) {
return 0;
}
else if (getHeartbeatValue()[0] > 0 && getHeartbeatValue()[1] > 0) {
return Math.min(getHeartbeatValue()[0], getHeartbeatValue()[1]);
}
else {
return (getHeartbeatValue()[0] > 0 ? getHeartbeatValue()[0] : getHeartbeatValue()[1]);
}
}
@Override
public void stopInternal() {
publishBrokerUnavailableEvent();
if (this.heartbeatFuture != null) {
this.heartbeatFuture.cancel(true);
}
}
@Override
@ -133,6 +215,9 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -133,6 +215,9 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(headers);
String destination = SimpMessageHeaderAccessor.getDestination(headers);
String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);
Principal user = SimpMessageHeaderAccessor.getUser(headers);
updateSessionReadTime(sessionId);
if (!checkDestinationPrefix(destination)) {
return;
@ -150,23 +235,21 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -150,23 +235,21 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
}
else if (SimpMessageType.CONNECT.equals(messageType)) {
logMessage(message);
long[] clientHeartbeat = SimpMessageHeaderAccessor.getHeartbeat(headers);
long[] serverHeartbeat = getHeartbeatValue();
this.sessions.put(sessionId, new SessionInfo(sessionId, user, clientHeartbeat, serverHeartbeat));
SimpMessageHeaderAccessor connectAck = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
initHeaders(connectAck);
connectAck.setSessionId(sessionId);
connectAck.setUser(SimpMessageHeaderAccessor.getUser(headers));
connectAck.setHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER, message);
connectAck.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, serverHeartbeat);
Message<byte[]> messageOut = MessageBuilder.createMessage(EMPTY_PAYLOAD, connectAck.getMessageHeaders());
getClientOutboundChannel().send(messageOut);
}
else if (SimpMessageType.DISCONNECT.equals(messageType)) {
logMessage(message);
this.subscriptionRegistry.unregisterAllSubscriptions(sessionId);
SimpMessageHeaderAccessor disconnectAck = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT_ACK);
initHeaders(disconnectAck);
disconnectAck.setSessionId(sessionId);
disconnectAck.setUser(SimpMessageHeaderAccessor.getUser(headers));
Message<byte[]> messageOut = MessageBuilder.createMessage(EMPTY_PAYLOAD, disconnectAck.getMessageHeaders());
getClientOutboundChannel().send(messageOut);
handleDisconnect(sessionId, user);
}
else if (SimpMessageType.SUBSCRIBE.equals(messageType)) {
logMessage(message);
@ -178,6 +261,15 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -178,6 +261,15 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
}
}
private void updateSessionReadTime(String sessionId) {
if (sessionId != null) {
SessionInfo info = this.sessions.get(sessionId);
if (info != null) {
info.setLastReadTime(System.currentTimeMillis());
}
}
}
private void logMessage(Message<?> message) {
if (logger.isDebugEnabled()) {
SimpMessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, SimpMessageHeaderAccessor.class);
@ -192,11 +284,23 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -192,11 +284,23 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
}
}
private void handleDisconnect(String sessionId, Principal user) {
this.sessions.remove(sessionId);
this.subscriptionRegistry.unregisterAllSubscriptions(sessionId);
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT_ACK);
accessor.setSessionId(sessionId);
accessor.setUser(user);
initHeaders(accessor);
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
getClientOutboundChannel().send(message);
}
protected void sendMessageToSubscribers(String destination, Message<?> message) {
MultiValueMap<String,String> subscriptions = this.subscriptionRegistry.findSubscriptions(message);
if (!subscriptions.isEmpty() && logger.isDebugEnabled()) {
logger.debug("Broadcasting to " + subscriptions.size() + " sessions.");
}
long now = System.currentTimeMillis();
for (String sessionId : subscriptions.keySet()) {
for (String subscriptionId : subscriptions.get(sessionId)) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
@ -212,6 +316,12 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -212,6 +316,12 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
catch (Throwable ex) {
logger.error("Failed to send " + message, ex);
}
finally {
SessionInfo info = this.sessions.get(sessionId);
if (info != null) {
info.setLastWriteTime(now);
}
}
}
}
}
@ -221,4 +331,93 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { @@ -221,4 +331,93 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
return "SimpleBroker[" + this.subscriptionRegistry + "]";
}
private static class SessionInfo {
/* STOMP spec: receiver SHOULD take into account an error margin */
private static final long HEARTBEAT_MULTIPLIER = 3;
private final String sessiondId;
private final Principal user;
private final long readInterval;
private final long writeInterval;
private volatile long lastReadTime;
private volatile long lastWriteTime;
public SessionInfo(String sessiondId, Principal user, long[] clientHeartbeat, long[] serverHeartbeat) {
this.sessiondId = sessiondId;
this.user = user;
if (clientHeartbeat != null && serverHeartbeat != null) {
this.readInterval = (clientHeartbeat[0] > 0 && serverHeartbeat[1] > 0 ?
Math.max(clientHeartbeat[0], serverHeartbeat[1]) * HEARTBEAT_MULTIPLIER : 0);
this.writeInterval = (clientHeartbeat[1] > 0 && serverHeartbeat[0] > 0 ?
Math.max(clientHeartbeat[1], serverHeartbeat[0]) : 0);
}
else {
this.readInterval = 0;
this.writeInterval = 0;
}
this.lastReadTime = this.lastWriteTime = System.currentTimeMillis();
}
public String getSessiondId() {
return this.sessiondId;
}
public Principal getUser() {
return this.user;
}
public long getReadInterval() {
return this.readInterval;
}
public long getWriteInterval() {
return this.writeInterval;
}
public long getLastReadTime() {
return this.lastReadTime;
}
public void setLastReadTime(long lastReadTime) {
this.lastReadTime = lastReadTime;
}
public long getLastWriteTime() {
return this.lastWriteTime;
}
public void setLastWriteTime(long lastWriteTime) {
this.lastWriteTime = lastWriteTime;
}
}
private class HeartbeatTask implements Runnable {
@Override
public void run() {
long now = System.currentTimeMillis();
for (SessionInfo info : sessions.values()) {
if (info.getReadInterval() > 0 && (now - info.getLastReadTime()) > info.getReadInterval()) {
handleDisconnect(info.getSessiondId(), info.getUser());
}
if (info.getWriteInterval() > 0 && (now - info.getLastWriteTime()) > info.getWriteInterval()) {
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
accessor.setSessionId(info.getSessiondId());
accessor.setUser(info.getUser());
initHeaders(accessor);
MessageHeaders headers = accessor.getMessageHeaders();
getClientOutboundChannel().send(MessageBuilder.createMessage(EMPTY_PAYLOAD, headers));
}
}
}
}
}

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

@ -19,6 +19,7 @@ package org.springframework.messaging.simp.config; @@ -19,6 +19,7 @@ package org.springframework.messaging.simp.config;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.scheduling.TaskScheduler;
/**
* Registration class for configuring a {@link SimpleBrokerMessageHandler}.
@ -28,14 +29,54 @@ import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; @@ -28,14 +29,54 @@ import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
*/
public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
private TaskScheduler taskScheduler;
private long[] heartbeat;
public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
super(inChannel, outChannel, prefixes);
}
/**
* Configure the {@link org.springframework.scheduling.TaskScheduler} to
* use for providing heartbeat support. Setting this property also sets the
* {@link #setHeartbeatValue heartbeatValue} to "10000, 10000".
* <p>By default this is not set.
* @since 4.2
*/
public SimpleBrokerRegistration setTaskScheduler(TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
return this;
}
/**
* Configure the value for the heartbeat settings. The first number
* represents how often the server will write or send a heartbeat.
* The second is how often the client should write. 0 means no heartbeats.
* <p>By default this is set to "0, 0" unless the {@link #setTaskScheduler
* taskScheduler} in which case the default becomes "10000,10000"
* (in milliseconds).
* @since 4.2
*/
public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
this.heartbeat = heartbeat;
return this;
}
@Override
protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
return new SimpleBrokerMessageHandler(getClientInboundChannel(),
SimpleBrokerMessageHandler handler = new SimpleBrokerMessageHandler(getClientInboundChannel(),
getClientOutboundChannel(), brokerChannel, getDestinationPrefixes());
if (this.taskScheduler != null) {
handler.setTaskScheduler(this.taskScheduler);
}
if (this.heartbeat != null) {
handler.setHeartbeatValue(this.heartbeat);
}
return handler;
}
}

24
spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java

@ -389,7 +389,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { @@ -389,7 +389,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
}
}
else if (StompCommand.CONNECTED.equals(command)) {
initHeartbeats(stompHeaders);
initHeartbeatTasks(stompHeaders);
this.sessionFuture.set(this);
this.sessionHandler.afterConnected(this, stompHeaders);
}
@ -420,20 +420,18 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { @@ -420,20 +420,18 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
handler.handleFrame(stompHeaders, object);
}
private void initHeartbeats(StompHeaders connectedHeaders) {
long clientRead = this.connectHeaders.getHeartbeat()[0];
long serverWrite = connectedHeaders.getHeartbeat()[1];
if (clientRead > 0 && serverWrite > 0) {
long interval = Math.max(clientRead, serverWrite);
private void initHeartbeatTasks(StompHeaders connectedHeaders) {
long[] connect = this.connectHeaders.getHeartbeat();
long[] connected = connectedHeaders.getHeartbeat();
if (connect == null || connected == null) {
return;
}
if (connect[0] > 0 && connected[1] > 0) {
long interval = Math.max(connect[0], connected[1]);
this.connection.onWriteInactivity(new WriteInactivityTask(), interval);
}
long clientWrite = this.connectHeaders.getHeartbeat()[1];
long serverRead = connectedHeaders.getHeartbeat()[0];
if (clientWrite > 0 && serverRead > 0) {
final long interval = Math.max(clientWrite, serverRead) * HEARTBEAT_MULTIPLIER;
if (connect[1] > 0 && connected[0] > 0) {
final long interval = Math.max(connect[1], connected[0]) * HEARTBEAT_MULTIPLIER;
this.connection.onReadInactivity(new ReadInactivityTask(), interval);
}
}

179
spring-messaging/src/test/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandlerTests.java

@ -16,12 +16,15 @@ @@ -16,12 +16,15 @@
package org.springframework.messaging.simp.broker;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import org.junit.Before;
import org.junit.Test;
@ -29,13 +32,16 @@ import org.mockito.ArgumentCaptor; @@ -29,13 +32,16 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.SubscribableChannel;
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.scheduling.TaskScheduler;
/**
* Unit tests for SimpleBrokerMessageHandler.
@ -43,6 +49,7 @@ import org.springframework.messaging.support.MessageBuilder; @@ -43,6 +49,7 @@ import org.springframework.messaging.support.MessageBuilder;
* @author Rossen Stoyanchev
* @since 4.0
*/
@SuppressWarnings("unchecked")
public class SimpleBrokerMessageHandlerTests {
private SimpleBrokerMessageHandler messageHandler;
@ -56,6 +63,9 @@ public class SimpleBrokerMessageHandlerTests { @@ -56,6 +63,9 @@ public class SimpleBrokerMessageHandlerTests {
@Mock
private SubscribableChannel brokerChannel;
@Mock
private TaskScheduler taskScheduler;
@Captor
ArgumentCaptor<Message<?>> messageCaptor;
@ -133,11 +143,11 @@ public class SimpleBrokerMessageHandlerTests { @@ -133,11 +143,11 @@ public class SimpleBrokerMessageHandlerTests {
@Test
public void connect() {
String sess1 = "sess1";
this.messageHandler.start();
Message<String> connectMessage = createConnectMessage(sess1);
String id = "sess1";
Message<String> connectMessage = createConnectMessage(id, new TestPrincipal("joe"), null);
this.messageHandler.setTaskScheduler(this.taskScheduler);
this.messageHandler.handleMessage(connectMessage);
verify(this.clientOutboundChannel, times(1)).send(this.messageCaptor.capture());
@ -145,10 +155,150 @@ public class SimpleBrokerMessageHandlerTests { @@ -145,10 +155,150 @@ public class SimpleBrokerMessageHandlerTests {
SimpMessageHeaderAccessor connectAckHeaders = SimpMessageHeaderAccessor.wrap(connectAckMessage);
assertEquals(connectMessage, connectAckHeaders.getHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER));
assertEquals(sess1, connectAckHeaders.getSessionId());
assertEquals(id, connectAckHeaders.getSessionId());
assertEquals("joe", connectAckHeaders.getUser().getName());
assertArrayEquals(new long[] {10000, 10000},
SimpMessageHeaderAccessor.getHeartbeat(connectAckHeaders.getMessageHeaders()));
}
@Test
public void heartbeatValueWithAndWithoutTaskScheduler() throws Exception {
assertNull(this.messageHandler.getHeartbeatValue());
this.messageHandler.setTaskScheduler(this.taskScheduler);
assertNotNull(this.messageHandler.getHeartbeatValue());
assertArrayEquals(new long[] {10000, 10000}, this.messageHandler.getHeartbeatValue());
}
@Test(expected = IllegalArgumentException.class)
public void startWithHeartbeatValueWithoutTaskScheduler() throws Exception {
this.messageHandler.setHeartbeatValue(new long[] {10000, 10000});
this.messageHandler.start();
}
@SuppressWarnings("unchecked")
@Test
public void startAndStopWithHeartbeatValue() throws Exception {
ScheduledFuture future = mock(ScheduledFuture.class);
when(this.taskScheduler.scheduleWithFixedDelay(any(Runnable.class), eq(15000L))).thenReturn(future);
this.messageHandler.setTaskScheduler(this.taskScheduler);
this.messageHandler.setHeartbeatValue(new long[] {15000, 16000});
this.messageHandler.start();
verify(this.taskScheduler).scheduleWithFixedDelay(any(Runnable.class), eq(15000L));
verifyNoMoreInteractions(this.taskScheduler, future);
this.messageHandler.stop();
verify(future).cancel(true);
verifyNoMoreInteractions(future);
}
@SuppressWarnings("unchecked")
@Test
public void startWithOneZeroHeartbeatValue() throws Exception {
this.messageHandler.setTaskScheduler(this.taskScheduler);
this.messageHandler.setHeartbeatValue(new long[] {0, 10000});
this.messageHandler.start();
verify(this.taskScheduler).scheduleWithFixedDelay(any(Runnable.class), eq(10000L));
}
@Test
public void readInactivity() throws Exception {
this.messageHandler.setHeartbeatValue(new long[] {0, 1});
this.messageHandler.setTaskScheduler(this.taskScheduler);
this.messageHandler.start();
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(this.taskScheduler).scheduleWithFixedDelay(taskCaptor.capture(), eq(1L));
Runnable heartbeatTask = taskCaptor.getValue();
assertNotNull(heartbeatTask);
String id = "sess1";
TestPrincipal user = new TestPrincipal("joe");
Message<String> connectMessage = createConnectMessage(id, user, new long[] {1, 0});
this.messageHandler.handleMessage(connectMessage);
Thread.sleep(10);
heartbeatTask.run();
verify(this.clientOutboundChannel, atLeast(2)).send(this.messageCaptor.capture());
List<Message<?>> messages = this.messageCaptor.getAllValues();
assertEquals(2, messages.size());
MessageHeaders headers = messages.get(0).getHeaders();
assertEquals(SimpMessageType.CONNECT_ACK, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
headers = messages.get(1).getHeaders();
assertEquals(SimpMessageType.DISCONNECT_ACK, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
assertEquals(id, headers.get(SimpMessageHeaderAccessor.SESSION_ID_HEADER));
assertEquals(user, headers.get(SimpMessageHeaderAccessor.USER_HEADER));
}
@Test
public void writeInactivity() throws Exception {
this.messageHandler.setHeartbeatValue(new long[] {1, 0});
this.messageHandler.setTaskScheduler(this.taskScheduler);
this.messageHandler.start();
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(this.taskScheduler).scheduleWithFixedDelay(taskCaptor.capture(), eq(1L));
Runnable heartbeatTask = taskCaptor.getValue();
assertNotNull(heartbeatTask);
String id = "sess1";
TestPrincipal user = new TestPrincipal("joe");
Message<String> connectMessage = createConnectMessage(id, user, new long[] {0, 1});
this.messageHandler.handleMessage(connectMessage);
Thread.sleep(10);
heartbeatTask.run();
verify(this.clientOutboundChannel, times(2)).send(this.messageCaptor.capture());
List<Message<?>> messages = this.messageCaptor.getAllValues();
assertEquals(2, messages.size());
MessageHeaders headers = messages.get(0).getHeaders();
assertEquals(SimpMessageType.CONNECT_ACK, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
headers = messages.get(1).getHeaders();
assertEquals(SimpMessageType.HEARTBEAT, headers.get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
assertEquals(id, headers.get(SimpMessageHeaderAccessor.SESSION_ID_HEADER));
assertEquals(user, headers.get(SimpMessageHeaderAccessor.USER_HEADER));
}
@Test
public void readWriteIntervalCalculation() throws Exception {
this.messageHandler.setHeartbeatValue(new long[] {1, 1});
this.messageHandler.setTaskScheduler(this.taskScheduler);
this.messageHandler.start();
ArgumentCaptor<Runnable> taskCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(this.taskScheduler).scheduleWithFixedDelay(taskCaptor.capture(), eq(1L));
Runnable heartbeatTask = taskCaptor.getValue();
assertNotNull(heartbeatTask);
String id = "sess1";
TestPrincipal user = new TestPrincipal("joe");
Message<String> connectMessage = createConnectMessage(id, user, new long[] {10000, 10000});
this.messageHandler.handleMessage(connectMessage);
Thread.sleep(10);
heartbeatTask.run();
verify(this.clientOutboundChannel, times(1)).send(this.messageCaptor.capture());
List<Message<?>> messages = this.messageCaptor.getAllValues();
assertEquals(1, messages.size());
assertEquals(SimpMessageType.CONNECT_ACK,
messages.get(0).getHeaders().get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
}
private Message<String> createSubscriptionMessage(String sessionId, String subcriptionId, String destination) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
@ -158,17 +308,18 @@ public class SimpleBrokerMessageHandlerTests { @@ -158,17 +308,18 @@ public class SimpleBrokerMessageHandlerTests {
return MessageBuilder.createMessage("", headers.getMessageHeaders());
}
private Message<String> createConnectMessage(String sessionId) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
headers.setSessionId(sessionId);
headers.setUser(new TestPrincipal("joe"));
return MessageBuilder.createMessage("", headers.getMessageHeaders());
private Message<String> createConnectMessage(String sessionId, Principal user, long[] heartbeat) {
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
accessor.setSessionId(sessionId);
accessor.setUser(user);
accessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, heartbeat);
return MessageBuilder.createMessage("", accessor.getMessageHeaders());
}
private Message<String> createMessage(String destination, String payload) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headers.setDestination(destination);
return MessageBuilder.createMessage("", headers.getMessageHeaders());
return MessageBuilder.createMessage(payload, headers.getMessageHeaders());
}
private boolean messageCaptured(String sessionId, String subcriptionId, String destination) {

8
spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java

@ -320,6 +320,14 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser { @@ -320,6 +320,14 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser {
String pathMatcherRef = messageBrokerElement.getAttribute("path-matcher");
brokerDef.getPropertyValues().add("pathMatcher", new RuntimeBeanReference(pathMatcherRef));
}
if (simpleBrokerElem.hasAttribute("scheduler")) {
String scheduler = simpleBrokerElem.getAttribute("scheduler");
brokerDef.getPropertyValues().add("taskScheduler", new RuntimeBeanReference(scheduler));
}
if (simpleBrokerElem.hasAttribute("heartbeat")) {
String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
}
}
else if (brokerRelayElem != null) {
String prefix = brokerRelayElem.getAttribute("prefix");

36
spring-websocket/src/main/java/org/springframework/web/socket/messaging/StompSubProtocolHandler.java

@ -34,6 +34,7 @@ import org.springframework.context.ApplicationEventPublisher; @@ -34,6 +34,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpAttributes;
import org.springframework.messaging.simp.SimpAttributesContextHolder;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
@ -233,17 +234,18 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE @@ -233,17 +234,18 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
StompHeaderAccessor headerAccessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (logger.isTraceEnabled()) {
logger.trace("From client: " + headerAccessor.getShortLogMessage(message.getPayload()));
}
headerAccessor.setSessionId(session.getId());
headerAccessor.setSessionAttributes(session.getAttributes());
headerAccessor.setUser(session.getPrincipal());
headerAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, headerAccessor.getHeartbeat());
if (!detectImmutableMessageInterceptor(outputChannel)) {
headerAccessor.setImmutable();
}
if (logger.isTraceEnabled()) {
logger.trace("From client: " + headerAccessor.getShortLogMessage(message.getPayload()));
}
if (StompCommand.CONNECT.equals(headerAccessor.getCommand())) {
this.stats.incrementConnectCount();
}
@ -401,13 +403,17 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE @@ -401,13 +403,17 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
}
else if (accessor instanceof SimpMessageHeaderAccessor) {
stompAccessor = StompHeaderAccessor.wrap(message);
if (SimpMessageType.CONNECT_ACK.equals(stompAccessor.getMessageType())) {
SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(message.getHeaders());
if (SimpMessageType.CONNECT_ACK.equals(messageType)) {
stompAccessor = convertConnectAcktoStompConnected(stompAccessor);
}
else if (SimpMessageType.DISCONNECT_ACK.equals(stompAccessor.getMessageType())) {
else if (SimpMessageType.DISCONNECT_ACK.equals(messageType)) {
stompAccessor = StompHeaderAccessor.create(StompCommand.ERROR);
stompAccessor.setMessage("Session closed.");
}
else if (SimpMessageType.HEARTBEAT.equals(messageType)) {
stompAccessor = StompHeaderAccessor.createForHeartbeat();
}
else if (stompAccessor.getCommand() == null || StompCommand.SEND.equals(stompAccessor.getCommand())) {
stompAccessor.updateStompCommandAsServerMessage();
}
@ -429,23 +435,21 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE @@ -429,23 +435,21 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
Message<?> message = (Message<?>) connectAckHeaders.getHeader(name);
Assert.notNull(message, "Original STOMP CONNECT not found in " + connectAckHeaders);
StompHeaderAccessor connectHeaders = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
String version;
StompHeaderAccessor connectedHeaders = StompHeaderAccessor.create(StompCommand.CONNECTED);
Set<String> acceptVersions = connectHeaders.getAcceptVersion();
if (acceptVersions.contains("1.2")) {
version = "1.2";
connectedHeaders.setVersion("1.2");
}
else if (acceptVersions.contains("1.1")) {
version = "1.1";
}
else if (acceptVersions.isEmpty()) {
version = null;
connectedHeaders.setVersion("1.1");
}
else {
else if (!acceptVersions.isEmpty()) {
throw new IllegalArgumentException("Unsupported STOMP version '" + acceptVersions + "'");
}
StompHeaderAccessor connectedHeaders = StompHeaderAccessor.create(StompCommand.CONNECTED);
connectedHeaders.setVersion(version);
connectedHeaders.setHeartbeat(0, 0); // not supported
long[] heartbeat = (long[]) connectAckHeaders.getHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER);
if (heartbeat != null) {
connectedHeaders.setHeartbeat(heartbeat[0], heartbeat[1]);
}
return connectedHeaders;
}

2
spring-websocket/src/main/resources/META-INF/spring.schemas

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
http\://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd=org/springframework/web/socket/config/spring-websocket-4.0.xsd
http\://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd=org/springframework/web/socket/config/spring-websocket-4.1.xsd
http\://www.springframework.org/schema/websocket/spring-websocket.xsd=org/springframework/web/socket/config/spring-websocket-4.1.xsd
http\://www.springframework.org/schema/websocket/spring-websocket.xsd=org/springframework/web/socket/config/spring-websocket-4.2.xsd

896
spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.2.xsd

@ -0,0 +1,896 @@ @@ -0,0 +1,896 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ Copyright 2002-2014 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ 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.
-->
<xsd:schema xmlns="http://www.springframework.org/schema/websocket"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
targetNamespace="http://www.springframework.org/schema/websocket"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans" schemaLocation="http://www.springframework.org/schema/beans/spring-beans.xsd"/>
<xsd:import namespace="http://www.springframework.org/schema/tool" schemaLocation="http://www.springframework.org/schema/tool/spring-tool.xsd" />
<xsd:complexType name="mapping">
<xsd:annotation>
<xsd:documentation><![CDATA[
An entry in the registered HandlerMapping that matches a path with a handler.
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="path" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation><![CDATA[
A path that maps a particular request to a handler.
Exact path mapping URIs (such as "/myPath") are supported as well as Ant-stype path patterns (such as /myPath/**).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="handler" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.WebSocketHandler"><![CDATA[
The bean name of a WebSocketHandler to use for requests that match the path configuration.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="handshake-handler">
<xsd:attribute name="ref" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.server.HandshakeHandler"><![CDATA[
The bean name of a HandshakeHandler to use for processing WebSocket handshake requests.
If none specified, a DefaultHandshakeHandler will be configured by default.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="handshake-interceptors">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.server.HandshakeInterceptor"><![CDATA[
A list of HandshakeInterceptor beans definition and references.
A HandshakeInterceptor can be used to inspect the handshake request and response as well as to pass attributes to the target WebSocketHandler.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="beans:bean">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.server.HandshakeInterceptor"><![CDATA[
A HandshakeInterceptor bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.server.HandshakeInterceptor"><![CDATA[
A reference to a HandshakeInterceptor bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="sockjs-service">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService"><![CDATA[
Configures a DefaultSockJsService for processing HTTP requests from SockJS clients.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="transport-handlers" minOccurs="0" maxOccurs="1">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.sockjs.transport.TransportHandler"><![CDATA[
List of TransportHandler beans to be configured for the current handlers element.
One can choose not to register the default TransportHandlers and/or override those using
custom TransportHandlers.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="beans:bean">
<xsd:annotation>
<xsd:documentation><![CDATA[
A TransportHandler bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to a TransportHandler bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="register-defaults" type="xsd:boolean" default="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Whether or not default TransportHandlers registrations should be added in addition to the ones provided within this element.
Default registrations include XhrPollingTransportHandler, XhrReceivingTransportHandler,
JsonpPollingTransportHandler, JsonpReceivingTransportHandler, XhrStreamingTransportHandler,
EventSourceTransportHandler, HtmlFileTransportHandler, and WebSocketTransportHandler.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
A unique name for the service, mainly for logging purposes.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="client-library-url" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
Transports with no native cross-domain communication (e.g. "eventsource",
"htmlfile") must get a simple page from the "foreign" domain in an invisible
iframe so that code in the iframe can run from a domain local to the SockJS
server. Since the iframe needs to load the SockJS javascript client library,
this property allows specifying where to load it from.
By default this is set to point to
"https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js". However it can
also be set to point to a URL served by the application.
Note that it's possible to specify a relative URL in which case the URL
must be relative to the iframe URL. For example assuming a SockJS endpoint
mapped to "/sockjs", and resulting iframe URL "/sockjs/iframe.html", then the
The relative URL must start with "../../" to traverse up to the location
above the SockJS mapping. In case of a prefix-based Servlet mapping one more
traversal may be needed.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="stream-bytes-limit" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
Minimum number of bytes that can be send over a single HTTP streaming request before it will be closed.
Defaults to 128K (i.e. 128 1024).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="session-cookie-needed" type="xsd:boolean">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
The "cookie_needed" value in the response from the SockJs "/info" endpoint.
This property indicates whether the use of a JSESSIONID cookie is required for the application to function correctly,
e.g. for load balancing or in Java Servlet containers for the use of an HTTP session.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="heartbeat-time" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
The amount of time in milliseconds when the server has not sent any messages and after which the server
should send a heartbeat frame to the client in order to keep the connection from breaking.
The default value is 25,000 (25 seconds).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="disconnect-delay" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
The amount of time in milliseconds before a client is considered disconnected after not having
a receiving connection, i.e. an active connection over which the server can send data to the client.
The default value is 5000.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="message-cache-size" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
The number of server-to-client messages that a session can cache while waiting for
the next HTTP polling request from the client.
The default size is 100.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="websocket-enabled" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
Some load balancers don't support websockets. Set this option to "false" to disable the WebSocket transport on the server side.
The default value is "true".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="scheduler" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
The bean name of a TaskScheduler; a new ThreadPoolTaskScheduler instance will be created if no value is provided.
This scheduler instance will be used for scheduling heart-beat messages.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="message-codec" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
The bean name of a SockJsMessageCodec to use for encoding and decoding SockJS messages.
By default Jackson2SockJsMessageCodec is used requiring the Jackson library to be present on the classpath.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="suppress-cors" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.web.socket.sockjs.support.AbstractSockJsService"><![CDATA[
This option can be used to disable automatic addition of CORS headers for SockJS requests.
The default value is "false".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="stomp-broker-relay">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
Configures a MessageHandler that handles messages by forwarding them to a STOMP broker.
This MessageHandler also opens a default "system" TCP connection to the message
broker that is used for sending messages that originate from the server application (as
opposed to from a client).
The "login", "password", "heartbeat-send-interval" and "heartbeat-receive-interval" attributes
are provided to configure this "system" connection.
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="prefix" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
Comma-separated list of destination prefixes supported by the broker being configured.
Destinations that do not match the given prefix(es) are ignored.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="relay-host" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The STOMP message broker host.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="relay-port" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The STOMP message broker port.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="client-login" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The login to use when creating connections to the STOMP broker on behalf of connected clients.
By default this is set to "guest".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="client-passcode" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The passcode to use when creating connections to the STOMP broker on behalf of connected clients.
By default this is set to "guest".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="system-login" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The login for the shared "system" connection used to send messages to
the STOMP broker from within the application, i.e. messages not associated
with a specific client session (e.g. REST/HTTP request handling method).
By default this is set to "guest".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="system-passcode" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The passcode for the shared "system" connection used to send messages to
the STOMP broker from within the application, i.e. messages not associated
with a specific client session (e.g. REST/HTTP request handling method).
By default this is set to "guest".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="heartbeat-send-interval" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The interval, in milliseconds, at which the "system" connection will send heartbeats to the STOMP broker.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="heartbeat-receive-interval" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The interval, in milliseconds, at which the "system" connection expects to receive heartbeats from the STOMP broker.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-startup" type="xsd:boolean">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
Whether or not the StompBrokerRelay should be automatically started as part of its SmartLifecycle,
i.e. at the time of an application context refresh.
Default value is "true".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="virtual-host" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler"><![CDATA[
The value of the "host" header to use in STOMP CONNECT frames sent to the STOMP broker.
This may be useful for example in a cloud environment where the actual host to which
the TCP connection is established is different from the host providing the cloud-based STOMP service.
By default this property is not set.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="simple-broker">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler"><![CDATA[
Configures a SimpleBrokerMessageHandler that handles messages as a simple message broker implementation.
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="prefix" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
Comma-separated list of destination prefixes supported by the broker being configured.
Destinations that do not match the given prefix(es) are ignored.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="heartbeat" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
Configure the value for the heartbeat settings. The first number represents how often the server will
write or send a heartbeat. The second is how often the client should write. 0 means no heartbeats.
By default this is set to "0, 0" unless the scheduler attribute is also set in which case the
default becomes "10000,10000" (in milliseconds).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="scheduler" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
The name of a task TaskScheduler to use for heartbeat support. Setting this property also
automatically sets the heartbeat attribute to "10000, 10000".
By default this attribute is not set.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="channel">
<xsd:sequence>
<xsd:element name="executor" type="channel-executor" minOccurs="0" maxOccurs="1"/>
<xsd:element name="interceptors" type="channel-interceptors" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="channel-executor">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Configuration for the ThreadPoolTaskExecutor that sends messages for the message channel.
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="core-pool-size" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Set the core pool size of the ThreadPoolExecutor.
NOTE: the core pool size is effectively the max pool size when an unbounded queue-capacity is configured (the default).
This is essentially the "Unbounded queues" strategy as explained in java.util.concurrent.ThreadPoolExecutor.
When this strategy is used, the max pool size is effectively ignored.
By default this is set to twice the value of Runtime.availableProcessors().
In an an application where tasks do not block frequently,
the number should be closer to or equal to the number of available CPUs/cores.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="max-pool-size" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Set the max pool size of the ThreadPoolExecutor.
NOTE: when an unbounded queue-capacity is configured (the default), the max pool size is effectively ignored.
See the "Unbounded queues" strategy in java.util.concurrent.ThreadPoolExecutor for more details.
By default this is set to Integer.MAX_VALUE.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="keep-alive-seconds" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Set the time limit for which threads may remain idle before being terminated.
If there are more than the core number of threads currently in the pool, after waiting this amount of time without
processing a task, excess threads will be terminated. This overrides any value set in the constructor.
By default this is set to 60.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="queue-capacity" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Set the queue capacity for the ThreadPoolExecutor.
NOTE: when an unbounded queue-capacity is configured (the default) the core pool size is effectively the max pool size.
This is essentially the "Unbounded queues" strategy as explained in java.util.concurrent.ThreadPoolExecutor.
When this strategy is used, the max pool size is effectively ignored.
By default this is set to Integer.MAX_VALUE.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="channel-interceptors">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.messaging.support.ChannelInterceptor"><![CDATA[
List of ChannelInterceptor beans to be used with this channel.
Empty by default.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="beans:bean">
<xsd:annotation>
<xsd:documentation><![CDATA[
A ChannelInterceptor bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to a ChannelInterceptor bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<!-- Elements definitions -->
<xsd:element name="handlers">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configures WebSocket support by registering a SimpleUrlHandlerMapping and mapping
paths to registered WebSocketHandlers.
If a sockjs service is configured within this element, then a
SockJsHttpRequestHandler will handle
requests mapped to the given path.
Otherwise a WebSocketHttpRequestHandler
will be registered for that purpose.
See EnableWebSocket Javadoc for
information on code-based alternatives to enabling WebSocket support.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="mapping" type="mapping" minOccurs="1" maxOccurs="unbounded"/>
<xsd:element name="handshake-handler" type="handshake-handler" minOccurs="0" maxOccurs="1"/>
<xsd:element name="handshake-interceptors" type="handshake-interceptors" minOccurs="0" maxOccurs="1"/>
<xsd:element name="sockjs" type="sockjs-service" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="order" type="xsd:token">
<xsd:annotation>
<xsd:documentation><![CDATA[
Order value for this SimpleUrlHandlerMapping.
Default value is 1.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="allowed-origins" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure allowed {@code Origin} header values. Multiple origins may be specified
as a comma-separated list.
This check is mostly designed for browser clients. There is noting preventing other
types of client to modify the Origin header value.
When SockJS is enabled and allowed origins are restricted, transport types that do not
use {@code Origin} headers for cross origin requests (jsonp-polling, iframe-xhr-polling,
iframe-eventsource and iframe-htmlfile) are disabled. As a consequence, IE6/IE7 won't be
supported anymore and IE8/IE9 will only be supported without cookies.
By default, all origins are allowed.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="message-broker">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configures broker-backed messaging over WebSocket using a higher-level messaging sub-protocol.
Registers a SimpleUrlHandlerMapping and maps paths to registered Controllers.
A StompSubProtocolHandler is registered to handle various versions of the STOMP protocol.
See EnableWebSocketMessageBroker javadoc for information on code-based alternatives to enabling broker-backed messaging.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="transport" minOccurs="0" maxOccurs="1">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure options related to the processing of messages received from and sent to WebSocket clients.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="decorator-factories" maxOccurs="1" minOccurs="0">
<xsd:complexType>
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory"><![CDATA[
Configure one or more factories to decorate the handler used to process WebSocket
messages. This may be useful for some advanced use cases, for example to allow
Spring Security to forcibly close the WebSocket session when the corresponding
HTTP session expires.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="beans:bean">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory"><![CDATA[
A WebSocketHandlerDecoratorFactory bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref">
<xsd:annotation>
<xsd:documentation source="org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory"><![CDATA[
A reference to a WebSocketHandlerDecoratorFactory bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="message-size" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure the maximum size for an incoming sub-protocol message.
For example a STOMP message may be received as multiple WebSocket messages
or multiple HTTP POST requests when SockJS fallback options are in use.
In theory a WebSocket message can be almost unlimited in size.
In practice WebSocket servers impose limits on incoming message size.
STOMP clients for example tend to split large messages around 16K
boundaries. Therefore a server must be able to buffer partial content
and decode when enough data is received. Use this property to configure
the max size of the buffer to use.
The default value is 64K (i.e. 64 * 1024).
NOTE that the current version 1.2 of the STOMP spec
does not specifically discuss how to send STOMP messages over WebSocket.
Version 2 of the spec will but in the mean time existing client libraries
have already established a practice that servers must handle.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="send-timeout" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure a time limit (in milliseconds) for the maximum amount of a time
allowed when sending messages to a WebSocket session or writing to an
HTTP response when SockJS fallback option are in use.
In general WebSocket servers expect that messages to a single WebSocket
session are sent from a single thread at a time. This is automatically
guaranteed when using {@code @EnableWebSocketMessageBroker} configuration.
If message sending is slow, or at least slower than rate of messages sending,
subsequent messages are buffered until either the {@code sendTimeLimit}
or the {@code sendBufferSizeLimit} are reached at which point the session
state is cleared and an attempt is made to close the session.
NOTE that the session time limit is checked only
on attempts to send additional messages. So if only a single message is
sent and it hangs, the session will not time out until another message is
sent or the underlying physical socket times out. So this is not a
replacement for WebSocket server or HTTP connection timeout but is rather
intended to control the extent of buffering of unsent messages.
NOTE that closing the session may not succeed in
actually closing the physical socket and may also hang. This is true
especially when using blocking IO such as the BIO connector in Tomcat
that is used by default on Tomcat 7. Therefore it is recommended to ensure
the server is using non-blocking IO such as Tomcat's NIO connector that
is used by default on Tomcat 8. If you must use blocking IO consider
customizing OS-level TCP settings, for example
{@code /proc/sys/net/ipv4/tcp_retries2} on Linux.
The default value is 10 seconds (i.e. 10 * 10000).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="send-buffer-size" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure the maximum amount of data to buffer when sending messages
to a WebSocket session, or an HTTP response when SockJS fallback
option are in use.
In general WebSocket servers expect that messages to a single WebSocket
session are sent from a single thread at a time. This is automatically
guaranteed when using {@code @EnableWebSocketMessageBroker} configuration.
If message sending is slow, or at least slower than rate of messages sending,
subsequent messages are buffered until either the {@code sendTimeLimit}
or the {@code sendBufferSizeLimit} are reached at which point the session
state is cleared and an attempt is made to close the session.
NOTE that closing the session may not succeed in
actually closing the physical socket and may also hang. This is true
especially when using blocking IO such as the BIO connector in Tomcat
configured by default on Tomcat 7. Therefore it is recommended to ensure
the server is using non-blocking IO such as Tomcat's NIO connector used
by default on Tomcat 8. If you must use blocking IO consider customizing
OS-level TCP settings, for example {@code /proc/sys/net/ipv4/tcp_retries2}
on Linux.
The default value is 512K (i.e. 512 * 1024).
@param sendBufferSizeLimit the maximum number of bytes to buffer when
sending messages; if the value is less than or equal to 0 then buffering
is effectively disabled.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="stomp-endpoint" minOccurs="1" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Registers STOMP over WebSocket endpoints.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="handshake-handler" type="handshake-handler" minOccurs="0" maxOccurs="1"/>
<xsd:element name="handshake-interceptors" type="handshake-interceptors" minOccurs="0" maxOccurs="1"/>
<xsd:element name="sockjs" type="sockjs-service" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="path" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
A path that maps a particular message destination to a handler method.
Exact path mapping URIs (such as "/myPath") are supported as well as Ant-stype path patterns (such as /myPath/**).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="allowed-origins" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure allowed {@code Origin} header values. Multiple origins may be specified
as a comma-separated list.
This check is mostly designed for browser clients. There is noting preventing other
types of client to modify the Origin header value.
When SockJS is enabled and allowed origins are restricted, transport types that do not
use {@code Origin} headers for cross origin requests (jsonp-polling, iframe-xhr-polling,
iframe-eventsource and iframe-htmlfile) are disabled. As a consequence, IE6/IE7 won't be
supported anymore and IE8/IE9 will only be supported without cookies.
By default, all origins are allowed.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:choice>
<xsd:element name="simple-broker" type="simple-broker"/>
<xsd:element name="stomp-broker-relay" type="stomp-broker-relay"/>
</xsd:choice>
<xsd:element name="argument-resolvers" minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configures HandlerMethodArgumentResolver types to support custom controller method argument types.
Using this option does not override the built-in support for resolving handler method arguments.
To customize the built-in support for argument resolution configure WebSocketAnnotationMethodMessageHandler directly.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="beans:bean" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
The HandlerMethodArgumentResolver (or WebArgumentResolver for backwards compatibility) bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to a HandlerMethodArgumentResolver bean definition.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java:org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
<xsd:element name="return-value-handlers" minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configures HandlerMethodReturnValueHandler types to support custom controller method return value handling.
Using this option does not override the built-in support for handling return values.
To customize the built-in support for handling return values configure WebSocketAnnotationMethodMessageHandler directly.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="beans:bean" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
The HandlerMethodReturnValueHandler bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to a HandlerMethodReturnValueHandler bean definition.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java:org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
<xsd:element name="message-converters" minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure the message converters to use when extracting the payload of messages in annotated methods
and when sending messages (e.g. through the "broker" SimpMessagingTemplate.
MessageConverter registrations provided here will take precedence over MessageConverter types registered by default.
Also see the register-defaults attribute if you want to turn off default registrations entirely.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="beans:bean">
<xsd:annotation>
<xsd:documentation><![CDATA[
A MessageConverter bean definition.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="beans:ref">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to an HttpMessageConverter bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="register-defaults" type="xsd:boolean" default="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Whether or not default MessageConverter registrations should be added in addition to the ones provided within this element.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="client-inbound-channel" type="channel" minOccurs="0" maxOccurs="1">
<xsd:annotation>
<xsd:documentation><![CDATA[
The channel for receiving messages from clients (e.g. WebSocket clients).
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="client-outbound-channel" type="channel" minOccurs="0" maxOccurs="1">
<xsd:annotation>
<xsd:documentation><![CDATA[
The channel for sending messages to clients (e.g. WebSocket clients).
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="broker-channel" type="channel" minOccurs="0" maxOccurs="1">
<xsd:annotation>
<xsd:documentation><![CDATA[
The channel for sending messages with translated user destinations.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="application-destination-prefix" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Comma-separated list of prefixes to match to the destinations of handled messages.
Messages whose destination does not start with one of the configured prefixes are ignored.
Prefix is removed from the destination part and then messages are delegated to
@SubscribeMapping and @MessageMapping}annotated methods.
Prefixes without a trailing slash will have one appended automatically.
By default the list of prefixes is empty in which case all destinations match.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="user-destination-prefix" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The prefix used to identify user destinations.
Any destinations that do not start with the given prefix are not be resolved.
The default value is "/user/".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="path-matcher" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to the PathMatcher to use to match the destinations of incoming
messages to @MessageMapping and @SubscribeMapping methods.
By default AntPathMatcher is configured.
However applications may provide an AntPathMatcher instance
customized to use "." (commonly used in messaging) instead of "/" as path
separator or provide a completely different PathMatcher implementation.
Note that the configured PathMatcher is only used for matching the
portion of the destination after the configured prefix. For example given
application destination prefix "/app" and destination "/app/price.stock.**",
the message might be mapped to a controller with "price" and "stock.**"
as its type and method-level mappings respectively.
When the simple broker is enabled, the PathMatcher configured here is
also used to match message destinations when brokering messages.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java:org.springframework.util.PathMatcher" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order" type="xsd:token">
<xsd:annotation>
<xsd:documentation><![CDATA[
Order value for this SimpleUrlHandlerMapping.
Default value is 1.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:schema>

9
spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package org.springframework.web.socket.config; @@ -18,6 +18,7 @@ package org.springframework.web.socket.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.hamcrest.Matchers;
@ -180,8 +181,10 @@ public class MessageBrokerBeanDefinitionParserTests { @@ -180,8 +181,10 @@ public class MessageBrokerBeanDefinitionParserTests {
SimpleBrokerMessageHandler brokerMessageHandler = this.appContext.getBean(SimpleBrokerMessageHandler.class);
assertNotNull(brokerMessageHandler);
assertEquals(Arrays.asList("/topic", "/queue"),
new ArrayList<String>(brokerMessageHandler.getDestinationPrefixes()));
Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<String>(prefixes));
assertNotNull(brokerMessageHandler.getTaskScheduler());
assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
List<Class<? extends MessageHandler>> subscriberTypes =
Arrays.<Class<? extends MessageHandler>>asList(SimpAnnotationMethodMessageHandler.class,

22
spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package org.springframework.web.socket.config.annotation;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
@ -37,6 +38,7 @@ import org.springframework.messaging.handler.annotation.SendTo; @@ -37,6 +38,7 @@ import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.simp.user.UserDestinationMessageHandler;
@ -44,6 +46,7 @@ import org.springframework.messaging.support.AbstractSubscribableChannel; @@ -44,6 +46,7 @@ import org.springframework.messaging.support.AbstractSubscribableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
import org.springframework.messaging.support.ImmutableMessageChannelInterceptor;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.HandlerMapping;
@ -149,14 +152,18 @@ public class WebSocketMessageBrokerConfigurationSupportTests { @@ -149,14 +152,18 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
@Test
public void messageBrokerSockJsTaskScheduler() {
public void taskScheduler() {
ApplicationContext config = createConfig(TestChannelConfig.class, TestConfigurer.class);
ThreadPoolTaskScheduler taskScheduler =
config.getBean("messageBrokerSockJsTaskScheduler", ThreadPoolTaskScheduler.class);
String name = "messageBrokerSockJsTaskScheduler";
ThreadPoolTaskScheduler taskScheduler = config.getBean(name, ThreadPoolTaskScheduler.class);
ScheduledThreadPoolExecutor executor = taskScheduler.getScheduledThreadPoolExecutor();
assertEquals(Runtime.getRuntime().availableProcessors(), executor.getCorePoolSize());
assertTrue(executor.getRemoveOnCancelPolicy());
SimpleBrokerMessageHandler handler = config.getBean(SimpleBrokerMessageHandler.class);
assertNotNull(handler.getTaskScheduler());
assertArrayEquals(new long[] {15000, 15000}, handler.getHeartbeatValue());
}
@Test
@ -200,6 +207,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests { @@ -200,6 +207,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
@SuppressWarnings("unused")
@Controller
static class TestController {
@ -215,6 +223,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests { @@ -215,6 +223,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
}
}
@SuppressWarnings("unused")
@Configuration
static class TestConfigurer extends AbstractWebSocketMessageBrokerConfigurer {
@ -234,6 +243,13 @@ public class WebSocketMessageBrokerConfigurationSupportTests { @@ -234,6 +243,13 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
registration.setSendTimeLimit(25 * 1000);
registration.setSendBufferSizeLimit(1024 * 1024);
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker()
.setTaskScheduler(mock(TaskScheduler.class))
.setHeartbeatValue(new long[] {15000, 15000});
}
}
@Configuration

284
spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java

@ -16,21 +16,12 @@ @@ -16,21 +16,12 @@
package org.springframework.web.socket.messaging;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import java.nio.ByteBuffer;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -42,6 +33,7 @@ import org.junit.Before; @@ -42,6 +33,7 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.PayloadApplicationEvent;
@ -53,7 +45,6 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -53,7 +45,6 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.TestPrincipal;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompDecoder;
import org.springframework.messaging.simp.stomp.StompEncoder;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.simp.user.DefaultUserSessionRegistry;
@ -103,7 +94,7 @@ public class StompSubProtocolHandlerTests { @@ -103,7 +94,7 @@ public class StompSubProtocolHandlerTests {
}
@Test
public void handleMessageToClientConnected() {
public void handleMessageToClientWithConnectedFrame() {
UserSessionRegistry registry = new DefaultUserSessionRegistry();
this.protocolHandler.setUserSessionRegistry(registry);
@ -120,7 +111,7 @@ public class StompSubProtocolHandlerTests { @@ -120,7 +111,7 @@ public class StompSubProtocolHandlerTests {
}
@Test
public void handleMessageToClientConnectedUniqueUserName() {
public void handleMessageToClientWithDestinationUserNameProvider() {
this.session.setPrincipal(new UniqueUser("joe"));
@ -140,130 +131,66 @@ public class StompSubProtocolHandlerTests { @@ -140,130 +131,66 @@ public class StompSubProtocolHandlerTests {
}
@Test
public void handleMessageToClientConnectedWithHeartbeats() {
public void handleMessageToClientWithSimpConnectAck() {
SockJsSession sockJsSession = Mockito.mock(SockJsSession.class);
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECT);
accessor.setHeartbeat(10000, 10000);
accessor.setAcceptVersion("1.0,1.1");
Message<?> connectMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
headers.setHeartbeat(0,10);
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
this.protocolHandler.handleMessageToClient(sockJsSession, message);
SimpMessageHeaderAccessor ackAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
ackAccessor.setHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER, connectMessage);
ackAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, new long[] {15000, 15000});
Message<byte[]> ackMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, ackAccessor.getMessageHeaders());
this.protocolHandler.handleMessageToClient(this.session, ackMessage);
verify(sockJsSession).disableHeartbeat();
assertEquals(1, this.session.getSentMessages().size());
TextMessage actual = (TextMessage) this.session.getSentMessages().get(0);
assertEquals("CONNECTED\n" + "version:1.1\n" + "heart-beat:15000,15000\n" +
"user-name:joe\n" + "\n" + "\u0000", actual.getPayload());
}
@Test
public void handleMessageToClientConnectAck() {
StompHeaderAccessor connectHeaders = StompHeaderAccessor.create(StompCommand.CONNECT);
connectHeaders.setHeartbeat(10000, 10000);
connectHeaders.setAcceptVersion("1.0,1.1");
Message<?> connectMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, connectHeaders.getMessageHeaders());
SimpMessageHeaderAccessor connectAckHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
connectAckHeaders.setHeader(SimpMessageHeaderAccessor.CONNECT_MESSAGE_HEADER, connectMessage);
Message<byte[]> connectAckMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, connectAckHeaders.getMessageHeaders());
public void handleMessageToClientWithSimpHeartbeat() {
this.protocolHandler.handleMessageToClient(this.session, connectAckMessage);
verifyNoMoreInteractions(this.channel);
// Check CONNECTED reply
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
accessor.setSessionId("s1");
accessor.setUser(new TestPrincipal("joe"));
Message<byte[]> ackMessage = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
this.protocolHandler.handleMessageToClient(this.session, ackMessage);
assertEquals(1, this.session.getSentMessages().size());
TextMessage textMessage = (TextMessage) this.session.getSentMessages().get(0);
List<Message<byte[]>> messages = new StompDecoder().decode(ByteBuffer.wrap(textMessage.getPayload().getBytes()));
assertEquals(1, messages.size());
StompHeaderAccessor replyHeaders = StompHeaderAccessor.wrap(messages.get(0));
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));
TextMessage actual = (TextMessage) this.session.getSentMessages().get(0);
assertEquals("\n", actual.getPayload());
}
@Test
public void eventPublication() {
TestPublisher publisher = new TestPublisher();
UserSessionRegistry registry = new DefaultUserSessionRegistry();
this.protocolHandler.setUserSessionRegistry(registry);
this.protocolHandler.setApplicationEventPublisher(publisher);
this.protocolHandler.afterSessionStarted(this.session, this.channel);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
TextMessage textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
this.protocolHandler.handleMessageToClient(this.session, message);
public void handleMessageToClientWithHeartbeatSuppressingSockJsHeartbeat() throws IOException {
headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
SockJsSession sockJsSession = Mockito.mock(SockJsSession.class);
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
accessor.setHeartbeat(0, 10);
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
this.protocolHandler.handleMessageToClient(sockJsSession, message);
headers = StompHeaderAccessor.create(StompCommand.UNSUBSCRIBE);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
verify(sockJsSession).getPrincipal();
verify(sockJsSession).disableHeartbeat();
verify(sockJsSession).sendMessage(any(WebSocketMessage.class));
verifyNoMoreInteractions(sockJsSession);
this.protocolHandler.afterSessionEnded(this.session, CloseStatus.BAD_DATA, this.channel);
sockJsSession = Mockito.mock(SockJsSession.class);
accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
accessor.setHeartbeat(0, 0);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
this.protocolHandler.handleMessageToClient(sockJsSession, message);
assertEquals("Unexpected events " + publisher.events, 5, publisher.events.size());
assertEquals(SessionConnectEvent.class, publisher.events.get(0).getClass());
assertEquals(SessionConnectedEvent.class, publisher.events.get(1).getClass());
assertEquals(SessionSubscribeEvent.class, publisher.events.get(2).getClass());
assertEquals(SessionUnsubscribeEvent.class, publisher.events.get(3).getClass());
assertEquals(SessionDisconnectEvent.class, publisher.events.get(4).getClass());
verify(sockJsSession).getPrincipal();
verify(sockJsSession).sendMessage(any(WebSocketMessage.class));
verifyNoMoreInteractions(sockJsSession);
}
@Test
public void eventPublicationWithExceptions() {
ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
UserSessionRegistry registry = new DefaultUserSessionRegistry();
this.protocolHandler.setUserSessionRegistry(registry);
this.protocolHandler.setApplicationEventPublisher(publisher);
this.protocolHandler.afterSessionStarted(this.session, this.channel);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
TextMessage textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
verify(this.channel).send(this.messageCaptor.capture());
Message<?> actual = this.messageCaptor.getValue();
assertNotNull(actual);
assertEquals(StompCommand.CONNECT, StompHeaderAccessor.wrap(actual).getCommand());
reset(this.channel);
headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
this.protocolHandler.handleMessageToClient(this.session, message);
assertEquals(1, this.session.getSentMessages().size());
textMessage = (TextMessage) this.session.getSentMessages().get(0);
assertEquals("CONNECTED\n" + "user-name:joe\n" + "\n" + "\u0000", textMessage.getPayload());
this.protocolHandler.afterSessionEnded(this.session, CloseStatus.BAD_DATA, this.channel);
verify(this.channel).send(this.messageCaptor.capture());
actual = this.messageCaptor.getValue();
assertNotNull(actual);
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(actual);
assertEquals(StompCommand.DISCONNECT, accessor.getCommand());
assertEquals("s1", accessor.getSessionId());
assertEquals("joe", accessor.getUser().getName());
}
@Test
public void handleMessageToClientUserDestination() {
public void handleMessageToClientWithUserDestination() {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.MESSAGE);
headers.setMessageId("mess0");
@ -282,7 +209,7 @@ public class StompSubProtocolHandlerTests { @@ -282,7 +209,7 @@ public class StompSubProtocolHandlerTests {
// SPR-12475
@Test
public void handleMessageToClientBinaryWebSocketMessage() {
public void handleMessageToClientWithBinaryWebSocketMessage() {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.MESSAGE);
headers.setMessageId("mess0");
@ -324,16 +251,19 @@ public class StompSubProtocolHandlerTests { @@ -324,16 +251,19 @@ public class StompSubProtocolHandlerTests {
Message<?> actual = this.messageCaptor.getValue();
assertNotNull(actual);
StompHeaderAccessor headers = StompHeaderAccessor.wrap(actual);
assertEquals(StompCommand.CONNECT, headers.getCommand());
assertEquals("s1", headers.getSessionId());
assertNotNull(headers.getSessionAttributes());
assertEquals("joe", headers.getUser().getName());
assertEquals("guest", headers.getLogin());
assertEquals("guest", headers.getPasscode());
assertArrayEquals(new long[] {10000, 10000}, headers.getHeartbeat());
assertEquals(new HashSet<>(Arrays.asList("1.1","1.0")), headers.getAcceptVersion());
assertEquals("s1", SimpMessageHeaderAccessor.getSessionId(actual.getHeaders()));
assertNotNull(SimpMessageHeaderAccessor.getSessionAttributes(actual.getHeaders()));
assertNotNull(SimpMessageHeaderAccessor.getUser(actual.getHeaders()));
assertEquals("joe", SimpMessageHeaderAccessor.getUser(actual.getHeaders()).getName());
assertNotNull(SimpMessageHeaderAccessor.getHeartbeat(actual.getHeaders()));
assertArrayEquals(new long[] {10000, 10000}, SimpMessageHeaderAccessor.getHeartbeat(actual.getHeaders()));
StompHeaderAccessor stompAccessor = StompHeaderAccessor.wrap(actual);
assertEquals(StompCommand.CONNECT, stompAccessor.getCommand());
assertEquals("guest", stompAccessor.getLogin());
assertEquals("guest", stompAccessor.getPasscode());
assertArrayEquals(new long[] {10000, 10000}, stompAccessor.getHeartbeat());
assertEquals(new HashSet<>(Arrays.asList("1.1","1.0")), stompAccessor.getAcceptVersion());
assertEquals(0, this.session.getSentMessages().size());
}
@ -379,8 +309,9 @@ public class StompSubProtocolHandlerTests { @@ -379,8 +309,9 @@ public class StompSubProtocolHandlerTests {
assertNotNull(mutable.get());
assertFalse(mutable.get());
}
@Test
public void handleMessageFromClientInvalidStompCommand() {
public void handleMessageFromClientWithInvalidStompCommand() {
TextMessage textMessage = new TextMessage("FOO\n\n\0");
@ -393,6 +324,85 @@ public class StompSubProtocolHandlerTests { @@ -393,6 +324,85 @@ public class StompSubProtocolHandlerTests {
assertTrue(actual.getPayload().startsWith("ERROR"));
}
@Test
public void eventPublication() {
TestPublisher publisher = new TestPublisher();
UserSessionRegistry registry = new DefaultUserSessionRegistry();
this.protocolHandler.setUserSessionRegistry(registry);
this.protocolHandler.setApplicationEventPublisher(publisher);
this.protocolHandler.afterSessionStarted(this.session, this.channel);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
TextMessage textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
this.protocolHandler.handleMessageToClient(this.session, message);
headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
headers = StompHeaderAccessor.create(StompCommand.UNSUBSCRIBE);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
this.protocolHandler.afterSessionEnded(this.session, CloseStatus.BAD_DATA, this.channel);
assertEquals("Unexpected events " + publisher.events, 5, publisher.events.size());
assertEquals(SessionConnectEvent.class, publisher.events.get(0).getClass());
assertEquals(SessionConnectedEvent.class, publisher.events.get(1).getClass());
assertEquals(SessionSubscribeEvent.class, publisher.events.get(2).getClass());
assertEquals(SessionUnsubscribeEvent.class, publisher.events.get(3).getClass());
assertEquals(SessionDisconnectEvent.class, publisher.events.get(4).getClass());
}
@Test
public void eventPublicationWithExceptions() {
ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
UserSessionRegistry registry = new DefaultUserSessionRegistry();
this.protocolHandler.setUserSessionRegistry(registry);
this.protocolHandler.setApplicationEventPublisher(publisher);
this.protocolHandler.afterSessionStarted(this.session, this.channel);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
Message<byte[]> message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
TextMessage textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, this.channel);
verify(this.channel).send(this.messageCaptor.capture());
Message<?> actual = this.messageCaptor.getValue();
assertNotNull(actual);
assertEquals(StompCommand.CONNECT, StompHeaderAccessor.wrap(actual).getCommand());
reset(this.channel);
headers = StompHeaderAccessor.create(StompCommand.CONNECTED);
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, headers.getMessageHeaders());
this.protocolHandler.handleMessageToClient(this.session, message);
assertEquals(1, this.session.getSentMessages().size());
textMessage = (TextMessage) this.session.getSentMessages().get(0);
assertEquals("CONNECTED\n" + "user-name:joe\n" + "\n" + "\u0000", textMessage.getPayload());
this.protocolHandler.afterSessionEnded(this.session, CloseStatus.BAD_DATA, this.channel);
verify(this.channel).send(this.messageCaptor.capture());
actual = this.messageCaptor.getValue();
assertNotNull(actual);
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(actual);
assertEquals(StompCommand.DISCONNECT, accessor.getCommand());
assertEquals("s1", accessor.getSessionId());
assertEquals("joe", accessor.getUser().getName());
}
@Test
public void webSocketScope() {
@ -421,10 +431,10 @@ public class StompSubProtocolHandlerTests { @@ -421,10 +431,10 @@ public class StompSubProtocolHandlerTests {
TextMessage textMessage = new TextMessage(new StompEncoder().encode(message));
this.protocolHandler.handleMessageFromClient(this.session, textMessage, testChannel);
assertEquals(Collections.emptyList(), session.getSentMessages());
assertEquals(Collections.<WebSocketMessage<?>>emptyList(), session.getSentMessages());
this.protocolHandler.afterSessionEnded(this.session, CloseStatus.BAD_DATA, testChannel);
assertEquals(Collections.emptyList(), session.getSentMessages());
assertEquals(Collections.<WebSocketMessage<?>>emptyList(), this.session.getSentMessages());
verify(runnable, times(1)).run();
}

8
spring-websocket/src/test/java/org/springframework/web/socket/messaging/WebSocketStompClientIntegrationTests.java

@ -175,6 +175,14 @@ public class WebSocketStompClientIntegrationTests { @@ -175,6 +175,14 @@ public class WebSocketStompClientIntegrationTests {
received.add((String) payload);
}
});
try {
// Delay send since server processes concurrently
// Ideally order should be preserved or receipts supported (simple broker)
Thread.sleep(500);
}
catch (InterruptedException ex) {
logger.error(ex);
}
session.send(this.topic, this.payload);
}

3
spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml

@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic, /queue"/>
<websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
</websocket:message-broker>
@ -42,5 +42,6 @@ @@ -42,5 +42,6 @@
<bean id="myHandler" class="org.springframework.web.socket.config.TestHandshakeHandler"/>
<bean id="barTestInterceptor" class="org.springframework.web.socket.config.BarTestInterceptor"/>
<bean id="scheduler" class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler"/>
</beans>

Loading…
Cancel
Save