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