Add STOMP client

WebSocketStompClient can be used with any implementation of
org.springframework.web.socket.client.WebSocketClient, which includes
org.springframework.web.socket.sockjs.client.SockJsClient.

Reactor11TcpStompClient can be used with reactor-net and provides STOMP
over TCP. It's also possible to adapt other WebSocket and TCP client
libraries (see StompClientSupport for more details).

For example usage see WebSocketStompClientIntegrationTests.

Issue: SPR-11588
This commit is contained in:
Rossen Stoyanchev
2015-02-25 16:19:18 -05:00
parent 8a5e47a043
commit d30b3eaf55
21 changed files with 3920 additions and 6 deletions

View File

@@ -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.
*
* <p>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<byte[]> {
/**
* Return a future that will complete when the session is ready for use.
*/
ListenableFuture<StompSession> getSessionFuture();
}

View File

@@ -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);
}
}

View File

@@ -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<byte[]> 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<StompSession> sessionFuture = new SettableListenableFuture<StompSession>();
private MessageConverter converter = new StringMessageConverter();
private TaskScheduler taskScheduler;
private long receiptTimeLimit = 15 * 1000;
private volatile boolean autoReceiptEnabled;
private volatile TcpConnection<byte[]> connection;
private final AtomicInteger subscriptionIndex = new AtomicInteger();
private final Map<String, DefaultSubscription> subscriptions = new ConcurrentHashMap<String, DefaultSubscription>(4);
private final AtomicInteger receiptIndex = new AtomicInteger();
private final Map<String, ReceiptHandler> receiptHandlers = new ConcurrentHashMap<String, ReceiptHandler>(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<StompSession> 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.
* <p>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.
* <p>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<byte[]> 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<byte[]> createMessage(StompHeaderAccessor accessor, Object payload) {
accessor.updateSimpMessageHeadersFromStompHeaders();
Message<byte[]> 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<byte[]>) 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<byte[]> message) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (logger.isTraceEnabled()) {
logger.trace("Sending " + accessor.getDetailedLogMessage(message.getPayload()));
}
TcpConnection<byte[]> 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<byte[]> message = createMessage(accessor, EMPTY_PAYLOAD);
execute(message);
return subscription;
}
private void unsubscribe(String id) {
StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.UNSUBSCRIBE);
accessor.setSubscriptionId(id);
Message<byte[]> message = createMessage(accessor, EMPTY_PAYLOAD);
execute(message);
}
@Override
public void disconnect() {
this.closing = true;
try {
StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.DISCONNECT);
Message<byte[]> message = createMessage(accessor, EMPTY_PAYLOAD);
execute(message);
}
finally {
resetConnection();
}
}
// TcpConnectionHandler
@Override
public void afterConnected(TcpConnection<byte[]> 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<byte[]> 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<byte[]> message) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
accessor.setSessionId(this.sessionId);
StompCommand command = accessor.getCommand();
Map<String, List<String>> 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<byte[]> 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<Runnable> receiptCallbacks = new ArrayList<Runnable>(2);
private final List<Runnable> receiptLostCallbacks = new ArrayList<Runnable>(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<Runnable> 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<byte[]> conn = connection;
if (conn != null) {
conn.send(HEARTBEAT).addCallback(
new ListenableFutureCallback<Void>() {
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();
}
}
}

View File

@@ -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<byte[]> 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<byte[]>(createNettyTcpClient(host, port));
}
private TcpClient<Message<byte[]>, Message<byte[]>> createNettyTcpClient(String host, int port) {
return new TcpClientSpec<Message<byte[]>, Message<byte[]>>(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<byte[]> 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<StompSession> 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<StompSession> 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.<DispatcherConfiguration>asList(config), dispatcherName, new Properties());
}
}
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
* <p>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.
*
* <p><strong>Note:</strong> 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.
*
* <p>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.
* <p>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 <a href="http://stomp.github.io/stomp-specification-1.2.html#Heart-beating">
* http://stomp.github.io/stomp-specification-1.2.html#Heart-beating</a>
*/
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.
* <p>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;
}
}

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -162,7 +162,7 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor {
}
}
private void updateStompHeadersFromSimpMessageHeaders() {
void updateStompHeadersFromSimpMessageHeaders() {
if (getDestination() != null) {
setNativeHeader(STOMP_DESTINATION_HEADER, getDestination());
}

View File

@@ -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.
*
* <p>In addition to the normal methods defined by {@link Map}, this class offers
* the following convenience methods:
* <ul>
* <li>{@link #getFirst(String)} return the first value for a header name</li>
* <li>{@link #add(String, String)} add to the list of values for a header name</li>
* <li>{@link #set(String, String)} set a header name to a single string value</li>
* </ul>
*
* @author Rossen Stoyanchev
* @since 4.2
* @see <a href="http://stomp.github.io/stomp-specification-1.2.html#Frames_and_Headers">
* http://stomp.github.io/stomp-specification-1.2.html#Frames_and_Headers</a>
*/
public class StompHeaders implements MultiValueMap<String, String>, 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<String, List<String>> headers;
/**
* Create a new instance to be populated with new header values.
*/
public StompHeaders() {
this(new LinkedMultiValueMap<String, String>(4), false);
}
private StompHeaders(Map<String, List<String>> headers, boolean readOnly) {
Assert.notNull(headers, "'headers' must not be null");
if (readOnly) {
Map<String, List<String>> map = new LinkedMultiValueMap<String, String>(headers.size());
for (Entry<String, List<String>> entry : headers.entrySet()) {
List<String> 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<String> 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<String> headerValues = headers.get(headerName);
if (headerValues == null) {
headerValues = new LinkedList<String>();
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<String> headerValues = new LinkedList<String>();
headerValues.add(headerValue);
headers.put(headerName, headerValues);
}
@Override
public void setAll(Map<String, String> values) {
for (Entry<String, String> entry : values.entrySet()) {
set(entry.getKey(), entry.getValue());
}
}
@Override
public Map<String, String> toSingleValueMap() {
LinkedHashMap<String, String> singleValueMap = new LinkedHashMap<String,String>(this.headers.size());
for (Entry<String, List<String>> 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<String> get(Object key) {
return this.headers.get(key);
}
@Override
public List<String> put(String key, List<String> value) {
return this.headers.put(key, value);
}
@Override
public List<String> remove(Object key) {
return this.headers.remove(key);
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> map) {
this.headers.putAll(map);
}
@Override
public void clear() {
this.headers.clear();
}
@Override
public Set<String> keySet() {
return this.headers.keySet();
}
@Override
public Collection<List<String>> values() {
return this.headers.values();
}
@Override
public Set<Entry<String, List<String>>> 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<String, List<String>> headers) {
return new StompHeaders(headers, true);
}
}

View File

@@ -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.
*
* <p>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();
}
}

View File

@@ -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.
*
* <p>This contract also extends {@link StompFrameHandler} in order to handle
* STOMP ERROR frames received from the broker.
*
* <p>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.
* <p>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);
}

View File

@@ -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) {
}
}

View File

@@ -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<String, String> headers) {
if (headers == null) {
return;
}
for (String header : headers.keySet()) {
for (String value : headers.get(header)) {
addNativeHeader(header, value);
}
}
}
public List<String> removeNativeHeader(String name) {
Assert.state(isMutable(), "Already immutable");
Map<String, List<String>> nativeHeaders = getNativeHeaders();

View File

@@ -196,7 +196,9 @@ public class Reactor11TcpClient<P> implements TcpOperations<P> {
};
}
finally {
this.environment.shutdown();
if (this.environment != null) {
this.environment.shutdown();
}
}
}