org.springframework.integration.stomp.AbstractStompSessionManager.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.stomp.AbstractStompSessionManager.java

Source

/*
 * 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);
                }
            }
        }

    }

}