diff --git a/spring-integration-smpp/build.gradle b/spring-integration-smpp/build.gradle
index a785176..75abfac 100644
--- a/spring-integration-smpp/build.gradle
+++ b/spring-integration-smpp/build.gradle
@@ -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"
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/AbstractReceivingMessageListener.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/AbstractReceivingMessageListener.java
index ae36adc..83daf41 100644
--- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/AbstractReceivingMessageListener.java
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/AbstractReceivingMessageListener.java
@@ -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);
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/DataCodingSpecification.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/DataCodingSpecification.java
new file mode 100644
index 0000000..fe76393
--- /dev/null
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/DataCodingSpecification.java
@@ -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
+ *
+ * - 160 for data coding 0/1
+ * - 70 for data coding 5/8/10/13/14
+ * - 140 for others
+ *
+ *
+ * @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:
+ *
+ * - US-ASCII for data coding 1
+ * - ISO-8859-1 for data coding 3
+ * - ISO-8859-5 for data coding 6
+ * - ISO-8859-8 for data coding 7
+ * - UTF-16 for data coding 8
+ * - EUC-KR for data coding 14
+ * - EUC-JP for data coding 5/10/13
+ * - UTF-8 for others
+ *
+ *
+ * @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();
+ }
+}
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java
index 3f0d826..1f45d12 100644
--- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java
@@ -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.valueIfHeaderExists(DST_NPI, msg));
spec.setSourceAddressNumberingPlanIndicator(SmesMessageSpecification.valueIfHeaderExists(SRC_NPI, msg));
spec.setDestinationAddressTypeOfNumber(SmesMessageSpecification.valueIfHeaderExists(DST_TON, msg));
spec.setSourceAddressTypeOfNumber(SmesMessageSpecification.valueIfHeaderExists(SRC_TON, msg));
spec.setServiceType(SmesMessageSpecification.valueIfHeaderExists(SERVICE_TYPE, msg));
- spec.setEsmClass(SmesMessageSpecification.esmClassFromHeader(msg));
spec.setScheduleDeliveryTime(SmesMessageSpecification.valueIfHeaderExists(SCHEDULED_DELIVERY_TIME, msg));
- spec.setDataCoding(SmesMessageSpecification. dataCodingFromHeader( msg));
spec.setValidityPeriod(SmesMessageSpecification.valueIfHeaderExists(VALIDITY_PERIOD, msg));
// byte landmine. autoboxing causes havoc with null 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}.
+ *
+ * The order of the selection is
+ *
+ * - If {@link SmppConstants#MAXIMUM_CHARACTERS} is set, use it
+ * - If {@link SmppConstants#DATA_CODING} is set, find maximum character for the data coding
+ * - Using default maximum character which is 140
+ *
+ * @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 quite 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
*
@@ -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;
}
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmppConstants.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmppConstants.java
index a9df301..6f74273 100644
--- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmppConstants.java
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmppConstants.java
@@ -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).
+ *
+ * Note:
+ *
+ * - That not many SMSC may support payload
+ * - The actual short message length which can be transmitted may vary according to the underlying network
+ *
+ */
+ public static final String USE_MSG_PAYLOAD_PARAM = "USE_MSG_PAYLOAD_PARAM";
}
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/UdhUtil.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/UdhUtil.java
new file mode 100644
index 0000000..f3dfa9b
--- /dev/null
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/UdhUtil.java
@@ -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;
+ }
+}
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/outbound/SmppOutboundGateway.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/outbound/SmppOutboundGateway.java
index 487053d..b35354e 100644
--- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/outbound/SmppOutboundGateway.java
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/outbound/SmppOutboundGateway.java
@@ -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),
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/ExtendedSmppSessionAdaptingDelegate.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/ExtendedSmppSessionAdaptingDelegate.java
index 79c6524..e2d0f64 100644
--- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/ExtendedSmppSessionAdaptingDelegate.java
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/ExtendedSmppSessionAdaptingDelegate.java
@@ -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);
}
diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/SmppSessionFactoryBean.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/SmppSessionFactoryBean.java
index bc1cd8d..6828112 100644
--- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/SmppSessionFactoryBean.java
+++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/session/SmppSessionFactoryBean.java
@@ -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;
*
* Here is a breakdown of the supported parameters on this factory bean:
*
- * 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)
+ *
+ * - 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)
+ * - transactionTimeout - timeout for doing work with session. e.g. sending message (default 2 seconds)
+ * - reconnect - boolean whether we allow the session to reconnect. (default true)
+ * - reconnectInterval - interval between reconnection in milliseconds. (default 5 seconds)
+ *
+ *
*
* @author Josh Long
- *
- * 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, SmartLifecycle, InitializingBean {
+public class SmppSessionFactoryBean implements FactoryBean, 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 messageReceiverListeners = new HashSet();
private boolean autoStartup;
private volatile boolean running;
@@ -88,6 +98,7 @@ public class SmppSessionFactoryBean implements FactoryBean,
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 requires 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,
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,
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(Arrays.asList(listeners)));
+ public void setSessionStateListener(SessionStateListener sessionStateListener) {
+ this.sessionStateListener = sessionStateListener;
}
public void setMessageReceiverListeners(Set 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,
/**
* {@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,
*/
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,
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,
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,
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,
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,
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);
+ }
+ }
+ });
+ }
}
- */
+}
\ No newline at end of file
diff --git a/spring-integration-smpp/src/main/resources/org/springframework/integration/smpp/config/xml/spring-integration-smpp-2.2.xsd b/spring-integration-smpp/src/main/resources/org/springframework/integration/smpp/config/xml/spring-integration-smpp-2.2.xsd
index 1b3766d..333ca95 100644
--- a/spring-integration-smpp/src/main/resources/org/springframework/integration/smpp/config/xml/spring-integration-smpp-2.2.xsd
+++ b/spring-integration-smpp/src/main/resources/org/springframework/integration/smpp/config/xml/spring-integration-smpp-2.2.xsd
@@ -66,6 +66,9 @@
+
+
+
@@ -106,8 +109,10 @@
-
+
+
@@ -166,6 +171,11 @@
+
+
+
+
@@ -378,9 +388,6 @@
Defines common configuration for gateway adapters.
-
-
-
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/MockSmppServer.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/MockSmppServer.java
index 4316588..04cb320 100644
--- a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/MockSmppServer.java
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/MockSmppServer.java
@@ -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 connectionSessionMap = new HashMap();
+ 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();
}
}
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundChannelAdapterWithChain.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundChannelAdapterWithChain.java
new file mode 100644
index 0000000..788f87c
--- /dev/null
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundChannelAdapterWithChain.java
@@ -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 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 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);
+ }
+}
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundGatewayWithChain.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundGatewayWithChain.java
new file mode 100644
index 0000000..6cd1745
--- /dev/null
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundGatewayWithChain.java
@@ -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 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 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);
+ }
+}
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionReconnection.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionReconnection.java
new file mode 100644
index 0000000..a701dda
--- /dev/null
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionReconnection.java
@@ -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
+ *
+ * - Client connects to SMSC
+ * - SMSC shuts down
+ * - SMSC restart
+ * - Client reconnect to SMSC
+ *
+ *
+ * @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");
+ }
+}
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.java
index 61d1902..0d3ac81 100644
--- a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.java
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.java
@@ -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;
+ }
+ }
}
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.java
index 0a6323a..b5a5f74 100644
--- a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.java
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.java
@@ -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;
+ }
+ }
+
}
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/DataCodingSpecificationTest.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/DataCodingSpecificationTest.java
new file mode 100644
index 0000000..a3ee709
--- /dev/null
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/DataCodingSpecificationTest.java
@@ -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));
+ }
+}
diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/UdhUtilTest.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/UdhUtilTest.java
new file mode 100644
index 0000000..cdc4db3
--- /dev/null
+++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/UdhUtilTest.java
@@ -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);
+ }
+}
diff --git a/spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapterWithChain-context.xml b/spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapterWithChain-context.xml
new file mode 100644
index 0000000..9114c26
--- /dev/null
+++ b/spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapterWithChain-context.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-smpp/src/test/resources/TestSmppOutboundGatewayWithChain-context.xml b/spring-integration-smpp/src/test/resources/TestSmppOutboundGatewayWithChain-context.xml
new file mode 100644
index 0000000..c340cd1
--- /dev/null
+++ b/spring-integration-smpp/src/test/resources/TestSmppOutboundGatewayWithChain-context.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.xml b/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.xml
index 2e38f68..9444bad 100644
--- a/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.xml
+++ b/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundChannelAdapterParserTests.xml
@@ -36,4 +36,11 @@
source-address="12345" source-ton="SUBSCRIBER_NUMBER" channel="target">
+
+
+
+
+
+
diff --git a/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.xml b/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.xml
index 1784101..2a6e919 100644
--- a/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.xml
+++ b/spring-integration-smpp/src/test/resources/org/springframework/integration/smpp/config/xml/SmppOutboundGatewayParserTests.xml
@@ -40,4 +40,15 @@
+
+
+
+
+
+
+
+
+