/*
* Copyright 2015-2016 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.integration.stomp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.SmartLifecycle;
import org.springframework.integration.stomp.event.StompConnectionFailedEvent;
import org.springframework.integration.stomp.event.StompSessionConnectedEvent;
import org.springframework.messaging.simp.stomp.StompClientSupport;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
/**
* Base {@link StompSessionManager} implementation to manage a single {@link StompSession}
* over its {@link ListenableFuture} from the target implementation of this class.
* <p>
* The connection to the {@link StompSession} is made during {@link #start()}.
* <p>
* The {@link #stop()} lifecycle method manages {@link StompSession#disconnect()}.
* <p>
* The {@link #connect(StompSessionHandler)} and {@link #disconnect(StompSessionHandler)} method
* implementations populate/remove the provided {@link StompSessionHandler} to/from an internal
* {@link AbstractStompSessionManager.CompositeStompSessionHandler}, which delegates all operations
* to the provided {@link StompSessionHandler}s.
* This {@link AbstractStompSessionManager.CompositeStompSessionHandler} is used for the
* {@link StompSession} connection.
*
* @author Artem Bilan
* @author Gary Russell
*
* @since 4.2
*/
public abstract class AbstractStompSessionManager implements StompSessionManager, ApplicationEventPublisherAware,
SmartLifecycle, DisposableBean, BeanNameAware {
private static final long DEFAULT_RECOVERY_INTERVAL = 10000;
protected final Log logger = LogFactory.getLog(getClass());
private final CompositeStompSessionHandler compositeStompSessionHandler = new CompositeStompSessionHandler();
private final Object lifecycleMonitor = new Object();
protected final StompClientSupport stompClient;
private final AtomicInteger epoch = new AtomicInteger();
private boolean autoStartup = false;
private boolean running = false;
private int phase = Integer.MAX_VALUE / 2;
private ApplicationEventPublisher applicationEventPublisher;
private volatile StompHeaders connectHeaders;
private volatile ListenableFuture<StompSession> stompSessionListenableFuture;
private volatile boolean autoReceipt;
private volatile boolean connecting;
private volatile boolean connected;
private volatile long recoveryInterval = DEFAULT_RECOVERY_INTERVAL;
private volatile ScheduledFuture<?> reconnectFuture;
private String name;
public AbstractStompSessionManager(StompClientSupport stompClient) {
Assert.notNull(stompClient, "'stompClient' is required.");
this.stompClient = stompClient;
}
public void setConnectHeaders(StompHeaders connectHeaders) {
this.connectHeaders = connectHeaders;
}
public void setAutoReceipt(boolean autoReceipt) {
this.autoReceipt = autoReceipt;
}
@Override
public boolean isAutoReceiptEnabled() {
return this.autoReceipt;
}
@Override
public boolean isConnected() {
return this.connected;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public void setBeanName(String name) {
this.name = name;
}
/**
* Specify a reconnect interval in milliseconds in case of lost connection.
* @param recoveryInterval the reconnect interval in milliseconds in case of lost connection.
* @since 4.2.2
*/
public void setRecoveryInterval(int recoveryInterval) {
this.recoveryInterval = recoveryInterval;
}
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
public void setPhase(int phase) {
this.phase = phase;
}
public long getRecoveryInterval() {
return this.recoveryInterval;
}
@Override
public boolean isAutoStartup() {
return this.autoStartup;
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int getPhase() {
return this.phase;
}
private synchronized void connect() {
if (this.connecting || this.connected) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Aborting connect; another thread is connecting.");
}
return;
}
final int epoch = this.epoch.get();
this.connecting = true;
if (this.logger.isDebugEnabled()) {
this.logger.debug("Connecting " + this);
}
try {
this.stompSessionListenableFuture = doConnect(this.compositeStompSessionHandler);
}
catch (Exception e) {
if (epoch == this.epoch.get()) {
scheduleReconnect(e);
}
else {
this.logger.error("STOMP doConnect() error for " + this, e);
}
return;
}
final CountDownLatch latch = new CountDownLatch(1);
this.stompSessionListenableFuture.addCallback(new ListenableFutureCallback<StompSession>() {
@Override
public void onFailure(Throwable e) {
if (AbstractStompSessionManager.this.logger.isDebugEnabled()) {
AbstractStompSessionManager.this.logger.debug("onFailure", e);
}
latch.countDown();
if (epoch == AbstractStompSessionManager.this.epoch.get()) {
scheduleReconnect(e);
}
}
@Override
public void onSuccess(StompSession stompSession) {
if (AbstractStompSessionManager.this.logger.isDebugEnabled()) {
AbstractStompSessionManager.this.logger.debug("onSuccess");
}
AbstractStompSessionManager.this.connected = true;
AbstractStompSessionManager.this.connecting = false;
stompSession.setAutoReceipt(isAutoReceiptEnabled());
if (AbstractStompSessionManager.this.applicationEventPublisher != null) {
AbstractStompSessionManager.this.applicationEventPublisher.publishEvent(
new StompSessionConnectedEvent(this));
}
AbstractStompSessionManager.this.reconnectFuture = null;
latch.countDown();
}
});
try {
if (!latch.await(10, TimeUnit.SECONDS)) {
this.logger.error("No response to connection attempt");
if (epoch == this.epoch.get()) {
scheduleReconnect(null);
}
}
}
catch (InterruptedException e1) {
this.logger.error("Interrupted while waiting for connection attempt");
Thread.currentThread().interrupt();
}
}
private void scheduleReconnect(Throwable e) {
this.epoch.incrementAndGet();
this.connecting = this.connected = false;
if (e != null) {
this.logger.error("STOMP connect error for " + this, e);
}
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(
new StompConnectionFailedEvent(this, e));
}
// cancel() after the publish in case we are on that thread; a send to a QueueChannel would fail.
if (this.reconnectFuture != null) {
this.reconnectFuture.cancel(true);
this.reconnectFuture = null;
}
if (this.stompClient.getTaskScheduler() != null) {
this.reconnectFuture = this.stompClient.getTaskScheduler()
.schedule((Runnable) () -> connect(), new Date(System.currentTimeMillis() + this.recoveryInterval));
}
else {
this.logger.info("For automatic reconnection the 'stompClient' should be configured with a TaskScheduler.");
}
}
@Override
public void destroy() {
if (this.stompSessionListenableFuture != null) {
if (this.reconnectFuture != null) {
this.reconnectFuture.cancel(false);
this.reconnectFuture = null;
}
this.stompSessionListenableFuture.addCallback(new ListenableFutureCallback<StompSession>() {
@Override
public void onFailure(Throwable ex) {
AbstractStompSessionManager.this.connected = false;
}
@Override
public void onSuccess(StompSession session) {
session.disconnect();
AbstractStompSessionManager.this.connected = false;
}
});
this.stompSessionListenableFuture = null;
}
}
@Override
public void start() {
synchronized (this.lifecycleMonitor) {
if (!isRunning()) {
if (this.logger.isInfoEnabled()) {
this.logger.info("Starting " + getClass().getSimpleName());
}
connect();
this.running = true;
}
}
}
@Override
public void stop(Runnable callback) {
synchronized (this.lifecycleMonitor) {
stop();
if (callback != null) {
callback.run();
}
}
}
@Override
public void stop() {
synchronized (this.lifecycleMonitor) {
if (isRunning()) {
this.running = false;
if (this.logger.isInfoEnabled()) {
this.logger.info("Stopping " + getClass().getSimpleName());
}
destroy();
}
}
}
@Override
public void connect(StompSessionHandler handler) {
this.compositeStompSessionHandler.addHandler(handler);
if (!isConnected() && !this.connecting) {
if (this.reconnectFuture != null) {
this.reconnectFuture.cancel(true);
this.reconnectFuture = null;
}
connect();
}
}
@Override
public void disconnect(StompSessionHandler handler) {
this.compositeStompSessionHandler.removeHandler(handler);
}
protected StompHeaders getConnectHeaders() {
return this.connectHeaders;
}
@Override
public String toString() {
return ObjectUtils.identityToString(this) +
" {connecting=" + this.connecting +
", connected=" + this.connected +
", name='" + this.name + '\'' +
'}';
}
protected abstract ListenableFuture<StompSession> doConnect(StompSessionHandler handler);
private class CompositeStompSessionHandler extends StompSessionHandlerAdapter {
private final List<StompSessionHandler> delegates =
Collections.synchronizedList(new ArrayList<StompSessionHandler>());
private volatile StompSession session;
CompositeStompSessionHandler() {
super();
}
void addHandler(StompSessionHandler delegate) {
if (this.session != null) {
delegate.afterConnected(this.session, getConnectHeaders());
}
this.delegates.add(delegate);
}
void removeHandler(StompSessionHandler delegate) {
this.delegates.remove(delegate);
}
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
this.session = session;
synchronized (this.delegates) {
for (StompSessionHandler delegate : this.delegates) {
delegate.afterConnected(session, connectedHeaders);
}
}
}
@Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload,
Throwable exception) {
synchronized (this.delegates) {
for (StompSessionHandler delegate : this.delegates) {
delegate.handleException(session, command, headers, payload, exception);
}
}
}
@Override
public void handleTransportError(StompSession session, Throwable exception) {
AbstractStompSessionManager.this.logger.error("STOMP transport error for session: [" + session + "]",
exception);
this.session = null;
scheduleReconnect(exception);
synchronized (this.delegates) {
for (StompSessionHandler delegate : this.delegates) {
delegate.handleTransportError(session, exception);
}
}
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
synchronized (this.delegates) {
for (StompSessionHandler delegate : this.delegates) {
delegate.handleFrame(headers, payload);
}
}
}
}
}