Merge pull request #37 from otnateos/INTEXT-58

* otnateos-INTEXT-58:
  INTEXT-58 Support SMPP Sending Using Other Charset and udh * Remove SmppSessionFactoryBean.setMessageReceiverListeners(MessageReceiverListener... listeners) to remove Spring's warning message for ambiguous method access * Allow using DeliveryReceipt id as is if hex parsing fail (tested with HSLSMS) * Add support for session reconnection which will attempt to reconnect every 5 sec by default until the session is destroyed * If additional session state listener is configured by user, it will register it * Update build dependency with Spring Integration 2.2.2 * Update javadoc * Add support for smpp transaction time using transactionTimeout * Support 'request-handler-advice-chain' for outbound channel adapter and gateway * Support sending using data_coding * Support sending using message_payload * Support for sending long sms using UDH * Update mock smpp server to simulate real SMPP error * Change message id reading logging to DEBUG instead of WARN * Wrap SMPPSession with proxy for auto reconnection * Use ExecutorService for reconnection * Fix some code and author
This commit is contained in:
Gunnar Hillert
2013-05-28 23:49:53 -04:00
22 changed files with 1316 additions and 103 deletions

View File

@@ -34,7 +34,7 @@ ext {
log4jVersion = '1.2.12'
mockitoVersion = '1.9.0'
springVersion = '3.1.3.RELEASE'
springIntegrationVersion = '2.2.0.RELEASE'
springIntegrationVersion = '2.2.2.RELEASE'
idPrefix = 'smpp'
@@ -72,7 +72,6 @@ dependencies {
compile "commons-lang:commons-lang:$commonsLangVersion"
compile "commons-beanutils:commons-beanutils:$commonsBeanUtilsVersion"
compile "org.springframework.integration:spring-integration-core:$springIntegrationVersion"
compile "org.springframework.integration:spring-integration-core:$springIntegrationVersion"
testCompile "org.springframework.integration:spring-integration-test:$springIntegrationVersion"
testCompile "junit:junit-dep:$junitVersion"
testCompile "log4j:log4j:$log4jVersion"

View File

@@ -35,10 +35,17 @@ abstract public class AbstractReceivingMessageListener implements MessageReceive
if (MessageType.SMSC_DEL_RECEIPT.containedIn(deliverSm.getEsmClass())) { // delivery receipt
try {
DeliveryReceipt delReceipt = deliverSm.getShortMessageAsDeliveryReceipt();
long id = Long.parseLong(delReceipt.getId());
String messageId = Long.toString(id, 16).toUpperCase();
onDeliveryReceipt(deliverSm, messageId, delReceipt);
String messageId;
try {
long id = Long.parseLong(delReceipt.getId());
messageId = Long.toString(id, 16).toUpperCase();
} catch (NumberFormatException nfe) {
logger.debug("Fail parsing message id into hex format from " + delReceipt.getId()
+ ". Now using id as it is");
messageId = delReceipt.getId();
}
logger.debug("Receiving delivery receipt for message '" + messageId + "' : " + delReceipt);
onDeliveryReceipt(deliverSm, messageId, delReceipt);
} catch (Exception e) {
logger.error("Failed getting delivery receipt", e);
throw new RuntimeException(e);

View File

@@ -0,0 +1,116 @@
/*
* 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.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
/**
* This is specification for data coding based on SMPP API and Java charset.
*
* @author Johanes Soetanto
* @since 1.0
*/
public class DataCodingSpecification {
private static final Logger log = LoggerFactory.getLogger(DataCodingSpecification.class);
public static final String US_ASCII = "US-ASCII";
public static final String ISO_8859_1 = "ISO-8859-1";
public static final String ISO_8859_5 = "ISO-8859-5";
public static final String ISO_8859_8 = "ISO-8859-8";
public static final String UTF_16 = "UTF-16";
public static final String UTF_8 = "UTF-8";
public static final String EUC_KR = "EUC-KR";
public static final String EUC_JP = "EUC-JP";
/**
* Get maximum characters for data coding. Returns
* <ul>
* <li>160 for data coding 0/1</li>
* <li>70 for data coding 5/8/10/13/14</li>
* <li>140 for others</li>
* </ul>
*
* @param dataCoding data coding
* @return maximum characters can be used for the text with specified data coding
*/
// reference https://www.cisco.com/en/US/docs/voice_ip_comm/connection/7x/administration/guide/7xcucsag200.pdf
public static int getMaxCharacters(byte dataCoding) {
switch (dataCoding) {
case 0:case 1: return 160; // these are 7bit, return full length
// JP and KR are suppose to use multi-byte character. This is probably needed to be tested once we get more
// people using those charset so they can maximize their allowed number of characters in single message.
// For now assume it is the same one as double byte characters
case 5:case 10:case 13: // just to be safe for japanese
case 8:
case 14: // just to be safe for korean
return 70;
default: return 140;
}
}
/**
* Get charset name based on data coding. Returns:
* <ul>
* <li>US-ASCII for data coding 1</li>
* <li>ISO-8859-1 for data coding 3</li>
* <li>ISO-8859-5 for data coding 6</li>
* <li>ISO-8859-8 for data coding 7</li>
* <li>UTF-16 for data coding 8</li>
* <li>EUC-KR for data coding 14</li>
* <li>EUC-JP for data coding 5/10/13</li>
* <li>UTF-8 for others</li>
* </ul>
*
* @param dataCoding data coding
* @return charset name related to the data coding
*/
public static String getCharsetName(byte dataCoding) {
switch (dataCoding) {
case 1: return US_ASCII;
case 3: return ISO_8859_1;
case 6: return ISO_8859_5;
case 7: return ISO_8859_8;
case 8: return UTF_16;
case 14: return EUC_KR;
case 5: case 10: case 13: return EUC_JP;
case 2: case 4: // since both 2 and 4 is unspecified binary, use UTF-8 encoding
case 0: // since dataCoding 0 is gsm 7bit, it is quite safe to use UTF-8
default: return UTF_8;
}
}
/**
* Get message in bytes. This will use {@link #getCharsetName(byte)} to get the message in bytes.
* @param message short message
* @param dataCoding data coding
* @return message in bytes based on the data coding
*/
public static byte[] getMessageInBytes(String message, byte dataCoding) {
final String charsetName = getCharsetName(dataCoding);
if (!charsetName.equals(UTF_8)) {
try {
return message.getBytes(charsetName);
}
catch (UnsupportedEncodingException e) {
log.warn("Fail to encode message using charset '{}'", charsetName);
}
}
return message.getBytes();
}
}

View File

@@ -61,6 +61,7 @@ public class SmesMessageSpecification {
private byte smDefaultMsgId;
private byte[] shortMessage;
private ClientSession smppSession;
private OptionalParameter messagePayloadParameter;
/**
* this method takes an inbound SMS message and converts it to a Spring Integration message
@@ -133,15 +134,26 @@ public class SmesMessageSpecification {
smsTxt = (String) payload;
}
}
SmesMessageSpecification spec = SmesMessageSpecification.newSmesMessageSpecification(smppSession, srcAddy, dstAddy, smsTxt);
final DataCoding dataCodingFromHeader = SmesMessageSpecification.dataCodingFromHeader(msg);
final SmesMessageSpecification spec = new SmesMessageSpecification()
.reset()
.setSmppSession(smppSession)
.setSourceAddress(srcAddy)
.setDestinationAddress(dstAddy)
.setDataCoding(dataCodingFromHeader);
spec.setMaxLengthSmsMessages(maximumCharactersFromHeader(msg));
spec.setEsmClass(SmesMessageSpecification.esmClassFromHeader(msg));
if (msg.getHeaders().containsKey(SmppConstants.USE_MSG_PAYLOAD_PARAM)) {
spec.setShortMessageUsingPayload(smsTxt);
} else {
spec.setShortTextMessage(smsTxt);
}
spec.setDestinationAddressNumberingPlanIndicator(SmesMessageSpecification.<NumberingPlanIndicator>valueIfHeaderExists(DST_NPI, msg));
spec.setSourceAddressNumberingPlanIndicator(SmesMessageSpecification.<NumberingPlanIndicator>valueIfHeaderExists(SRC_NPI, msg));
spec.setDestinationAddressTypeOfNumber(SmesMessageSpecification.<TypeOfNumber>valueIfHeaderExists(DST_TON, msg));
spec.setSourceAddressTypeOfNumber(SmesMessageSpecification.<TypeOfNumber>valueIfHeaderExists(SRC_TON, msg));
spec.setServiceType(SmesMessageSpecification.<String>valueIfHeaderExists(SERVICE_TYPE, msg));
spec.setEsmClass(SmesMessageSpecification.esmClassFromHeader(msg));
spec.setScheduleDeliveryTime(SmesMessageSpecification.<Date>valueIfHeaderExists(SCHEDULED_DELIVERY_TIME, msg));
spec.setDataCoding(SmesMessageSpecification. dataCodingFromHeader( msg));
spec.setValidityPeriod(SmesMessageSpecification.<String>valueIfHeaderExists(VALIDITY_PERIOD, msg));
// byte landmine. autoboxing causes havoc with <em>null</em> bytes.
@@ -178,6 +190,33 @@ public class SmesMessageSpecification {
return null ;
}
/**
* Getting maximum characters from header. This will allow checking maximum character based on
* {@link SmppConstants#MAXIMUM_CHARACTERS} header or determine the maximum character based on data coding
* header {@link SmppConstants#DATA_CODING}.
* <p/>
* The order of the selection is
* <ol>
* <li>If {@link SmppConstants#MAXIMUM_CHARACTERS} is set, use it</li>
* <li>If {@link SmppConstants#DATA_CODING} is set, find maximum character for the data coding</li>
* <li>Using default maximum character which is 140</li>
* </ol>
* @param msg the Spring Integration message
* @return maximum character can be sent through the session
*/
private static int maximumCharactersFromHeader(Message<?> msg) {
if (msg.getHeaders().containsKey(MAXIMUM_CHARACTERS))
return msg.getHeaders().get(MAXIMUM_CHARACTERS, Integer.class);
if (msg.getHeaders().containsKey(DATA_CODING)) {
final Object dc = msg.getHeaders().get(DATA_CODING);
if (dc instanceof Byte)
return DataCodingSpecification.getMaxCharacters((Byte)dc);
else
return DataCodingSpecification.getMaxCharacters(((DataCoding)dc).toByte());
}
return 140;
}
/**
* need to be a little flexibile about what we take in as {@link SmppConstants#REGISTERED_DELIVERY_MODE}. The value can
* be a String or a member of the {@link SMSCDeliveryReceipt} enum.
@@ -306,7 +345,9 @@ public class SmesMessageSpecification {
*/
public String send() throws Exception {
validate();
String msgId = this.smppSession.submitShortMessage(
final String msgId;
if (messagePayloadParameter == null) {
msgId = this.smppSession.submitShortMessage(
this.serviceType,
this.sourceAddressTypeOfNumber,
this.sourceAddressNumberingPlanIndicator,
@@ -326,6 +367,32 @@ public class SmesMessageSpecification {
this.dataCoding,
this.smDefaultMsgId,
this.shortMessage);
} else {
// SPEC 3.2.3
log.debug("Sending message using message_payload");
msgId = this.smppSession.submitShortMessage(
this.serviceType,
this.sourceAddressTypeOfNumber,
this.sourceAddressNumberingPlanIndicator,
this.sourceAddress,
this.destinationAddressTypeOfNumber,
this.destinationAddressNumberingPlanIndicator,
this.destinationAddress,
this.esmClass,
this.protocolId,
this.priorityFlag,
this.scheduleDeliveryTime,
this.validityPeriod,
this.registeredDelivery,
this.replaceIfPresentFlag,
this.dataCoding,
this.smDefaultMsgId,
new byte[0],
this.messagePayloadParameter
);
}
return msgId;
}
@@ -333,7 +400,11 @@ public class SmesMessageSpecification {
protected void validate() {
Assert.notNull(this.sourceAddress, "the source address must not be null");
Assert.notNull(this.destinationAddress, "the destination address must not be null");
Assert.isTrue(this.shortMessage != null && this.shortMessage.length > 0, "the message must not be null");
final boolean shortMessageSet = this.shortMessage != null && this.shortMessage.length > 0;
Assert.isTrue(messagePayloadParameter != null ^ shortMessageSet,
"message can only be set in payload or short message. cannot be both");
if (messagePayloadParameter == null)
Assert.isTrue(shortMessageSet, "the message must not be null");
}
public SmesMessageSpecification setSourceAddress(String sourceAddr) {
@@ -475,19 +546,38 @@ public class SmesMessageSpecification {
}
/**
* todo it'running not <em>quite</em> true that the payload needs to be 140c. A large message can be split up into smaller messages,
* but for now it'running more useful to have this validation in place than not.
* Setting short message. This will take into account if {@link #dataCoding} or if {@link #maxLengthSmsMessages}
* is set through header to validate the maximum characters can be set.
*
* @param s the text message body
* @return the SmesMessageSpecification
*/
public SmesMessageSpecification setShortTextMessage(String s) {
Assert.notNull(s, "the SMS message payload must not be null");
Assert.isTrue(s.length() <= this.maxLengthSmsMessages, "the SMS message payload must be 140 characters or less.");
this.shortMessage = s.getBytes();
if (esmClass != null && GSMSpecificFeature.UDHI.containedIn(esmClass)) {
log.debug("Setting short message with UDH");
this.shortMessage = UdhUtil.getMessageWithUdhInBytes(s, dataCoding.toByte());
} else {
Assert.isTrue(s.length() <= this.maxLengthSmsMessages,
"the SMS message payload must be " + maxLengthSmsMessages + " characters or less.");
this.shortMessage = DataCodingSpecification.getMessageInBytes(s, dataCoding.toByte());
}
return this;
}
/**
* Setting short message using message_payload ({@link org.jsmpp.bean.OptionalParameter.Tag#MESSAGE_PAYLOAD})
* optional parameter
* @param s the text messages body
* @return the SmesMessageSpecification
*/
public SmesMessageSpecification setShortMessageUsingPayload(String s) {
final byte[] content = DataCodingSpecification.getMessageInBytes(s, dataCoding.toByte());
this.messagePayloadParameter =
new OptionalParameter.OctetString(OptionalParameter.Tag.MESSAGE_PAYLOAD.code(), content);
return this;
}
/**
* this is a good value, but not strictly speaking universal. This is intended only for exceptional configuration cases
* <p/>
@@ -531,6 +621,7 @@ public class SmesMessageSpecification {
smDefaultMsgId = 0;
shortMessage = null; // the bytes to the 140 character text message
smppSession = null;
messagePayloadParameter = null;
return this;
}

View File

@@ -63,4 +63,20 @@ public abstract class SmppConstants {
public static final String REPLY_PATH = "REPLY_PATH";
public static final String DEST_ADDRESS = DST_ADDR;
public static final String OPTIONAL_PARAMETERS = "OPTIONAL_PARAMETERS";
/** Additional support header to allow user to customise the maximum characters can be sent. Unless this header
* is set, the default is 140 characters or if {@link #DATA_CODING} header is set, the maximum character will
* be based on {@link DataCodingSpecification#getMaxCharacters(byte)}. Setting this header manually may have
* unintended consequences.
*/
public static final String MAXIMUM_CHARACTERS = "MAXIMUM_CHARACTERS";
/** Additional support header to send the sms using message_payload instead of setting using short_message.
* This can be used when we need to send long sms. (SPEC 3.2.3).
* <p/>
* Note:
* <ul>
* <li>That not many SMSC may support payload</li>
* <li>The actual short message length which can be transmitted may vary according to the underlying network</li>
* </ul>
*/
public static final String USE_MSG_PAYLOAD_PARAM = "USE_MSG_PAYLOAD_PARAM";
}

View File

@@ -0,0 +1,41 @@
/*
* 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.core;
/**
* @author Johanes Soetanto
* @since 1.0
*/
public class UdhUtil {
/**
* Get message with UDH to byte[]. This method converts the UDH to byte[] using {@link String#getBytes()} and
* converts the string using {@link DataCodingSpecification#getMessageInBytes(String, byte)}.
*
* @param s string message
* @param dataCoding data coding
* @return byte array result
*/
public static byte[] getMessageWithUdhInBytes(String s, byte dataCoding) {
final int udhLength = ((byte)s.charAt(0))+1;
final byte[] udh = s.substring(0, udhLength).getBytes();
final byte[] content = DataCodingSpecification.getMessageInBytes(s.substring(udhLength), dataCoding);
final byte[] contentWithUdh = new byte[udhLength + content.length];
System.arraycopy(udh, 0, contentWithUdh, 0, udhLength);
System.arraycopy(content, 0, contentWithUdh, udhLength, content.length);
return contentWithUdh;
}
}

View File

@@ -45,6 +45,7 @@ import org.springframework.util.StringUtils;
public class SmppOutboundGateway extends AbstractReplyProducingMessageHandler {
@Override
protected void onInit() {
super.onInit();
Assert.isTrue(
this.smppSession.getBindType().equals(BindType.BIND_TX) ||
this.smppSession.getBindType().equals(BindType.BIND_TRX),

View File

@@ -101,6 +101,14 @@ public class ExtendedSmppSessionAdaptingDelegate implements /*Lifecycle,*/ Exten
this.session.setMessageReceiverListener(this.delegatingMessageReceiverListener);
}
/**
* Get message receiver listeners.
* @return message listener that contains multiple listeners
*/
public DelegatingMessageReceiverListener getDelegateMessageListener() {
return delegatingMessageReceiverListener;
}
public void addMessageReceiverListener(MessageReceiverListener messageReceiverListener) {
this.delegatingMessageReceiverListener.addMessageReceiverListener(messageReceiverListener);
}

View File

@@ -22,6 +22,7 @@ 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;
@@ -29,6 +30,10 @@ 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;
@@ -40,9 +45,10 @@ import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.Socket;
import java.util.Arrays;
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},
@@ -52,33 +58,37 @@ import java.util.Set;
* <p/>
* Here is a breakdown of the supported parameters on this factory bean:
* <p/>
* host the SMSC host to which the session is bound (think of this as the host of your email server)
* port the SMSC port to which the session is bound (think of this as a port on your email server)
* 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}).
* systemId the system ID for the server being bound to
* password the password for the server being bound to
* systemType the SMSC system type
* addrTon a value from the {@link org.jsmpp.bean.TypeOfNumber} enumeration. default is {@link org.jsmpp.bean.TypeOfNumber#UNKNOWN}
* addrNpi a value from the {@link org.jsmpp.bean.NumberingPlanIndicator} enumeration. Default is {@link org.jsmpp.bean.NumberingPlanIndicator#UNKNOWN}
* addressRange can be null. Specifies the address range.
* timeout a good default value is 60000 (1 minute)
* <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
* <p/>
* todo support a proxied SMPPSession that automatically recovers from disconnects a la the examples {@link org.jsmpp.examples.gateway.AutoReconnectGateway}
* @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 {
public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>, SmartLifecycle, InitializingBean,
DisposableBean {
/**
* impl of {@link Lifecycle} that connects and disconnects respectively in
* {@link org.springframework.context.Lifecycle#start()} and {@link org.springframework.context.Lifecycle#stop()}
*
* @author Josh Long
*/
private Set<MessageReceiverListener> messageReceiverListeners = new HashSet<MessageReceiverListener>();
private boolean autoStartup;
private volatile boolean running;
@@ -88,6 +98,7 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
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
@@ -95,8 +106,15 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
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;
@@ -145,39 +163,82 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
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;
}
public void setSessionStateListener(SessionStateListener sessionStateListener) {
this.sessionStateListener = sessionStateListener;
}
/**
* 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 setMessageReceiverListeners(MessageReceiverListener... listeners) {
setMessageReceiverListeners(new HashSet<MessageReceiverListener>(Arrays.asList(listeners)));
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 {
SMPPSession smppSession = null;
if (!ssl) {
smppSession = new SMPPSession();
} else {
smppSession = new SMPPSession(new SynchronizedPDUSender(new DefaultPDUSender(new DefaultComposer())), new DefaultPDUReader(), sslConnectionFactory);
}
final SMPPSession smppSession = createNewSession();
final ExtendedSmppSessionAdaptingDelegate extendedSmppSessionAdaptingDelegate;
if (reconnect) {
sessionFactoryBean.setAutodetectInterfaces(false);
sessionFactoryBean.setTarget(smppSession);
final SMPPSession proxiedSession = (SMPPSession)sessionFactoryBean.getObject();
ExtendedSmppSessionAdaptingDelegate extendedSmppSessionAdaptingDelegate = new ExtendedSmppSessionAdaptingDelegate(smppSession, new ConnectingLifecycle(smppSession));
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;
}
@@ -196,14 +257,10 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
/**
* {@inheritDoc}
*/
@Override
public void stop(Runnable callback) {
try {
log.debug("shutting down in " + getClass().getName() + "#stop(Runnable).");
callback.run();
} catch (Throwable throwable) {
log.warn("error when trying to shutdown " + getClass().getName() + ", could not invoke the callback's Runnable#run method");
}
this.stop();
callback.run();
}
/**
@@ -211,6 +268,10 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
*/
public void start() {
log.debug("starting up in " + getClass().getName() + "#start().");
if (reconnectingExecutor == null) {
this.reconnectingExecutor = Executors.newFixedThreadPool(1);
}
( product).start();
this.running = true;
}
@@ -221,6 +282,12 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
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;
}
@@ -261,7 +328,34 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
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 {
@@ -276,7 +370,15 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
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() {
@@ -311,12 +413,12 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
if (session.getSessionState().isBound()) {
try {
session.unbindAndClose();
} catch (Throwable t) {
log.warn("couldn't close and unbind the session", t);
} catch (Exception t) {
log.warn("Couldn't close and unbind the session", t);
}
}
} else {
log.warn("the smppSession given to close is null");
log.warn("The smppSession given to close is null");
}
}
@@ -325,46 +427,149 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
session.connectAndBind(host, port, bindType, systemId, password, systemType, addrTon, addrNpi, addressRange, timeout);
this.running = true;
} catch (IOException e) {
log.error("something happened when trying to connect", 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());
}
}
}
}
/* private void reconnectAfter(final long timeInMillis) {
new Thread() {
@Override
public void run() {
logger.info("Schedule reconnect after " + timeInMillis + " millis");
try {
Thread.sleep(timeInMillis);
} catch (InterruptedException e) {
}
/**
* 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 {
int attempt = 0;
while (session == null || session.getSessionState().equals(SessionState.CLOSED)) {
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 {
logger.info("Reconnecting attempt #" + (++attempt) + "...");
session = newSession();
} catch (IOException e) {
logger.error("Failed opening connection and bind to " + remoteIpAddress + ":" + remotePort, e);
// wait for a second
try { Thread.sleep(1000); } catch (InterruptedException ee) {}
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");
}
}.start();
}
}
@Override
public void start() {
connect();
private class SessionStateListenerImpl implements SessionStateListener {
public void onStateChange(SessionState newState, SessionState oldState,
Object source) {
if (newState.equals(SessionState.CLOSED)) {
logger.info("Session closed");
reconnectAfter(reconnectInterval);
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);
}
}
});
}
}
*/
}

View File

@@ -66,6 +66,9 @@
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="smppGatewayType">
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
</xsd:choice>
<xsd:attribute name="error-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
@@ -106,8 +109,10 @@
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:choice minOccurs="0" maxOccurs="2">
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
<xsd:element name="request-handler-advice-chain"
type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:choice>
<xsd:attributeGroup ref="coreSmppComponentAttributes"/>
<xsd:attribute name="channel" type="xsd:string">
@@ -166,6 +171,11 @@
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="smppGatewayType">
<xsd:choice minOccurs="0" maxOccurs="2">
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
<xsd:element name="request-handler-advice-chain"
type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
</xsd:choice>
<xsd:attribute name="time-formatter">
<xsd:annotation>
<xsd:documentation>
@@ -378,9 +388,6 @@
Defines common configuration for gateway adapters.
</xsd:documentation>
</xsd:annotation>
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
</xsd:choice>
<xsd:attributeGroup ref="coreSmppComponentAttributes"/>
<xsd:attribute name="reply-channel" type="xsd:string">
<xsd:annotation>

View File

@@ -31,6 +31,8 @@ import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -46,11 +48,27 @@ import java.util.concurrent.TimeoutException;
* @since 1.0
*/
public class MockSmppServer extends ServerResponseDeliveryAdapter implements Runnable, ServerMessageReceiverListener {
/** Agreement to make it easy to test some functionality */
public static final class Agreement {
/** Agreement to throw error when destination set to this value */
public static final String THROW_NO_DESTINATION_EXCEPTION = "NoRouteDestination";
/** Agreement to delay processing of sms message. Useful to simulate slow connection */
public static final String DELAY_PROCESSING = "DelayMe";
/** Agreement to delay sending back delivery receipt. Useful for testing Delivery Receipt */
public static final String DELAY_DELIVERY_RECEIPT = "DelayDeliveryReceipt";
}
private static int messageDelay = 3000;
private static final Logger logger = LoggerFactory.getLogger(MockSmppServer.class);
private String systemId;
private String password;
private int port;
private Map<SMPPServerSession,String> connectionSessionMap = new HashMap<SMPPServerSession,String>();
private boolean run = true;
private int acceptConnectionTimeout = 5000;
private SMPPServerSessionListener sessionListener;
private boolean initServerAtStart = true;
private final ExecutorService execService = Executors.newFixedThreadPool(5);
private final ExecutorService execServiceDelReceipt = Executors.newFixedThreadPool(100);
@@ -64,14 +82,24 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
public void run() {
try {
SMPPServerSessionListener sessionListener = new SMPPServerSessionListener(port);
this.sessionListener = new SMPPServerSessionListener(port);
sessionListener.setTimeout(acceptConnectionTimeout);
logger.info("Listening on port {}", port);
while (true) {
SMPPServerSession serverSession = sessionListener.accept();
logger.info("Accepting connection for session {}", serverSession.getSessionId());
serverSession.setMessageReceiverListener(this);
serverSession.setResponseDeliveryListener(this);
execService.execute(new WaitBindTask(serverSession, systemId, password, connectionSessionMap));
while (run) {
try {
SMPPServerSession serverSession = sessionListener.accept();
logger.info("Accepting connection for session {}", serverSession.getSessionId());
serverSession.setMessageReceiverListener(this);
serverSession.setResponseDeliveryListener(this);
execService.execute(new WaitBindTask(serverSession, systemId, password, connectionSessionMap));
}
catch (SocketTimeoutException ste) {
logger.info("SocketTimeoutException: {}", ste.getMessage());
}
catch (SocketException se) {
logger.info("SocketException: {}", se.getMessage());
}
}
}
catch (IOException e) {
@@ -79,14 +107,44 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
}
}
/**
* Forcing the server to stop.
* @throws InterruptedException
* @throws IOException
*/
public void stop() throws InterruptedException, IOException {
run = false;
execService.shutdown();
sessionListener.close();
for (SMPPServerSession server : connectionSessionMap.keySet()) {
server.close();
}
}
public QuerySmResult onAcceptQuerySm(QuerySm querySm,
SMPPServerSession source) throws ProcessRequestException {
logger.info("Accepting query sm, but not implemented");
return null;
}
/* Perform special handling just to simulate something on the SMSC */
private void onSpecialHandling(SubmitSm submitSm,
SMPPServerSession source) throws ProcessRequestException {
if (submitSm.getDestAddress().equals(Agreement.THROW_NO_DESTINATION_EXCEPTION)) {
throw new ProcessRequestException ("Invalid Dest Addr", 0x0B);
}
if (new String(submitSm.getShortMessage()).equals(Agreement.DELAY_PROCESSING)) {
try {
logger.debug("Delaying handling for {} ms", messageDelay);
Thread.sleep(3000);
} catch (InterruptedException ignored) {}
}
}
public MessageId onAcceptSubmitSm(SubmitSm submitSm,
SMPPServerSession source) throws ProcessRequestException {
onSpecialHandling(submitSm, source);
MessageId messageId = messageIDGenerator.newMessageId();
logger.debug("Receiving submit_sm '{}', and will return message id {}",
new String(submitSm.getShortMessage()), messageId);
@@ -131,7 +189,11 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
throws ProcessRequestException {
}
private static class WaitBindTask implements Runnable {
public void setAcceptConnectionTimeout(int acceptConnectionTimeout) {
this.acceptConnectionTimeout = acceptConnectionTimeout;
}
private static class WaitBindTask implements Runnable {
private final SMPPServerSession serverSession;
private final String systemId;
private final String password;
@@ -242,7 +304,12 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
public void run() {
try {
Thread.sleep(1000);
if (new String(shortMessage).equals(Agreement.DELAY_DELIVERY_RECEIPT)) {
logger.debug("Receive request to delay sending of delivery receipt");
Thread.sleep(messageDelay);
} else {
Thread.sleep(300); // just give a little bit of delay
}
} catch (InterruptedException e1) {
e1.printStackTrace();
}
@@ -257,6 +324,7 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
DeliveryReceipt delRec = new DeliveryReceipt(stringValue, totalSubmitted, totalDelivered, new Date(),
new Date(), DeliveryReceiptState.DELIVRD, null, new String(shortMessage));
logger.debug("Sending delivery receipt for message id " + messageId + ":" + stringValue);
session.deliverShortMessage(
"mc",
sourceAddrTon,
@@ -271,7 +339,7 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
new RegisteredDelivery(0),
DataCodings.ZERO,
delRec.toString().getBytes());
logger.debug("Sending delivery receipt for message id " + messageId + ":" + stringValue);
logger.debug("Delivery receipt sent");
} catch (Exception e) {
logger.error("Failed sending delivery_receipt for message id " + messageId + ":" + stringValue, e);
}
@@ -351,13 +419,27 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
@PostConstruct
public void onPostConstruct() {
logger.debug("Starting mock SMPP server");
execService.submit(this);
}
if (initServerAtStart) {
startServer();
}
}
public void startServer() {
logger.debug("Starting mock SMPP server");
execService.submit(this);
}
public void restartServer() throws InterruptedException, IOException {
logger.debug("Stopping server");
stop();
connectionSessionMap.clear();
startServer();
}
@PreDestroy
public void onDestroy() {
public void onDestroy() throws InterruptedException, IOException {
logger.debug("Destroying mock SMPP server");
stop();
connectionSessionMap.clear();
}
}

View File

@@ -0,0 +1,75 @@
/* 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;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.MessagingException;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.smpp.core.SmppConstants;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* exercises the outbound adapter.
*
* @author Johanes Soetanto
* @since 1.0
*/
@ContextConfiguration("classpath:TestSmppOutboundChannelAdapterWithChain-context.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSmppOutboundChannelAdapterWithChain {
@Autowired
private MessagingTemplate messagingTemplate;
@Autowired
private MessageChannel outChannel;
private String smsMessageToSend = "jSMPP is truly a convenient, and powerful API for SMPP " +
"on the Java and Spring Integration platforms (sent " + System.currentTimeMillis() + ")";
@Test
public void testSendingGoesToExceptionChannel() throws Throwable {
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
.setHeader(SmppConstants.SRC_ADDR, "1616")
.setHeader(SmppConstants.DST_ADDR, "NoRouteDestination")
.build();
outChannel.send(smsMsg);
Thread.sleep(500);
Message<?> exception = messagingTemplate.receive("exceptionChannel");
Assert.assertNotNull(exception);
Assert.assertTrue(exception.getPayload() instanceof MessagingException);
}
@Test
public void testSending() throws Throwable {
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
.setHeader(SmppConstants.SRC_ADDR, "1616")
.setHeader(SmppConstants.DST_ADDR, "1616")
.build();
outChannel.send(smsMsg);
Thread.sleep(5000);
Message<?> exception = messagingTemplate.receive("exceptionChannel");
Assert.assertNull(exception);
}
}

View File

@@ -0,0 +1,77 @@
/* 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;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.MessagingException;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.smpp.core.SmppConstants;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Simple tests to make sure that gateway will perform retry and route MessagingException to exception channel when
* advice chain is defined with a retry advice and error callback.
*
* @author Johanes Soetanto
* @since 1.0
*/
@ContextConfiguration("classpath:TestSmppOutboundGatewayWithChain-context.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TestSmppOutboundGatewayWithChain {
@Autowired
private MessagingTemplate messagingTemplate;
@Autowired
private MessageChannel outChannel;
private String smsMessageToSend = "jSMPP is truly a convenient, and powerful API for SMPP " +
"on the Java and Spring Integration platforms (sent " + System.currentTimeMillis() + ")";
@Test
public void testSendingGoesToExceptionChannel() throws Throwable {
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
.setHeader(SmppConstants.SRC_ADDR, "1616")
.setHeader(SmppConstants.DST_ADDR, "NoRouteDestination")
.build();
outChannel.send(smsMsg);
Thread.sleep(500);
Message<?> exception = messagingTemplate.receive("exceptionChannel");
Assert.assertNotNull(exception);
Assert.assertTrue(exception.getPayload() instanceof MessagingException);
}
@Test
public void testSendingGoesToReplyChannel() throws Throwable {
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
.setHeader(SmppConstants.SRC_ADDR, "1616")
.setHeader(SmppConstants.DST_ADDR, "1616")
.build();
outChannel.send(smsMsg);
Thread.sleep(500);
Message<?> exception = messagingTemplate.receive("replyChannel");
Assert.assertNotNull(exception);
Assert.assertTrue(exception.getPayload() instanceof String);
}
}

View File

@@ -0,0 +1,133 @@
package org.springframework.integration.smpp;
import org.junit.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.smpp.session.SmppSessionFactoryBean;
import org.springframework.integration.test.util.SocketUtils;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* This is to test whether reconnection will work. The test procedure is
* <ol>
* <li>Client connects to SMSC</li>
* <li>SMSC shuts down</li>
* <li>SMSC restart</li>
* <li>Client reconnect to SMSC</li>
* </ol>
*
* @author Johanes Soetanto
* @since 1.0
*/
public class TestSmppSessionReconnection {
private Logger log = LoggerFactory.getLogger(getClass());
int port;
String systemId = "pavel";
String pass = "wpsd";
MockSmppServer server;
SmppSessionFactoryBean client;
ExecutorService executor = Executors.newSingleThreadExecutor();
int serverAcceptTimeout = 1500;
int clientReconnectInterval = 1000;
@Before
public void setUp() throws Exception {
port = SocketUtils.findAvailableServerSocket(13000);
client = new SmppSessionFactoryBean();
client.setPort(port);
client.setSystemId(systemId);
client.setPassword(pass);
client.setReconnectInterval((long)clientReconnectInterval);
}
private void startServer() {
executor.submit(new Runnable() {
@Override
public void run() {
try {
log.debug("Starting server");
server = new MockSmppServer(port, systemId, pass);
server.setAcceptConnectionTimeout(serverAcceptTimeout);
server.onPostConstruct();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
//@org.junit.Test
public void testReconnection() throws Exception {
startServer();
Thread.sleep(500);
log.debug("Starting client");
client.afterPropertiesSet();
client.start();
Thread.sleep(3000);
log.debug("Stopping server");
server.stop();
server.onDestroy();
Thread.sleep(3000);
log.debug("Starting server again");
startServer();
log.debug("The client should reconnect");
Thread.sleep(3000);
}
// if client session is destroyed by the container, we don't want to try to re-establish session
//@org.junit.Test
public void testReconnection_whenClientSessionDestroyed() throws Exception {
startServer();
Thread.sleep(500);
log.debug("Starting client");
client.afterPropertiesSet();
client.start();
Thread.sleep(2000);
log.debug("Destroy the client");
client.destroy();
Thread.sleep(3000);
log.debug("Stopping server");
server.stop();
server.onDestroy();
Thread.sleep(3000);
log.debug("Starting server again");
startServer();
Thread.sleep(3000);
}
// if reconnect is disabled, well do not reconnect
//@org.junit.Test
public void testDisableReconnect() throws Exception {
startServer();
Thread.sleep(500);
client.setReconnect(false);
client.afterPropertiesSet();
client.start();
Thread.sleep(2000);
log.debug("Stop server");
server.stop();
server.onDestroy();
Thread.sleep(2000);
log.debug("Starting server again");
startServer();
Thread.sleep(2000);
log.debug("There should be no reconnection from client");
}
}

View File

@@ -20,8 +20,11 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.Message;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.smpp.core.SmppConstants;
import org.springframework.integration.smpp.outbound.SmppOutboundChannelAdapter;
import org.springframework.integration.smpp.session.ExtendedSmppSession;
@@ -77,6 +80,19 @@ public class SmppOutboundChannelAdapterParserTests {
template.send("target", message);
}
@Test
public void testWithAdvice() throws Exception {
context = new ClassPathXmlApplicationContext("SmppOutboundChannelAdapterParserTests.xml", getClass());
AbstractEndpoint endpoint = this.context.getBean("smppOutboundChannelAdapterWithChain", AbstractEndpoint.class);
MessageHandler handler = TestUtils.getPropertyValue(endpoint, "handler", MessageHandler.class);
Message<?> message = MessageBuilder.withPayload("foo")
.setHeader(SmppConstants.SRC_ADDR, "X")
.setHeader(SmppConstants.DST_ADDR, "Y")
.build();
handler.handleMessage(message);
assertEquals(1, adviceCalled);
}
@After
public void tearDown(){
if(context != null){
@@ -89,4 +105,12 @@ public class SmppOutboundChannelAdapterParserTests {
consumer = this.context.getBean("smppOutboundChannelAdapter", EventDrivenConsumer.class);
}
private static int adviceCalled = 0;
public static class FooAdvice extends AbstractRequestHandlerAdvice {
@Override
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) throws Exception {
adviceCalled++;
return null;
}
}
}

View File

@@ -18,10 +18,16 @@ import org.junit.After;
import org.junit.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.Message;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.smpp.core.SmppConstants;
import org.springframework.integration.smpp.outbound.SmppOutboundGateway;
import org.springframework.integration.smpp.session.ExtendedSmppSession;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import static org.junit.Assert.assertEquals;
@@ -72,6 +78,20 @@ public class SmppOutboundGatewayParserTests {
assertNotNull(timeFormatter);
}
@Test
public void testWithAdvice() throws Exception {
context = new ClassPathXmlApplicationContext("SmppOutboundGatewayParserTests.xml", getClass());
AbstractEndpoint endpoint = this.context.getBean("smppOutboundGatewayWithAdvice", AbstractEndpoint.class);
System.out.println("endpoint "+endpoint);
MessageHandler handler = TestUtils.getPropertyValue(endpoint, "handler", MessageHandler.class);
Message<?> message = MessageBuilder.withPayload("foo")
.setHeader(SmppConstants.SRC_ADDR, "X")
.setHeader(SmppConstants.DST_ADDR, "Y")
.build();
handler.handleMessage(message);
assertEquals(1, adviceCalled);
}
@After
public void tearDown() {
if (context != null) {
@@ -84,4 +104,13 @@ public class SmppOutboundGatewayParserTests {
consumer = this.context.getBean(gatewayId, EventDrivenConsumer.class);
}
private static int adviceCalled = 0;
public static class FooAdvice extends AbstractRequestHandlerAdvice {
@Override
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) throws Exception {
adviceCalled++;
return null;
}
}
}

View File

@@ -0,0 +1,64 @@
/* 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.core;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.Charset;
/**
* @author Johanes Soetanto
* @since 1.0
*/
public class DataCodingSpecificationTest {
@Test
public void testGetMaxCharacters() throws Exception {
Assert.assertEquals(160, DataCodingSpecification.getMaxCharacters((byte)0));
Assert.assertEquals(160, DataCodingSpecification.getMaxCharacters((byte)1));
Assert.assertEquals(140, DataCodingSpecification.getMaxCharacters((byte)2));
Assert.assertEquals(140, DataCodingSpecification.getMaxCharacters((byte)3));
Assert.assertEquals(140, DataCodingSpecification.getMaxCharacters((byte)4));
Assert.assertEquals(70, DataCodingSpecification.getMaxCharacters((byte)5));
Assert.assertEquals(140, DataCodingSpecification.getMaxCharacters((byte)6));
Assert.assertEquals(140, DataCodingSpecification.getMaxCharacters((byte)7));
Assert.assertEquals(70, DataCodingSpecification.getMaxCharacters((byte)8));
Assert.assertEquals(70, DataCodingSpecification.getMaxCharacters((byte)10));
Assert.assertEquals(70, DataCodingSpecification.getMaxCharacters((byte)13));
Assert.assertEquals(70, DataCodingSpecification.getMaxCharacters((byte)14));
}
private void assertEqualsAndSupported(String expected, String charsetName) {
Assert.assertEquals(expected, charsetName);
Assert.assertTrue(Charset.isSupported(charsetName));
}
@Test
public void testGetCharsetName() throws Exception {
assertEqualsAndSupported("UTF-8", DataCodingSpecification.getCharsetName((byte)0));
assertEqualsAndSupported("US-ASCII", DataCodingSpecification.getCharsetName((byte)1));
assertEqualsAndSupported("UTF-8", DataCodingSpecification.getCharsetName((byte)2));
assertEqualsAndSupported("ISO-8859-1", DataCodingSpecification.getCharsetName((byte)3));
assertEqualsAndSupported("UTF-8", DataCodingSpecification.getCharsetName((byte)4));
assertEqualsAndSupported("EUC-JP", DataCodingSpecification.getCharsetName((byte)5));
assertEqualsAndSupported("ISO-8859-5", DataCodingSpecification.getCharsetName((byte)6));
assertEqualsAndSupported("ISO-8859-8", DataCodingSpecification.getCharsetName((byte)7));
assertEqualsAndSupported("UTF-16", DataCodingSpecification.getCharsetName((byte)8));
assertEqualsAndSupported("EUC-JP", DataCodingSpecification.getCharsetName((byte)10));
assertEqualsAndSupported("EUC-JP", DataCodingSpecification.getCharsetName((byte)13));
assertEqualsAndSupported("EUC-KR", DataCodingSpecification.getCharsetName((byte)14));
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.core;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.Random;
/**
* @author Johanes Soetanto
* @since 1.0
*/
public class UdhUtilTest {
short ref = (short)new Random().nextInt(Short.MAX_VALUE);
byte[] udh;
byte total = 2;
byte seqNum = 1;
@Before
public void setUp() throws Exception {
udh = new byte[6];
udh[0] = 0x05;
udh[1] = 0x00;
udh[2] = 3;
udh[3] = (byte)(ref & 0x7F);
udh[4] = total;
udh[5] = seqNum;
}
@Test
public void testGetMessageWithUdhInBytes() throws Exception {
String message = "This is the message";
byte dataCoding = 0;
String messageWithUdh = new String(udh).concat(message);
byte[] result = UdhUtil.getMessageWithUdhInBytes(messageWithUdh, dataCoding);
Assert.assertEquals(udh[0], result[0]);
Assert.assertEquals(udh[1], result[1]);
Assert.assertEquals(udh[2], result[2]);
Assert.assertEquals(udh[3], result[3]);
Assert.assertEquals(udh[4], result[4]);
Assert.assertEquals(udh[5], result[5]);
byte[] content = new byte[result.length-6];
System.arraycopy(result, 6, content, 0, content.length);
String decoded = new String(content, "UTF-8");
Assert.assertEquals(message, decoded);
}
@Test
public void testGetMessageWithUdhInBytes_dataCoding8() throws Exception {
String message = "這將是一個長期的短信";
byte dataCoding = 8;
String messageWithUdh = new String(udh).concat(message);
byte[] result = UdhUtil.getMessageWithUdhInBytes(messageWithUdh, dataCoding);
Assert.assertEquals(udh[0], result[0]);
Assert.assertEquals(udh[1], result[1]);
Assert.assertEquals(udh[2], result[2]);
Assert.assertEquals(udh[3], result[3]);
Assert.assertEquals(udh[4], result[4]);
Assert.assertEquals(udh[5], result[5]);
byte[] content = new byte[result.length-6];
System.arraycopy(result, 6, content, 0, content.length);
String decoded = new String(content, "UTF-16");
Assert.assertEquals(message, decoded);
}
}

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-smpp="http://www.springframework.org/schema/integration/smpp"
xsi:schemaLocation="
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/smpp http://www.springframework.org/schema/integration/smpp/spring-integration-smpp.xsd">
<import resource="classpath:smppConnection-context.xml"/>
<!-- this is channel for going out -->
<int:channel id="outChannel">
<int:interceptors>
<int:wire-tap channel="outChannelLogger"/>
</int:interceptors>
</int:channel>
<int:logging-channel-adapter id="outChannelLogger" expression="'Outbound Channel Adapter: '+payload"/>
<!-- this is the gateway for testing -->
<int-smpp:outbound-channel-adapter channel="outChannel" smpp-session-ref="session">
<int-smpp:request-handler-advice-chain>
<ref bean="smppSendingRetryAdvice"/>
</int-smpp:request-handler-advice-chain>
</int-smpp:outbound-channel-adapter>
<!-- this is smpp session -->
<bean id="session"
class="org.springframework.integration.smpp.session.SmppSessionFactoryBean">
<property name="host" value="${smpp.host}" />
<property name="port" ref="smppPort" />
<property name="password" value="${smpp.password}" />
<property name="systemId" value="${smpp.systemId}" />
<property name="bindType" value="BIND_TRX" />
</bean>
<!-- example of using chain advice to retry and send the error to exception channel -->
<bean id="smppSendingRetryAdvice"
class="org.springframework.integration.handler.advice.RequestHandlerRetryAdvice">
<property name="recoveryCallback">
<bean class="org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer">
<constructor-arg ref="exceptionChannel" />
</bean>
</property>
<property name="retryTemplate">
<bean class="org.springframework.retry.support.RetryTemplate">
<property name="retryPolicy">
<bean class="org.springframework.retry.policy.NeverRetryPolicy" />
</property>
</bean>
</property>
</bean>
<int:channel id="exceptionChannel">
<int:queue capacity="10" />
<int:interceptors>
<int:wire-tap channel="exceptionLogger"/>
</int:interceptors>
</int:channel>
<int:logging-channel-adapter id="exceptionLogger" log-full-message="true" level="ERROR" />
<!-- messaging template -->
<bean class="org.springframework.integration.core.MessagingTemplate">
<property name="receiveTimeout" value="1000"/>
</bean>
</beans>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-smpp="http://www.springframework.org/schema/integration/smpp"
xsi:schemaLocation="
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/smpp http://www.springframework.org/schema/integration/smpp/spring-integration-smpp.xsd">
<import resource="classpath:smppConnection-context.xml"/>
<!-- this is channel for going out -->
<int:channel id="outChannel">
<int:interceptors>
<int:wire-tap channel="outChannelLogger"/>
</int:interceptors>
</int:channel>
<int:logging-channel-adapter id="outChannelLogger" expression="'Outbound Gateway: ' + payload"/>
<!-- this is the gateway for testing -->
<int-smpp:outbound-gateway request-channel="outChannel"
reply-channel="replyChannel"
smpp-session-ref="session">
<int-smpp:request-handler-advice-chain>
<ref bean="smppSendingRetryAdvice"/>
</int-smpp:request-handler-advice-chain>
</int-smpp:outbound-gateway>
<!-- this is channel to receive reply -->
<int:channel id="replyChannel">
<int:queue capacity="10"/>
</int:channel>
<!-- this is smpp session -->
<bean id="session"
class="org.springframework.integration.smpp.session.SmppSessionFactoryBean">
<property name="host" value="${smpp.host}" />
<property name="port" ref="smppPort" />
<property name="password" value="${smpp.password}" />
<property name="systemId" value="${smpp.systemId}" />
<property name="bindType" value="BIND_TRX" />
</bean>
<!-- example of using chain advice to retry and send the error to exception channel -->
<bean id="smppSendingRetryAdvice"
class="org.springframework.integration.handler.advice.RequestHandlerRetryAdvice">
<property name="recoveryCallback">
<bean class="org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer">
<constructor-arg ref="exceptionChannel" />
</bean>
</property>
<property name="retryTemplate">
<bean class="org.springframework.retry.support.RetryTemplate">
<property name="retryPolicy">
<bean class="org.springframework.retry.policy.NeverRetryPolicy" />
</property>
</bean>
</property>
</bean>
<int:channel id="exceptionChannel">
<int:queue capacity="10" />
<int:interceptors>
<int:wire-tap channel="exceptionLogger"/>
</int:interceptors>
</int:channel>
<int:logging-channel-adapter id="exceptionLogger" log-full-message="true" level="ERROR"/>
<!-- messaging template -->
<bean class="org.springframework.integration.core.MessagingTemplate">
<property name="receiveTimeout" value="1000"/>
</bean>
</beans>

View File

@@ -36,4 +36,11 @@
source-address="12345" source-ton="SUBSCRIBER_NUMBER" channel="target">
</int-smpp:outbound-channel-adapter>
<int-smpp:outbound-channel-adapter id="smppOutboundChannelAdapterWithChain"
channel="target" smpp-session-ref="session">
<int-smpp:request-handler-advice-chain>
<bean class="org.springframework.integration.smpp.config.xml.SmppOutboundChannelAdapterParserTests$FooAdvice"/>
</int-smpp:request-handler-advice-chain>
</int-smpp:outbound-channel-adapter>
</beans>

View File

@@ -40,4 +40,15 @@
</int-smpp:session>
</int-smpp:outbound-gateway>
<int-smpp:outbound-gateway id="smppOutboundGatewayWithAdvice" request-channel="in" reply-channel="out">
<int-smpp:session>
<bean
class="org.springframework.integration.smpp.config.xml.MockSmppSessionFactory"
factory-method="getOutSmppSession" />
</int-smpp:session>
<int-smpp:request-handler-advice-chain>
<bean class="org.springframework.integration.smpp.config.xml.SmppOutboundGatewayParserTests$FooAdvice"/>
</int-smpp:request-handler-advice-chain>
</int-smpp:outbound-gateway>
</beans>