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:
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStompHeadersFromSimpMessageHeaders() {
|
||||
void updateStompHeadersFromSimpMessageHeaders() {
|
||||
if (getDestination() != null) {
|
||||
setNativeHeader(STOMP_DESTINATION_HEADER, getDestination());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -196,7 +196,9 @@ public class Reactor11TcpClient<P> implements TcpOperations<P> {
|
||||
};
|
||||
}
|
||||
finally {
|
||||
this.environment.shutdown();
|
||||
if (this.environment != null) {
|
||||
this.environment.shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user