org.springframework.integration.smpp.session.SmppSessionFactoryBean.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.smpp.session.SmppSessionFactoryBean.java

Source

/* Copyright 2002-2013 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.smpp.session;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jsmpp.DefaultPDUReader;
import org.jsmpp.DefaultPDUSender;
import org.jsmpp.SynchronizedPDUSender;
import org.jsmpp.bean.BindType;
import org.jsmpp.bean.NumberingPlanIndicator;
import org.jsmpp.bean.TypeOfNumber;
import org.jsmpp.extra.SessionState;
import org.jsmpp.session.MessageReceiverListener;
import org.jsmpp.session.SMPPSession;
import org.jsmpp.session.SessionStateListener;
import org.jsmpp.session.connection.Connection;
import org.jsmpp.session.connection.ConnectionFactory;
import org.jsmpp.session.connection.socket.SocketConnection;
import org.jsmpp.util.DefaultComposer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.Lifecycle;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.Ordered;
import org.springframework.util.Assert;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Factory bean to create a {@link SMPPSession}. Usually, you need little more than the {@link #host},
 * the {@link #port}, perhaps a {@link #password}, and a {@link #systemId}.
 * <p/>
 * The {@link SMPPSession } represents a connection to a SMSC, through which SMS messages are sent and received.
 * <p/>
 * Here is a breakdown of the supported parameters on this factory bean:
 * <p/>
 * <ul>
 *     <li>host - the SMSC host to which the session is bound   (think of this as the host of your email server)</li>
 *     <li>port - the SMSC port to which the session is bound (think of this as a port on your email server)</li>
 *     <li>bindType - values of type {@link org.jsmpp.bean.BindType}. The bind type specifies whether this
 *     {@link SMPPSession} can send ({@link org.jsmpp.bean.BindType#BIND_TX}),
 *     receive ({@link org.jsmpp.bean.BindType#BIND_RX}), or both send and receive
 *     ({@link org.jsmpp.bean.BindType#BIND_TRX}).</li>
 *     <li>systemId - the system ID for the server being bound to</li>
 *     <li>password - the password for the server being bound to</li>
 *     <li>systemType - the SMSC system type</li>
 *     <li>addrTon - a value from the {@link org.jsmpp.bean.TypeOfNumber} enumeration. Default is
 *     {@link org.jsmpp.bean.TypeOfNumber#UNKNOWN}</li>
 *     <li>addrNpi - a value from  the {@link org.jsmpp.bean.NumberingPlanIndicator} enumeration.
 *     Default is {@link org.jsmpp.bean.NumberingPlanIndicator#UNKNOWN}</li>
 *     <li>addressRange - can be null. Specifies the address range.</li>
 *     <li>timeout - a good default value is 60000  (1 minute)</li>
 *     <li>transactionTimeout - timeout for doing work with session. e.g. sending message (default 2 seconds)</li>
 *     <li>reconnect - boolean whether we allow the session to reconnect. (default true)</li>
 *     <li>reconnectInterval - interval between reconnection in milliseconds. (default 5 seconds)</li>
 * </ul>
 *
 *
 * @author Josh Long
 * @see org.jsmpp.session.SMPPSession#SMPPSession()
 * @see org.jsmpp.session.SMPPSession#connectAndBind(String, int, org.jsmpp.session.BindParameter)
 * @see org.jsmpp.session.SMPPSession#connectAndBind(String, int, org.jsmpp.bean.BindType, String, String, String, org.jsmpp.bean.TypeOfNumber, org.jsmpp.bean.NumberingPlanIndicator, String, long)
 * @since 1.0
 */
public class SmppSessionFactoryBean
        implements FactoryBean<ExtendedSmppSession>, SmartLifecycle, InitializingBean, DisposableBean {

    private Set<MessageReceiverListener> messageReceiverListeners = new HashSet<MessageReceiverListener>();
    private boolean autoStartup;
    private volatile boolean running;
    private Log log = LogFactory.getLog(getClass());
    private SessionStateListener sessionStateListener;
    private boolean ssl = false;
    private String host = "127.0.0.1";
    private String addressRange;
    private long timeout = 60 * 1000;// 1 minute
    private long transactionTimeout = 2 * 1000; // 2 seconds
    private int port = 2775; // good default though this has been known to change
    private BindType bindType = BindType.BIND_TRX; // bind as a 'transceiver' - only 3.4 of the spec <em>requires</em> support for this
    private String systemId = getClass().getSimpleName().toLowerCase(); // what would typically be called 'user' in a user/pw scheme
    private String password;
    private String systemType = "cp";
    private TypeOfNumber addrTon = TypeOfNumber.UNKNOWN;
    private NumberingPlanIndicator addrNpi = NumberingPlanIndicator.UNKNOWN;
    private long reconnectInterval = 5 * 1000; // 5 seconds
    private boolean reconnect = true; // flag whether we want to reconnect
    private volatile boolean destroyed = false; // flag that this session factory has been disposed

    private ExtendedSmppSessionAdaptingDelegate product;
    private final ProxyFactoryBean sessionFactoryBean = new ProxyFactoryBean();

    private ExecutorService reconnectingExecutor;
    private boolean reconnectingExecutorSet;

    public void setSsl(boolean ssl) {
        this.ssl = ssl;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public void setBindType(BindType bindType) {
        this.bindType = bindType;
    }

    public void setSystemId(String systemId) {
        this.systemId = systemId;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setSystemType(String systemType) {
        this.systemType = systemType;
    }

    public void setAddrTon(TypeOfNumber addrTon) {
        this.addrTon = addrTon;
    }

    public void setAddrNpi(NumberingPlanIndicator addrNpi) {
        this.addrNpi = addrNpi;
    }

    /**
     * this specifies the range of numbers we want to <em>listen</em> to - as a consumer. If you
     * specify '1234' as a destination address, and want to listen / receive all messages sent
     * to that number, then specify '1234' as the {@link #addressRange}.
     *
     * @param addressRange the range of phone numbers to receive from.
     */
    public void setAddressRange(String addressRange) {
        this.addressRange = addressRange;
    }

    /**
     * Setting timeout for the session. This value is used to establish connection to SMSC, e.g. trying to establish
     * connection. (default is 1 minute). This should not be confused with {@link #setTransactionTimeout(long)} which
     * is the timeout to perform request on the actual session after it has been established.
     *
     * @param timeout timeout in milliseconds
     */
    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    /**
     * Setting transaction timeout preforming request on the session. ({@link SMPPSession#setTransactionTimer(long)}.
     * This transaction timeout is similar to the concept of send timeout / request timeout. (default 2 seconds).
     * If you receive a lot of {@link org.jsmpp.extra.ResponseTimeoutException} for waiting response from your SMSC,
     * this indicates you need to increase this value.
     *
     * @param transactionTimeout transaction timeout in milliseconds
     */
    public void setTransactionTimeout(long transactionTimeout) {
        this.transactionTimeout = transactionTimeout;
    }

    public void setSessionStateListener(SessionStateListener sessionStateListener) {
        this.sessionStateListener = sessionStateListener;
    }

    public void setMessageReceiverListeners(Set<MessageReceiverListener> messageReceiverListeners) {
        this.messageReceiverListeners = messageReceiverListeners;
    }

    /**
     * Creating new SMPPSession. This will create default SMPPSession for non-SSL connection or create SMPPSession
     * using different factory for SSL connection.
     * @return SMPP session
     */
    private SMPPSession createNewSession() {
        final SMPPSession newSession;
        if (!ssl) {
            newSession = new SMPPSession();
        } else {
            newSession = new SMPPSession(new SynchronizedPDUSender(new DefaultPDUSender(new DefaultComposer())),
                    new DefaultPDUReader(), sslConnectionFactory);
        }
        newSession.setTransactionTimer(transactionTimeout);
        return newSession;
    }

    /**
     * Logic to build smpp session
     * @return the configured SMPPSession
     * @throws Exception should anything go wrong
     */
    private ExtendedSmppSessionAdaptingDelegate buildSmppSession() throws Exception {
        final SMPPSession smppSession = createNewSession();
        final ExtendedSmppSessionAdaptingDelegate extendedSmppSessionAdaptingDelegate;
        if (reconnect) {
            sessionFactoryBean.setAutodetectInterfaces(false);
            sessionFactoryBean.setTarget(smppSession);
            final SMPPSession proxiedSession = (SMPPSession) sessionFactoryBean.getObject();

            extendedSmppSessionAdaptingDelegate = new ExtendedSmppSessionAdaptingDelegate(proxiedSession,
                    new AutoReconnectLifecycle(proxiedSession));
        } else {
            extendedSmppSessionAdaptingDelegate = new ExtendedSmppSessionAdaptingDelegate(smppSession,
                    new ConnectingLifecycle(smppSession));
        }

        for (MessageReceiverListener mrl : this.messageReceiverListeners)
            extendedSmppSessionAdaptingDelegate.addMessageReceiverListener(mrl);

        // if session state listener not null, add it
        if (sessionStateListener != null) {
            extendedSmppSessionAdaptingDelegate.addSessionStateListener(sessionStateListener);
        }

        extendedSmppSessionAdaptingDelegate.setBindType(this.bindType);
        return extendedSmppSessionAdaptingDelegate;
    }

    public void setAutoStartup(boolean autoStartup) {
        this.autoStartup = autoStartup;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isAutoStartup() {
        return this.autoStartup;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stop(Runnable callback) {
        this.stop();
        callback.run();
    }

    /**
     * {@inheritDoc}
     */
    public void start() {
        log.debug("starting up in " + getClass().getName() + "#start().");
        if (reconnectingExecutor == null) {
            this.reconnectingExecutor = Executors.newFixedThreadPool(1);
        }

        (product).start();
        this.running = true;
    }

    /**
     * {@inheritDoc}
     */
    public void stop() {
        log.debug("shutting down in " + getClass().getName() + "#stop().");
        (product).stop();

        // if we are running default executor, shut it down
        if (!reconnectingExecutorSet && reconnectingExecutor != null) {
            reconnectingExecutor.shutdown();
            this.reconnectingExecutor = null;
        }
        this.running = false;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isRunning() {
        return this.running;
    }

    /**
     * {@inheritDoc}
     */
    public int getPhase() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * delegates to {@link #buildSmppSession()}
     */
    public ExtendedSmppSession getObject() throws Exception {
        return product;
    }

    /**
     * {@inheritDoc}
     */
    public Class<?> getObjectType() {
        return ExtendedSmppSessionAdaptingDelegate.class;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isSingleton() {
        return true;
    }

    /**
     * Set whether we want to reconnect the session. Default is true.
     *
     * @param reconnect true/false
     */
    public void setReconnect(boolean reconnect) {
        this.reconnect = reconnect;
    }

    /**
     * Set session reconnection interval. Default is 5 seconds.
     *
     * @param reconnectInterval reconnection interval in milliseconds
     */
    public void setReconnectInterval(long reconnectInterval) {
        this.reconnectInterval = reconnectInterval;
    }

    /**
     * Set executor service for performing SMPP reconnection.
     * @param reconnectingExecutor executor service
     */
    public void setReconnectingExecutor(ExecutorService reconnectingExecutor) {
        this.reconnectingExecutor = reconnectingExecutor;
        this.reconnectingExecutorSet = true;
    }

    /**
     * {@inheritDoc}
     */
    public void afterPropertiesSet() throws Exception {

        // NB, the reference handed back by {@link org.springframework.beans.factory.FactoryBean#getObject()}  isn't itself
        // managed, only the factory, so we cache it and then delegate through the factory's lifecycle methods.

        Assert.notNull(this.systemId, "the systemId can't be null");
        Assert.notNull(this.host, "the host can't be null");
        Assert.notNull(this.port, "the port can't be null");

        this.product = buildSmppSession();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void destroy() throws Exception {
        this.destroyed = true;
    }

    /**
     * singleton {@link ConnectionFactory} that handles SSL
     */
    final private static ConnectionFactory sslConnectionFactory = new ConnectionFactory() {

        public Connection createConnection(String host, int port) throws IOException {
            SocketFactory socketFactory = SSLSocketFactory.getDefault();
            Socket socket = socketFactory.createSocket(host, port);
            return new SocketConnection(socket);
        }
    };

    /**
     * lifecycle implementation that simply {@link SMPPSession#connectAndBind(String, int, org.jsmpp.session.BindParameter)} and
     * {@link org.jsmpp.session.SMPPSession#unbindAndClose()}.
     */
    private class ConnectingLifecycle implements Lifecycle {

        private volatile boolean running;

        private SMPPSession session;

        private ConnectingLifecycle(SMPPSession smppSession) {
            this.session = smppSession;
        }

        public boolean isRunning() {
            return this.running;
        }

        public void stop() {
            if (session != null) {
                if (session.getSessionState().isBound()) {
                    try {
                        session.unbindAndClose();
                    } catch (Exception t) {
                        log.warn("Couldn't close and unbind the session", t);
                    }
                }
            } else {
                log.warn("The smppSession given to close is null");
            }
        }

        public void start() {
            try {
                session.connectAndBind(host, port, bindType, systemId, password, systemType, addrTon, addrNpi,
                        addressRange, timeout);
                this.running = true;
            } catch (IOException e) {
                if (log.isDebugEnabled()) {
                    log.error("Error happened when trying to connect to " + host + ":" + port, e);
                } else {
                    log.error("Error happened when trying to connect to " + host + ":" + port + ". Cause: "
                            + e.getMessage());
                }
            }
        }
    }

    /**
     * Lifecycle implementation that will try to re-establish connection with specific interval. At the start of the
     * connection.
     *
     * @author Johanes Soetanto
     */
    private class AutoReconnectLifecycle implements Lifecycle {

        private final Logger log = LoggerFactory.getLogger(AutoReconnectLifecycle.class);
        private final SMPPSession session;
        private volatile boolean running;

        /**
         * Creating auto reconnect lifecycle using SMPP session and reconnect interval in milliseconds
         * @param smppSession reference to SMPP session
         */
        private AutoReconnectLifecycle(SMPPSession smppSession) {
            this.session = smppSession;
        }

        @Override
        public boolean isRunning() {
            return this.running;
        }

        @Override
        public void stop() {
            if (session != null) {
                if (session.getSessionState().isBound()) {
                    try {
                        session.unbindAndClose();
                    } catch (Exception t) {
                        log.warn("Couldn't close and unbind the session", t);
                    }
                }
            } else {
                log.warn("The smppSession given to close is null");
            }
        }

        @Override
        public void start() {
            connect();

            if (!running) {
                log.debug("Try to connect at later time. The delay is {}ms", reconnectInterval);
                scheduleReconnect();
            } else {
                registerSessionCloseListener();
            }
        }

        /**
         * Register session state listener to reconnect when session is closed by server.
         */
        private void registerSessionCloseListener() {
            log.debug("Registering session close listener");
            session.addSessionStateListener(new SessionStateListener() {
                @Override
                public void onStateChange(SessionState newState, SessionState oldState, Object source) {
                    // when session is closed but client session has not been destroyed can indicates client
                    // lose connection to server
                    if (newState.equals(SessionState.CLOSED)) {
                        running = false;
                        if (!destroyed) {
                            log.info("Session to {}:{} has been closed. Try to reconnect later", host, port);

                            final SMPPSession newSession = createNewSession();
                            newSession.setMessageReceiverListener(product.getDelegateMessageListener());
                            if (sessionStateListener != null) {
                                session.addSessionStateListener(sessionStateListener);
                            }
                            sessionFactoryBean.setTarget(newSession);

                            scheduleReconnect();
                        }
                    }
                }
            });
        }

        /**
         * Perform connection logic.
         */
        private void connect() {
            try {
                session.connectAndBind(host, port, bindType, systemId, password, systemType, addrTon, addrNpi,
                        addressRange, timeout);
                this.running = true;

            } catch (IOException e) {
                if (log.isDebugEnabled()) {
                    log.error("Error happened when trying to connect to " + host + ":" + port, e);
                } else {
                    log.error("Error happened when trying to connect to {}:{}. Cause: {}",
                            new Object[] { host, port, e.getMessage() });
                }
            }
        }

        /**
         * Schedule a session reconnection.
         */
        private void scheduleReconnect() {

            reconnectingExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(reconnectInterval);

                        int attempt = 0;
                        // if this session is still not run and the session has not been destroyed, re-connect
                        while (!running && !destroyed) {
                            log.info("Reconnecting attempt #{} ...", ++attempt);
                            connect();

                            if (!running) {
                                // if still not running, then perform another sleep
                                Thread.sleep(reconnectInterval);
                            }
                        }

                        if (running) {
                            log.info("Successfully reconnect at attempt #{}", attempt);
                            // if finish re-connection loop and session is run we register session close listener
                            registerSessionCloseListener();
                        }

                    } catch (InterruptedException e) {
                        log.info("Interrupted when trying to connect to {}:{}", host, port);
                    }
                }
            });
        }
    }

}