diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java
new file mode 100644
index 0000000000..e2abc57194
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+import org.springframework.messaging.tcp.TcpConnectionHandler;
+import org.springframework.util.concurrent.ListenableFuture;
+
+
+/**
+ * A {@link StompSession} that implements
+ * {@link org.springframework.messaging.tcp.TcpConnectionHandler
+ * TcpConnectionHandler} in order to send and receive messages.
+ *
+ *
A ConnectionHandlingStompSession can be used with any TCP or WebSocket
+ * library that is adapted to the {@code TcpConnectionHandler} contract.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public interface ConnectionHandlingStompSession extends StompSession, TcpConnectionHandler {
+
+ /**
+ * Return a future that will complete when the session is ready for use.
+ */
+ ListenableFuture getSessionFuture();
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionLostException.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionLostException.java
new file mode 100644
index 0000000000..eeb5c96c77
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionLostException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+
+/**
+ * Raised when the connection for a STOMP session is lost rather than closed.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+@SuppressWarnings("serial")
+public class ConnectionLostException extends Exception {
+
+
+ public ConnectionLostException(String message) {
+ super(message);
+ }
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java
new file mode 100644
index 0000000000..2d22bc9717
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompSession.java
@@ -0,0 +1,656 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageDeliveryException;
+import org.springframework.messaging.converter.MessageConversionException;
+import org.springframework.messaging.converter.MessageConverter;
+import org.springframework.messaging.converter.StringMessageConverter;
+import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.messaging.support.MessageHeaderAccessor;
+import org.springframework.messaging.tcp.TcpConnection;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.util.AlternativeJdkIdGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.IdGenerator;
+import org.springframework.util.StringUtils;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.ListenableFutureCallback;
+import org.springframework.util.concurrent.SettableListenableFuture;
+
+
+/**
+ * Default implementation of {@link ConnectionHandlingStompSession}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public class DefaultStompSession implements ConnectionHandlingStompSession {
+
+ private static Log logger = LogFactory.getLog(DefaultStompSession.class);
+
+ private static final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
+
+ public static final byte[] EMPTY_PAYLOAD = new byte[0];
+
+ /* STOMP spec: receiver SHOULD take into account an error margin */
+ private static final long HEARTBEAT_MULTIPLIER = 3;
+
+ private static final Message HEARTBEAT;
+
+
+ static {
+ StompHeaderAccessor accessor = StompHeaderAccessor.createForHeartbeat();
+ HEARTBEAT = MessageBuilder.createMessage(StompDecoder.HEARTBEAT_PAYLOAD, accessor.getMessageHeaders());
+ }
+
+
+ private final String sessionId;
+
+ private final StompSessionHandler sessionHandler;
+
+ private final StompHeaders connectHeaders;
+
+ private final SettableListenableFuture sessionFuture = new SettableListenableFuture();
+
+ private MessageConverter converter = new StringMessageConverter();
+
+ private TaskScheduler taskScheduler;
+
+ private long receiptTimeLimit = 15 * 1000;
+
+ private volatile boolean autoReceiptEnabled;
+
+
+ private volatile TcpConnection connection;
+
+ private final AtomicInteger subscriptionIndex = new AtomicInteger();
+
+ private final Map subscriptions = new ConcurrentHashMap(4);
+
+ private final AtomicInteger receiptIndex = new AtomicInteger();
+
+ private final Map receiptHandlers = new ConcurrentHashMap(4);
+
+ /* Whether the client is willfully closing the connection */
+ private volatile boolean closing = false;
+
+
+ /**
+ * Create a new session.
+ *
+ * @param sessionHandler the application handler for the session
+ * @param connectHeaders headers for the STOMP CONNECT frame
+ */
+ public DefaultStompSession(StompSessionHandler sessionHandler, StompHeaders connectHeaders) {
+ Assert.notNull(sessionHandler, "'sessionHandler' is required.");
+ Assert.notNull(connectHeaders, "'connectHeaders' is required.");
+ this.sessionId = idGenerator.generateId().toString();
+ this.sessionHandler = sessionHandler;
+ this.connectHeaders = connectHeaders;
+ }
+
+
+ @Override
+ public String getSessionId() {
+ return this.sessionId;
+ }
+
+ /**
+ * Return the configured session handler.
+ */
+ public StompSessionHandler getSessionHandler() {
+ return this.sessionHandler;
+ }
+
+ @Override
+ public ListenableFuture getSessionFuture() {
+ return this.sessionFuture;
+ }
+
+ /**
+ * Set the {@link MessageConverter} to use to convert the payload of incoming
+ * and outgoing messages to and from {@code byte[]} based on object type, or
+ * expected object type, and the "content-type" header.
+ *
By default, {@link StringMessageConverter} is configured.
+ * @param messageConverter the message converter to use
+ */
+ public void setMessageConverter(MessageConverter messageConverter) {
+ Assert.notNull(messageConverter, "'messageConverter' must not be null");
+ this.converter = messageConverter;
+ }
+
+ /**
+ * Return the configured {@link MessageConverter}.
+ */
+ public MessageConverter getMessageConverter() {
+ return this.converter;
+ }
+
+ /**
+ * Configure the TaskScheduler to use for receipt tracking.
+ */
+ public void setTaskScheduler(TaskScheduler taskScheduler) {
+ this.taskScheduler = taskScheduler;
+ }
+
+ /**
+ * Return the configured TaskScheduler to use for receipt tracking.
+ */
+ public TaskScheduler getTaskScheduler() {
+ return this.taskScheduler;
+ }
+
+ /**
+ * Configure the time in milliseconds before a receipt expires.
+ *
By default set to 15,000 (15 seconds).
+ */
+ public void setReceiptTimeLimit(long receiptTimeLimit) {
+ Assert.isTrue(receiptTimeLimit > 0);
+ this.receiptTimeLimit = receiptTimeLimit;
+ }
+
+ /**
+ * Return the configured time limit before a receipt expires.
+ */
+ public long getReceiptTimeLimit() {
+ return this.receiptTimeLimit;
+ }
+
+ @Override
+ public void setAutoReceipt(boolean autoReceiptEnabled) {
+ this.autoReceiptEnabled = autoReceiptEnabled;
+ }
+
+ /**
+ * Whether receipt headers should be automatically added.
+ */
+ public boolean isAutoReceiptEnabled() {
+ return this.autoReceiptEnabled;
+ }
+
+
+ @Override
+ public boolean isConnected() {
+ return this.connection != null;
+ }
+
+ @Override
+ public Receiptable send(String destination, Object payload) {
+ StompHeaders stompHeaders = new StompHeaders();
+ stompHeaders.setDestination(destination);
+ return send(stompHeaders, payload);
+ }
+
+ @Override
+ public Receiptable send(StompHeaders stompHeaders, Object payload) {
+ Assert.hasText(stompHeaders.getDestination(), "'destination' header is required");
+
+ String receiptId = checkOrAddReceipt(stompHeaders);
+ Receiptable receiptable = new ReceiptHandler(receiptId);
+
+ StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.SEND);
+ accessor.addNativeHeaders(stompHeaders);
+ Message message = createMessage(accessor, payload);
+ execute(message);
+
+ return receiptable;
+ }
+
+ private String checkOrAddReceipt(StompHeaders stompHeaders) {
+ String receiptId = stompHeaders.getReceipt();
+ if (isAutoReceiptEnabled() && receiptId == null) {
+ receiptId = String.valueOf(DefaultStompSession.this.receiptIndex.getAndIncrement());
+ stompHeaders.setReceipt(receiptId);
+ }
+ return receiptId;
+ }
+
+ private StompHeaderAccessor createHeaderAccessor(StompCommand command) {
+ StompHeaderAccessor accessor = StompHeaderAccessor.create(command);
+ accessor.setSessionId(this.sessionId);
+ accessor.setLeaveMutable(true);
+ return accessor;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Message createMessage(StompHeaderAccessor accessor, Object payload) {
+ accessor.updateSimpMessageHeadersFromStompHeaders();
+ Message message;
+ if (payload == null) {
+ message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
+ }
+ else if (payload instanceof byte[]) {
+ message = MessageBuilder.createMessage((byte[]) payload, accessor.getMessageHeaders());
+ }
+ else {
+ message = (Message) getMessageConverter().toMessage(payload, accessor.getMessageHeaders());
+ accessor.updateStompHeadersFromSimpMessageHeaders();
+ if (message == null) {
+ throw new MessageConversionException("Unable to convert payload with type='" +
+ payload.getClass().getName() + "', contentType='" + accessor.getContentType() +
+ "', converter=[" + getMessageConverter() + "]");
+ }
+ }
+ return message;
+ }
+
+ private void execute(Message message) {
+ StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Sending " + accessor.getDetailedLogMessage(message.getPayload()));
+ }
+ TcpConnection conn = this.connection;
+ Assert.state(conn != null, "Connection closed");
+ try {
+ conn.send(message).get();
+ }
+ catch (ExecutionException ex) {
+ throw new MessageDeliveryException(message, ex.getCause());
+ }
+ catch (Throwable ex) {
+ throw new MessageDeliveryException(message, ex);
+ }
+ }
+
+ @Override
+ public Subscription subscribe(String destination, StompFrameHandler handler) {
+ StompHeaders stompHeaders = new StompHeaders();
+ stompHeaders.setDestination(destination);
+ return subscribe(stompHeaders, handler);
+ }
+
+ @Override
+ public Subscription subscribe(StompHeaders stompHeaders, StompFrameHandler handler) {
+ String destination = stompHeaders.getDestination();
+ Assert.hasText(destination, "'destination' is required");
+ Assert.notNull(handler, "'handler' is required");
+
+ String subscriptionId = stompHeaders.getId();
+ if (!StringUtils.hasText(subscriptionId)) {
+ subscriptionId = String.valueOf(DefaultStompSession.this.subscriptionIndex.getAndIncrement());
+ stompHeaders.setId(subscriptionId);
+ }
+ String receiptId = checkOrAddReceipt(stompHeaders);
+ Subscription subscription = new DefaultSubscription(subscriptionId, destination, receiptId, handler);
+
+ StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.SUBSCRIBE);
+ accessor.addNativeHeaders(stompHeaders);
+ Message message = createMessage(accessor, EMPTY_PAYLOAD);
+ execute(message);
+
+ return subscription;
+ }
+
+ private void unsubscribe(String id) {
+ StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.UNSUBSCRIBE);
+ accessor.setSubscriptionId(id);
+ Message message = createMessage(accessor, EMPTY_PAYLOAD);
+ execute(message);
+ }
+
+ @Override
+ public void disconnect() {
+ this.closing = true;
+ try {
+ StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.DISCONNECT);
+ Message message = createMessage(accessor, EMPTY_PAYLOAD);
+ execute(message);
+ }
+ finally {
+ resetConnection();
+ }
+ }
+
+ // TcpConnectionHandler
+
+ @Override
+ public void afterConnected(TcpConnection connection) {
+ this.connection = connection;
+ if (logger.isDebugEnabled()) {
+ logger.debug("Connection established in session id=" + this.sessionId);
+ }
+ StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.CONNECT);
+ accessor.addNativeHeaders(this.connectHeaders);
+ accessor.setAcceptVersion("1.1,1.2");
+ Message message = createMessage(accessor, EMPTY_PAYLOAD);
+ execute(message);
+ }
+
+ @Override
+ public void afterConnectFailure(Throwable ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Failed to connect session id=" + this.sessionId, ex);
+ }
+ this.sessionFuture.setException(ex);
+ this.sessionHandler.handleTransportError(this, ex);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
+ accessor.setSessionId(this.sessionId);
+ StompCommand command = accessor.getCommand();
+ Map> nativeHeaders = accessor.getNativeHeaders();
+ StompHeaders stompHeaders = StompHeaders.readOnlyStompHeaders(nativeHeaders);
+ boolean isHeartbeat = accessor.isHeartbeat();
+ if (logger.isTraceEnabled()) {
+ logger.trace("Received " + accessor.getDetailedLogMessage(message.getPayload()));
+ }
+ try {
+ if (StompCommand.MESSAGE.equals(command)) {
+ DefaultSubscription subscription = this.subscriptions.get(stompHeaders.getSubscription());
+ if (subscription != null) {
+ invokeHandler(subscription.getHandler(), message, stompHeaders);
+ }
+ else if (logger.isDebugEnabled()) {
+ logger.debug("No handler for: " + accessor.getDetailedLogMessage(message.getPayload()) +
+ ". Perhaps just unscubscribed?");
+ }
+ }
+ else {
+ if (StompCommand.RECEIPT.equals(command)) {
+ String receiptId = stompHeaders.getReceiptId();
+ ReceiptHandler handler = this.receiptHandlers.get(receiptId);
+ if (handler != null) {
+ handler.handleReceiptReceived();
+ }
+ else if (logger.isDebugEnabled()) {
+ logger.debug("No matching receipt: " + accessor.getDetailedLogMessage(message.getPayload()));
+ }
+ }
+ else if (StompCommand.CONNECTED.equals(command)) {
+ initHeartbeats(stompHeaders);
+ this.sessionFuture.set(this);
+ this.sessionHandler.afterConnected(this, stompHeaders);
+ }
+ else if (StompCommand.ERROR.equals(command)) {
+ invokeHandler(this.sessionHandler, message, stompHeaders);
+ }
+ else if (!isHeartbeat && logger.isTraceEnabled()) {
+ logger.trace("Message not handled.");
+ }
+ }
+ }
+ catch (Throwable ex) {
+ this.sessionHandler.handleException(this, command, stompHeaders, message.getPayload(), ex);
+ }
+ }
+
+ private void invokeHandler(StompFrameHandler handler, Message message, StompHeaders stompHeaders) {
+ if (message.getPayload().length == 0) {
+ handler.handleFrame(stompHeaders, null);
+ return;
+ }
+ Type type = handler.getPayloadType(stompHeaders);
+ Class> payloadType = ResolvableType.forType(type).getRawClass();
+ Object object = getMessageConverter().fromMessage(message, payloadType);
+ if (object == null) {
+ throw new MessageConversionException("No suitable converter, payloadType=" + payloadType);
+ }
+ 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);
+ 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;
+ this.connection.onReadInactivity(new ReadInactivityTask(), interval);
+ }
+ }
+
+ @Override
+ public void handleFailure(Throwable ex) {
+ try {
+ this.sessionFuture.setException(ex); // no-op if already set
+ this.sessionHandler.handleTransportError(this, ex);
+ }
+ catch (Throwable ex2) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Uncaught failure while handling transport failure", ex2);
+ }
+ }
+ }
+
+ @Override
+ public void afterConnectionClosed() {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Connection closed session id=" + this.sessionId);
+ }
+ if (!this.closing) {
+ handleFailure(new ConnectionLostException("Connection closed"));
+ resetConnection();
+ }
+ }
+
+ private void resetConnection() {
+ TcpConnection> conn = this.connection;
+ this.connection = null;
+ if (conn != null) {
+ try {
+ conn.close();
+ }
+ catch (Throwable ex) {
+ // Ignore
+ }
+ }
+ }
+
+
+
+ private class ReceiptHandler implements Receiptable {
+
+ private final String receiptId;
+
+ private final List receiptCallbacks = new ArrayList(2);
+
+ private final List receiptLostCallbacks = new ArrayList(2);
+
+ private ScheduledFuture> future;
+
+ private Boolean result;
+
+
+ public ReceiptHandler(String receiptId) {
+ this.receiptId = receiptId;
+ if (this.receiptId != null) {
+ initReceiptHandling();
+ }
+ }
+
+ private void initReceiptHandling() {
+ Assert.notNull(getTaskScheduler(), "To track receipts a TaskScheduler must be configured");
+ DefaultStompSession.this.receiptHandlers.put(this.receiptId, this);
+ Date startTime = new Date(System.currentTimeMillis() + getReceiptTimeLimit());
+ this.future = getTaskScheduler().schedule(new Runnable() {
+ @Override
+ public void run() {
+ handleReceiptNotReceived();
+ }
+ }, startTime);
+ }
+
+ @Override
+ public String getReceiptId() {
+ return this.receiptId;
+ }
+
+ @Override
+ public void addReceiptTask(Runnable task) {
+ addTask(task, true);
+ }
+
+ @Override
+ public void addReceiptLostTask(Runnable task) {
+ addTask(task, false);
+ }
+
+ private void addTask(Runnable task, boolean successTask) {
+ Assert.notNull(this.receiptId, "To track receipts, set autoReceiptEnabled=true or add 'receiptId' header");
+ synchronized (this) {
+ if (this.result != null && this.result == successTask) {
+ invoke(Arrays.asList(task));
+ }
+ else {
+ if (successTask) {
+ this.receiptCallbacks.add(task);
+ }
+ else {
+ this.receiptLostCallbacks.add(task);
+ }
+ }
+ }
+ }
+
+ private void invoke(List callbacks) {
+ for (Runnable runnable : callbacks) {
+ try {
+ runnable.run();
+ }
+ catch (Throwable ex) {
+ // Ignore
+ }
+ }
+ }
+
+ public void handleReceiptReceived() {
+ handleInternal(true);
+ }
+
+ public void handleReceiptNotReceived() {
+ handleInternal(false);
+ }
+
+ private void handleInternal(boolean result) {
+ synchronized (this) {
+ if (this.result != null) {
+ return;
+ }
+ this.result = result;
+ invoke(result ? this.receiptCallbacks : this.receiptLostCallbacks);
+ DefaultStompSession.this.receiptHandlers.remove(this.receiptId);
+ if (this.future != null) {
+ this.future.cancel(true);
+ }
+ }
+ }
+ }
+
+ private class DefaultSubscription extends ReceiptHandler implements Subscription {
+
+ private final String id;
+
+ private final String destination;
+
+ private final StompFrameHandler handler;
+
+
+ public DefaultSubscription(String id, String destination, String receiptId, StompFrameHandler handler) {
+ super(receiptId);
+ Assert.notNull(destination, "'destination' is required");
+ Assert.notNull(handler, "'handler' handler is required");
+ this.id = id;
+ this.destination = destination;
+ this.handler = handler;
+
+ DefaultStompSession.this.subscriptions.put(id, this);
+ }
+
+ @Override
+ public String getSubscriptionId() {
+ return this.id;
+ }
+
+ public String getDestination() {
+ return this.destination;
+ }
+
+ public StompFrameHandler getHandler() {
+ return this.handler;
+ }
+
+ @Override
+ public void unsubscribe() {
+ DefaultStompSession.this.subscriptions.remove(getSubscriptionId());
+ DefaultStompSession.this.unsubscribe(getSubscriptionId());
+ }
+
+ @Override
+ public String toString() {
+ return "Subscription [id=" + getSubscriptionId() + ", destination='" + getDestination() +
+ "', receiptId='" + getReceiptId() + "', handler=" + getHandler() + "]";
+ }
+ }
+
+ private class WriteInactivityTask implements Runnable {
+
+ @Override
+ public void run() {
+ TcpConnection conn = connection;
+ if (conn != null) {
+ conn.send(HEARTBEAT).addCallback(
+ new ListenableFutureCallback() {
+ public void onSuccess(Void result) {
+ }
+ public void onFailure(Throwable ex) {
+ handleFailure(ex);
+ }
+ });
+ }
+ }
+ }
+
+ private class ReadInactivityTask implements Runnable {
+
+ @Override
+ public void run() {
+ closing = true;
+ String error = "Server has gone quite. Closing connection in session id=" + sessionId + ".";
+ if (logger.isDebugEnabled()) {
+ logger.debug(error);
+ }
+ handleFailure(new IllegalStateException(error));
+ resetConnection();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/Reactor11TcpStompClient.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/Reactor11TcpStompClient.java
new file mode 100644
index 0000000000..0f04c5330b
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/Reactor11TcpStompClient.java
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+import java.util.Arrays;
+import java.util.Properties;
+
+import reactor.core.Environment;
+import reactor.core.configuration.ConfigurationReader;
+import reactor.core.configuration.DispatcherConfiguration;
+import reactor.core.configuration.DispatcherType;
+import reactor.core.configuration.ReactorConfiguration;
+import reactor.net.netty.tcp.NettyTcpClient;
+import reactor.net.tcp.TcpClient;
+import reactor.net.tcp.spec.TcpClientSpec;
+
+import org.springframework.messaging.Message;
+import org.springframework.messaging.tcp.TcpOperations;
+import org.springframework.messaging.tcp.reactor.Reactor11TcpClient;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.SettableListenableFuture;
+
+/**
+ * A STOMP over TCP client that uses
+ * {@link org.springframework.messaging.tcp.reactor.Reactor11TcpClient
+ * Reactor11TcpClient}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public class Reactor11TcpStompClient extends StompClientSupport {
+
+ private final TcpOperations tcpClient;
+
+
+ /**
+ * Create an instance with host "127.0.0.1" and port 61613.
+ */
+ public Reactor11TcpStompClient() {
+ this("127.0.0.1", 61613);
+ }
+
+ /**
+ * Create an instance with the given host and port.
+ * @param host the host
+ * @param port the port
+ */
+ public Reactor11TcpStompClient(String host, int port) {
+ this.tcpClient = new Reactor11TcpClient(createNettyTcpClient(host, port));
+ }
+
+ private TcpClient, Message> createNettyTcpClient(String host, int port) {
+ return new TcpClientSpec, Message>(NettyTcpClient.class)
+ .env(new Environment(new StompClientDispatcherConfigReader()))
+ .codec(new Reactor11StompCodec(new StompEncoder(), new StompDecoder()))
+ .connect(host, port)
+ .get();
+ }
+
+ /**
+ * Create an instance with a pre-configured TCP client.
+ * @param tcpClient the client to use
+ */
+ public Reactor11TcpStompClient(TcpOperations tcpClient) {
+ this.tcpClient = tcpClient;
+ }
+
+
+ /**
+ * Connect and notify the given {@link StompSessionHandler} when connected
+ * on the STOMP level,
+ * @param handler the handler for the STOMP session
+ * @return ListenableFuture for access to the session when ready for use
+ */
+ public ListenableFuture connect(StompSessionHandler handler) {
+ return connect(null, handler);
+ }
+
+ /**
+ * An overloaded version of {@link #connect(StompSessionHandler)} that
+ * accepts headers to use for the STOMP CONNECT frame.
+ * @param connectHeaders headers to add to the CONNECT frame
+ * @param handler the handler for the STOMP session
+ * @return ListenableFuture for access to the session when ready for use
+ */
+ public ListenableFuture connect(StompHeaders connectHeaders, StompSessionHandler handler) {
+ ConnectionHandlingStompSession session = createSession(connectHeaders, handler);
+ this.tcpClient.connect(session);
+ return session.getSessionFuture();
+ }
+
+ /**
+ * Shut down the client and release resources.
+ */
+ public void shutdown() {
+ this.tcpClient.shutdown();
+ }
+
+
+ /**
+ * A ConfigurationReader with a thread pool-based dispatcher.
+ */
+ private static class StompClientDispatcherConfigReader implements ConfigurationReader {
+
+ @Override
+ public ReactorConfiguration read() {
+ String dispatcherName = "StompClient";
+ DispatcherType dispatcherType = DispatcherType.THREAD_POOL_EXECUTOR;
+ DispatcherConfiguration config = new DispatcherConfiguration(dispatcherName, dispatcherType, 128, 0);
+ return new ReactorConfiguration(Arrays.asList(config), dispatcherName, new Properties());
+ }
+ }
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompClientSupport.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompClientSupport.java
new file mode 100644
index 0000000000..ecd6d6c6ae
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompClientSupport.java
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+import java.util.Arrays;
+
+import org.springframework.messaging.converter.MessageConverter;
+import org.springframework.messaging.converter.StringMessageConverter;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.util.Assert;
+
+
+/**
+ * Base class for STOMP client implementations.
+ *
+ *
Sub-classes can connect over WebSocket or TCP using any library.
+ * When creating a new connection a sub-class can create an instance of
+ * {@link DefaultStompSession} which extends
+ * {@link org.springframework.messaging.tcp.TcpConnectionHandler
+ * TcpConnectionHandler} whose lifecycle methods the sub-class must then invoke.
+ *
+ *
In effect {@code TcpConnectionHandler} and {@code TcpConnection} are the
+ * contracts any sub-class must adapt to while using {@link StompEncoder} and
+ * {@link StompDecoder} to encode and decode STOMP messages.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public abstract class StompClientSupport {
+
+ private MessageConverter messageConverter = new StringMessageConverter();
+
+ private TaskScheduler taskScheduler;
+
+ private long[] defaultHeartbeat = new long[] {10000, 10000};
+
+ private long receiptTimeLimit = 15 * 1000;
+
+
+ /**
+ * Set the {@link MessageConverter} to use to convert the payload of incoming
+ * and outgoing messages to and from {@code byte[]} based on object type
+ * and the "content-type" header.
+ *
By default, {@link StringMessageConverter} is configured.
+ * @param messageConverter the message converter to use
+ */
+ public void setMessageConverter(MessageConverter messageConverter) {
+ Assert.notNull(messageConverter, "'messageConverter' must not be null");
+ this.messageConverter = messageConverter;
+ }
+
+ /**
+ * Return the configured {@link MessageConverter}.
+ */
+ public MessageConverter getMessageConverter() {
+ return this.messageConverter;
+ }
+
+ /**
+ * Configure a scheduler to use for heartbeats and for receipt tracking.
+ *
+ *
Note: some transports have built-in support to work
+ * with heartbeats and therefore do not require a TaskScheduler.
+ * Receipts however, if needed, do require a TaskScheduler to be configured.
+ *
+ *
By default this is not set.
+ */
+ public void setTaskScheduler(TaskScheduler taskScheduler) {
+ this.taskScheduler = taskScheduler;
+ }
+
+ /**
+ * The configured TaskScheduler.
+ */
+ public TaskScheduler getTaskScheduler() {
+ return this.taskScheduler;
+ }
+
+ /**
+ * Configure the default value for the "heart-beat" header of the STOMP
+ * CONNECT frame. The first number represents how often the client will write
+ * or send a heart-beat. The second is how often the server should write.
+ * A value of 0 means no heart-beats.
+ *
By default this is set to "10000,10000" but sub-classes may override
+ * that default and for example set it to "0,0" if they require a
+ * TaskScheduler to be configured first.
+ * @param heartbeat the value for the CONNECT "heart-beat" header
+ * @see
+ * http://stomp.github.io/stomp-specification-1.2.html#Heart-beating
+ */
+ public void setDefaultHeartbeat(long[] heartbeat) {
+ Assert.notNull(heartbeat);
+ Assert.isTrue(heartbeat[0] >= 0 && heartbeat[1] >=0 , "Invalid heart-beat: " + Arrays.toString(heartbeat));
+ this.defaultHeartbeat = heartbeat;
+ }
+
+ /**
+ * Return the configured default heart-beat value, never {@code null}.
+ */
+ public long[] getDefaultHeartbeat() {
+ return this.defaultHeartbeat;
+ }
+
+ /**
+ * Whether heartbeats are enabled. Returns {@code false} if
+ * {@link #setDefaultHeartbeat defaultHeartbeat} is set to "0,0", and
+ * {@code true} otherwise.
+ */
+ public boolean isDefaultHeartbeatEnabled() {
+ return (getDefaultHeartbeat() != null && getDefaultHeartbeat()[0] != 0 && getDefaultHeartbeat()[1] != 0);
+ }
+
+ /**
+ * Configure the number of milliseconds before a receipt is considered expired.
+ *
By default set to 15,000 (15 seconds).
+ */
+ public void setReceiptTimeLimit(long receiptTimeLimit) {
+ Assert.isTrue(receiptTimeLimit > 0);
+ this.receiptTimeLimit = receiptTimeLimit;
+ }
+
+ /**
+ * Return the configured receipt time limit.
+ */
+ public long getReceiptTimeLimit() {
+ return receiptTimeLimit;
+ }
+
+
+ /**
+ * Factory method for create and configure a new session.
+ * @param connectHeaders headers for the STOMP CONNECT frame
+ * @param handler the handler for the STOMP session
+ * @return the created session
+ */
+ protected ConnectionHandlingStompSession createSession(StompHeaders connectHeaders, StompSessionHandler handler) {
+ connectHeaders = processConnectHeaders(connectHeaders);
+ DefaultStompSession session = new DefaultStompSession(handler, connectHeaders);
+ session.setMessageConverter(getMessageConverter());
+ session.setTaskScheduler(getTaskScheduler());
+ session.setReceiptTimeLimit(getReceiptTimeLimit());
+ return session;
+ }
+
+ /**
+ * Further initialize the StompHeaders, for example setting the heart-beat
+ * header if necessary.
+ * @param connectHeaders the headers to modify
+ * @return the modified headers
+ */
+ protected StompHeaders processConnectHeaders(StompHeaders connectHeaders) {
+ connectHeaders = (connectHeaders != null ? connectHeaders : new StompHeaders());
+ if (connectHeaders.getHeartbeat() == null) {
+ connectHeaders.setHeartbeat(getDefaultHeartbeat());
+ }
+ return connectHeaders;
+ }
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java
index bc197db52a..228fbcdb57 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java
@@ -73,6 +73,9 @@ public final class StompEncoder {
DataOutputStream output = new DataOutputStream(baos);
if (SimpMessageType.HEARTBEAT.equals(SimpMessageHeaderAccessor.getMessageType(headers))) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Encoding heartbeat");
+ }
output.write(StompDecoder.HEARTBEAT_PAYLOAD);
}
else {
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompFrameHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompFrameHandler.java
new file mode 100644
index 0000000000..3a5cff050c
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompFrameHandler.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+import java.lang.reflect.Type;
+
+
+/**
+ * Contract to handle a STOMP frame.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public interface StompFrameHandler {
+
+ /**
+ * Invoked before {@link #handleFrame(StompHeaders, Object)} to determine the
+ * type of Object the payload should be converted to.
+ * @param headers the headers of a message
+ */
+ Type getPayloadType(StompHeaders headers);
+
+ /**
+ * Handle a STOMP frame with the payload converted to the target type returned
+ * from {@link #getPayloadType(StompHeaders)}.
+ *
+ * @param headers the headers of the frame
+ * @param payload the payload or {@code null} if there was no payload
+ */
+ void handleFrame(StompHeaders headers, Object payload);
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java
index 4747c9dfdf..3538a63935 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java
@@ -162,7 +162,7 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor {
}
}
- private void updateStompHeadersFromSimpMessageHeaders() {
+ void updateStompHeadersFromSimpMessageHeaders() {
if (getDestination() != null) {
setNativeHeader(STOMP_DESTINATION_HEADER, getDestination());
}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java
new file mode 100644
index 0000000000..166fcd125b
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java
@@ -0,0 +1,528 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MimeType;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Represents STOMP frame headers.
+ *
+ *
In addition to the normal methods defined by {@link Map}, this class offers
+ * the following convenience methods:
+ *
+ *
{@link #getFirst(String)} return the first value for a header name
+ *
{@link #add(String, String)} add to the list of values for a header name
+ *
{@link #set(String, String)} set a header name to a single string value
+ *
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ * @see
+ * http://stomp.github.io/stomp-specification-1.2.html#Frames_and_Headers
+ */
+public class StompHeaders implements MultiValueMap, Serializable {
+
+ private static final long serialVersionUID = 7514642206528452544L;
+
+
+ // Standard headers (as defined in the spec)
+
+ public static final String CONTENT_TYPE = "content-type"; // SEND, MESSAGE, ERROR
+
+ public static final String CONTENT_LENGTH = "content-length"; // SEND, MESSAGE, ERROR
+
+ public static final String RECEIPT = "receipt"; // any client frame other than CONNECT
+
+ // CONNECT
+
+ public static final String HOST = "host";
+
+ public static final String LOGIN = "login";
+
+ public static final String PASSCODE = "passcode";
+
+ public static final String HEARTBEAT = "heart-beat";
+
+ // CONNECTED
+
+ public static final String SESSION = "session";
+
+ public static final String SERVER = "server";
+
+ // SEND
+
+ public static final String DESTINATION = "destination";
+
+ // SUBSCRIBE, UNSUBSCRIBE
+
+ public static final String ID = "id";
+
+ public static final String ACK = "ack";
+
+ // MESSAGE
+
+ public static final String SUBSCRIPTION = "subscription";
+
+ public static final String MESSAGE_ID = "message-id";
+
+ // RECEIPT
+
+ public static final String RECEIPT_ID = "receipt-id";
+
+
+ private final Map> headers;
+
+
+ /**
+ * Create a new instance to be populated with new header values.
+ */
+ public StompHeaders() {
+ this(new LinkedMultiValueMap(4), false);
+ }
+
+ private StompHeaders(Map> headers, boolean readOnly) {
+ Assert.notNull(headers, "'headers' must not be null");
+ if (readOnly) {
+ Map> map = new LinkedMultiValueMap(headers.size());
+ for (Entry> entry : headers.entrySet()) {
+ List values = Collections.unmodifiableList(entry.getValue());
+ map.put(entry.getKey(), values);
+ }
+ this.headers = Collections.unmodifiableMap(map);
+ }
+ else {
+ this.headers = headers;
+ }
+ }
+
+
+ /**
+ * Set the content-type header.
+ * Applies to the SEND, MESSAGE, and ERROR frames.
+ */
+ public void setContentType(MimeType mimeType) {
+ Assert.isTrue(!mimeType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'");
+ Assert.isTrue(!mimeType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'");
+ set(CONTENT_TYPE, mimeType.toString());
+ }
+
+ /**
+ * Return the content-type header value.
+ */
+ public MimeType getContentType() {
+ String value = getFirst(CONTENT_TYPE);
+ return (StringUtils.hasLength(value) ? MimeTypeUtils.parseMimeType(value) : null);
+ }
+
+ /**
+ * Set the content-length header.
+ * Applies to the SEND, MESSAGE, and ERROR frames.
+ */
+ public void setContentLength(long contentLength) {
+ set(CONTENT_LENGTH, Long.toString(contentLength));
+ }
+
+ /**
+ * Return the content-length header or -1 if unknown.
+ */
+ public long getContentLength() {
+ String value = getFirst(CONTENT_LENGTH);
+ return (value != null ? Long.parseLong(value) : -1);
+ }
+
+ /**
+ * Set the receipt header.
+ * Applies to any client frame other than CONNECT.
+ */
+ public void setReceipt(String receipt) {
+ set(RECEIPT, receipt);
+ }
+
+ /**
+ * Get the receipt header.
+ */
+ public String getReceipt() {
+ return getFirst(RECEIPT);
+ }
+
+ /**
+ * Set the host header.
+ * Applies to the CONNECT frame.
+ */
+ public void setHost(String host) {
+ set(HOST, host);
+ }
+
+ /**
+ * Get the host header.
+ */
+ public String getHost() {
+ return getFirst(HOST);
+ }
+
+ /**
+ * Set the login header.
+ * Applies to the CONNECT frame.
+ */
+ public void setLogin(String login) {
+ set(LOGIN, login);
+ }
+
+ /**
+ * Get the login header.
+ */
+ public String getLogin() {
+ return getFirst(LOGIN);
+ }
+
+ /**
+ * Set the passcode header.
+ * Applies to the CONNECT frame.
+ */
+ public void setPasscode(String passcode) {
+ set(PASSCODE, passcode);
+ }
+
+ /**
+ * Get the passcode header.
+ */
+ public String getPasscode() {
+ return getFirst(PASSCODE);
+ }
+
+ /**
+ * Set the heartbeat header.
+ * Applies to the CONNECT and CONNECTED frames.
+ */
+ public void setHeartbeat(long[] heartbeat) {
+ Assert.notNull(heartbeat);
+ String value = heartbeat[0] + "," + heartbeat[1];
+ Assert.isTrue(heartbeat[0] >= 0 && heartbeat[1] >= 0, "Heart-beat values cannot be negative: " + value);
+ set(HEARTBEAT, value);
+ }
+
+ /**
+ * Get the heartbeat header.
+ */
+ public long[] getHeartbeat() {
+ String rawValue = getFirst(HEARTBEAT);
+ if (!StringUtils.hasText(rawValue)) {
+ return null;
+ }
+ String[] rawValues = StringUtils.commaDelimitedListToStringArray(rawValue);
+ return new long[] {Long.valueOf(rawValues[0]), Long.valueOf(rawValues[1])};
+ }
+
+ /**
+ * Whether heartbeats are enabled. Returns {@code false} if
+ * {@link #setHeartbeat} is set to "0,0", and {@code true} otherwise.
+ */
+ public boolean isHeartbeatEnabled() {
+ long[] heartbeat = getHeartbeat();
+ return (heartbeat != null && heartbeat[0] != 0 && heartbeat[1] != 0);
+ }
+
+ /**
+ * Set the session header.
+ * Applies to the CONNECTED frame.
+ */
+ public void setSession(String session) {
+ set(SESSION, session);
+ }
+
+ /**
+ * Get the session header.
+ */
+ public String getSession() {
+ return getFirst(SESSION);
+ }
+
+ /**
+ * Set the server header.
+ * Applies to the CONNECTED frame.
+ */
+ public void setServer(String server) {
+ set(SERVER, server);
+ }
+
+ /**
+ * Get the server header.
+ * Applies to the CONNECTED frame.
+ */
+ public String getServer() {
+ return getFirst(SERVER);
+ }
+
+ /**
+ * Set the destination header.
+ */
+ public void setDestination(String destination) {
+ set(DESTINATION, destination);
+ }
+
+ /**
+ * Get the destination header.
+ * Applies to the SEND, SUBSCRIBE, and MESSAGE frames.
+ */
+ public String getDestination() {
+ return getFirst(DESTINATION);
+ }
+
+ /**
+ * Set the id header.
+ * Applies to the SUBSCR0BE, UNSUBSCRIBE, and ACK or NACK frames.
+ */
+ public void setId(String id) {
+ set(ID, id);
+ }
+
+ /**
+ * Get the id header.
+ */
+ public String getId() {
+ return getFirst(ID);
+ }
+
+ /**
+ * Set the ack header to one of "auto", "client", or "client-individual".
+ * Applies to the SUBSCRIBE and MESSAGE frames.
+ */
+ public void setAck(String ack) {
+ set(ACK, ack);
+ }
+
+ /**
+ * Get the ack header.
+ */
+ public String getAck() {
+ return getFirst(ACK);
+ }
+
+ /**
+ * Set the login header.
+ * Applies to the MESSAGE frame.
+ */
+ public void setSubscription(String subscription) {
+ set(SUBSCRIPTION, subscription);
+ }
+
+ /**
+ * Get the subscription header.
+ */
+ public String getSubscription() {
+ return getFirst(SUBSCRIPTION);
+ }
+
+ /**
+ * Set the message-id header.
+ * Applies to the MESSAGE frame.
+ */
+ public void setMessageId(String messageId) {
+ set(MESSAGE_ID, messageId);
+ }
+
+ /**
+ * Get the message-id header.
+ */
+ public String getMessageId() {
+ return getFirst(MESSAGE_ID);
+ }
+
+ /**
+ * Set the receipt-id header.
+ * Applies to the RECEIPT frame.
+ */
+ public void setReceiptId(String receiptId) {
+ set(RECEIPT_ID, receiptId);
+ }
+
+ /**
+ * Get the receipt header.
+ */
+ public String getReceiptId() {
+ return getFirst(RECEIPT_ID);
+ }
+
+ /**
+ * Return the first header value for the given header name, if any.
+ * @param headerName the header name
+ * @return the first header value, or {@code null} if none
+ */
+ @Override
+ public String getFirst(String headerName) {
+ List headerValues = headers.get(headerName);
+ return headerValues != null ? headerValues.get(0) : null;
+ }
+
+ /**
+ * Add the given, single header value under the given name.
+ * @param headerName the header name
+ * @param headerValue the header value
+ * @throws UnsupportedOperationException if adding headers is not supported
+ * @see #put(String, List)
+ * @see #set(String, String)
+ */
+ @Override
+ public void add(String headerName, String headerValue) {
+ List headerValues = headers.get(headerName);
+ if (headerValues == null) {
+ headerValues = new LinkedList();
+ this.headers.put(headerName, headerValues);
+ }
+ headerValues.add(headerValue);
+ }
+
+ /**
+ * Set the given, single header value under the given name.
+ * @param headerName the header name
+ * @param headerValue the header value
+ * @throws UnsupportedOperationException if adding headers is not supported
+ * @see #put(String, List)
+ * @see #add(String, String)
+ */
+ @Override
+ public void set(String headerName, String headerValue) {
+ List headerValues = new LinkedList();
+ headerValues.add(headerValue);
+ headers.put(headerName, headerValues);
+ }
+
+ @Override
+ public void setAll(Map values) {
+ for (Entry entry : values.entrySet()) {
+ set(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public Map toSingleValueMap() {
+ LinkedHashMap singleValueMap = new LinkedHashMap(this.headers.size());
+ for (Entry> entry : headers.entrySet()) {
+ singleValueMap.put(entry.getKey(), entry.getValue().get(0));
+ }
+ return singleValueMap;
+ }
+
+
+ // Map implementation
+
+ @Override
+ public int size() {
+ return this.headers.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.headers.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return this.headers.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return this.headers.containsValue(value);
+ }
+
+ @Override
+ public List get(Object key) {
+ return this.headers.get(key);
+ }
+
+ @Override
+ public List put(String key, List value) {
+ return this.headers.put(key, value);
+ }
+
+ @Override
+ public List remove(Object key) {
+ return this.headers.remove(key);
+ }
+
+ @Override
+ public void putAll(Map extends String, ? extends List> map) {
+ this.headers.putAll(map);
+ }
+
+ @Override
+ public void clear() {
+ this.headers.clear();
+ }
+
+ @Override
+ public Set keySet() {
+ return this.headers.keySet();
+ }
+
+ @Override
+ public Collection> values() {
+ return this.headers.values();
+ }
+
+ @Override
+ public Set>> entrySet() {
+ return this.headers.entrySet();
+ }
+
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof StompHeaders)) {
+ return false;
+ }
+ StompHeaders otherHeaders = (StompHeaders) other;
+ return this.headers.equals(otherHeaders.headers);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.headers.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return this.headers.toString();
+ }
+
+
+ /**
+ * Return a {@code StompHeaders} object that can only be read, not written to.
+ */
+ public static StompHeaders readOnlyStompHeaders(Map> headers) {
+ return new StompHeaders(headers, true);
+ }
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java
new file mode 100644
index 0000000000..5df7ca1d7f
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+
+/**
+ * Represents a STOMP session with operations to send messages, create
+ * subscriptions and receive messages on those subscriptions.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public interface StompSession {
+
+ /**
+ * Return the id for the session.
+ */
+ String getSessionId();
+
+ /**
+ * Whether the session is connected.
+ */
+ boolean isConnected();
+
+ /**
+ * When enabled, a receipt header is automatically added to future
+ * {@code send} and {@code subscribe} operations on this session, which causes
+ * the server to return a RECEIPT. An application can then use the
+ * {@link StompSession.Receiptable
+ * Receiptable} returned from the operation to track the receipt.
+ *
+ *
A receipt header can also be added manually through the overloaded
+ * methods that accept {@code StompHeaders}.
+ */
+ void setAutoReceipt(boolean enabled);
+
+ /**
+ * Send a message to the specified destination, converting the payload to a
+ * {@code byte[]} with the help of a
+ * {@link org.springframework.messaging.converter.MessageConverter MessageConverter}.
+ * @param destination the destination to send a message to
+ * @param payload the message payload
+ * @return a Receiptable for tracking receipts
+ */
+ Receiptable send(String destination, Object payload);
+
+ /**
+ * An overloaded version of {@link #send(String, Object)} that accepts
+ * full {@link StompHeaders} instead of a destination. The headers must
+ * contain a destination and may also have other headers such as
+ * "content-type" or custom headers for the broker to propagate to subscribers,
+ * or broker-specific, non-standard headers..
+ * @param headers the message headers
+ * @param payload the message payload
+ * @return a Receiptable for tracking receipts
+ */
+ Receiptable send(StompHeaders headers, Object payload);
+
+ /**
+ * Subscribe to the given destination by sending a SUBSCRIBE frame and handle
+ * received messages with the specified {@link StompFrameHandler}.
+ * @param destination the destination to subscribe to
+ * @param handler the handler for received messages
+ * @return a handle to use to unsubscribe and/or track receipts
+ */
+ Subscription subscribe(String destination, StompFrameHandler handler);
+
+ /**
+ * An overloaded version of {@link #subscribe(String, StompFrameHandler)}
+ * that accepts full {@link StompHeaders} rather instead of a destination.
+ * @param headers the headers for the subscribe message frame
+ * @param handler the handler for received messages
+ * @return a handle to use to unsubscribe and/or track receipts
+ */
+ Subscription subscribe(StompHeaders headers, StompFrameHandler handler);
+
+ /**
+ * Disconnect the session by sending a DISCONNECT frame.
+ */
+ void disconnect();
+
+
+ /**
+ * A handle to use to track receipts.
+ * @see #setAutoReceipt(boolean)
+ */
+ interface Receiptable {
+
+ /**
+ * Return the receipt id, or {@code null} if the STOMP frame for which
+ * the handle was returned did not have a "receipt" header.
+ */
+ String getReceiptId();
+
+ /**
+ * Task to invoke when a receipt is received.
+ * @throws java.lang.IllegalArgumentException if the receiptId is {@code null}
+ */
+ void addReceiptTask(Runnable runnable);
+
+ /**
+ * Task to invoke when a receipt is not received in the configured time.
+ * @throws java.lang.IllegalArgumentException if the receiptId is {@code null}
+ * @see org.springframework.messaging.simp.stomp.StompClientSupport#setReceiptTimeLimit(long)
+ */
+ void addReceiptLostTask(Runnable runnable);
+
+ }
+
+ /**
+ * A handle to use to unsubscribe or to track a receipt.
+ */
+ interface Subscription extends Receiptable {
+
+ /**
+ * Return the id for the subscription.
+ */
+ String getSubscriptionId();
+
+ /**
+ * Remove the subscription by sending an UNSUBSCRIBE frame.
+ */
+ void unsubscribe();
+
+ }
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandler.java
new file mode 100644
index 0000000000..e52ee0ce99
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandler.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+
+/**
+ * A contract for client STOMP session lifecycle events including a callback
+ * when the session is established and notifications of transport or message
+ * handling failures.
+ *
+ *
This contract also extends {@link StompFrameHandler} in order to handle
+ * STOMP ERROR frames received from the broker.
+ *
+ *
Implementations of this interface should consider extending
+ * {@link StompSessionHandlerAdapter}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ * @see StompSessionHandlerAdapter
+ */
+public interface StompSessionHandler extends StompFrameHandler {
+
+ /**
+ * Invoked when the session is ready to use, i.e. after the underlying
+ * transport (TCP, WebSocket) is connected and a STOMP CONNECTED frame is
+ * received from the broker.
+ * @param session the client STOMP session
+ * @param connectedHeaders the STOMP CONNECTED frame headers
+ */
+ void afterConnected(StompSession session, StompHeaders connectedHeaders);
+
+ /**
+ * Handle any exception arising while processing a STOMP frame such as a
+ * failure to convert the payload or an unhandled exception in the
+ * application {@code StompFrameHandler}.
+ * @param session the client STOMP session
+ * @param command the STOMP command of the frame
+ * @param headers the headers
+ * @param payload the raw payload
+ * @param exception the exception
+ */
+ void handleException(StompSession session, StompCommand command, StompHeaders headers,
+ byte[] payload, Throwable exception);
+
+ /**
+ * Handle a low level transport error which could be an I/O error or a
+ * failure to encode or decode a STOMP message.
+ *
Note that
+ * {@link org.springframework.messaging.simp.stomp.ConnectionLostException
+ * ConnectionLostException} will be passed into this method when the
+ * connection is lost rather than closed normally via
+ * {@link StompSession#disconnect()}.
+ * @param session the client STOMP session
+ * @param exception the exception that occurred
+ */
+ void handleTransportError(StompSession session, Throwable exception);
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandlerAdapter.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandlerAdapter.java
new file mode 100644
index 0000000000..d4137806ec
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandlerAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.messaging.simp.stomp;
+
+import java.lang.reflect.Type;
+
+/**
+ * Abstract adapter class for {@link StompSessionHandler} with mostly empty
+ * implementation methods except for {@link #getPayloadType} which returns String
+ * as the default Object type expected for STOMP ERROR frame payloads.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public abstract class StompSessionHandlerAdapter implements StompSessionHandler {
+
+ /**
+ * This implementation is empty.
+ */
+ @Override
+ public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
+ }
+
+ /**
+ * This implementation returns String as the expected payload type
+ * for STOMP ERROR frames.
+ */
+ @Override
+ public Type getPayloadType(StompHeaders headers) {
+ return String.class;
+ }
+
+ /**
+ * This implementation is empty.
+ */
+ @Override
+ public void handleFrame(StompHeaders headers, Object payload) {
+ }
+
+ /**
+ * This implementation is empty.
+ */
+ @Override
+ public void handleException(StompSession session, StompCommand command, StompHeaders headers,
+ byte[] payload, Throwable exception) {
+ }
+
+ /**
+ * This implementation is empty.
+ */
+ @Override
+ public void handleTransportError(StompSession session, Throwable exception) {
+ }
+
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java
index db8a534a17..881d38e1c2 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java
@@ -25,6 +25,7 @@ import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
/**
@@ -186,6 +187,17 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor {
setModified(true);
}
+ public void addNativeHeaders(MultiValueMap headers) {
+ if (headers == null) {
+ return;
+ }
+ for (String header : headers.keySet()) {
+ for (String value : headers.get(header)) {
+ addNativeHeader(header, value);
+ }
+ }
+ }
+
public List removeNativeHeader(String name) {
Assert.state(isMutable(), "Already immutable");
Map> nativeHeaders = getNativeHeaders();
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/Reactor11TcpClient.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/Reactor11TcpClient.java
index 093147fb57..a38eb324ff 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/Reactor11TcpClient.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/Reactor11TcpClient.java
@@ -196,7 +196,9 @@ public class Reactor11TcpClient