From f9207dae2b5a3ae34179ca3156087d5248994098 Mon Sep 17 00:00:00 2001 From: Johanes Soetanto Date: Thu, 14 Mar 2013 16:57:04 +1100 Subject: [PATCH] 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 --- spring-integration-smpp/build.gradle | 3 +- .../AbstractReceivingMessageListener.java | 13 +- .../smpp/core/DataCodingSpecification.java | 116 ++++++ .../smpp/core/SmesMessageSpecification.java | 109 +++++- .../integration/smpp/core/SmppConstants.java | 16 + .../integration/smpp/core/UdhUtil.java | 41 +++ .../smpp/outbound/SmppOutboundGateway.java | 1 + .../ExtendedSmppSessionAdaptingDelegate.java | 8 + .../smpp/session/SmppSessionFactoryBean.java | 347 ++++++++++++++---- .../xml/spring-integration-smpp-2.2.xsd | 15 +- .../integration/smpp/MockSmppServer.java | 110 +++++- ...stSmppOutboundChannelAdapterWithChain.java | 75 ++++ .../TestSmppOutboundGatewayWithChain.java | 77 ++++ .../smpp/TestSmppSessionReconnection.java | 133 +++++++ ...SmppOutboundChannelAdapterParserTests.java | 24 ++ .../xml/SmppOutboundGatewayParserTests.java | 29 ++ .../core/DataCodingSpecificationTest.java | 64 ++++ .../integration/smpp/core/UdhUtilTest.java | 81 ++++ ...utboundChannelAdapterWithChain-context.xml | 66 ++++ ...stSmppOutboundGatewayWithChain-context.xml | 73 ++++ .../SmppOutboundChannelAdapterParserTests.xml | 7 + .../xml/SmppOutboundGatewayParserTests.xml | 11 + 22 files changed, 1316 insertions(+), 103 deletions(-) create mode 100644 spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/DataCodingSpecification.java create mode 100644 spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/UdhUtil.java create mode 100644 spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundChannelAdapterWithChain.java create mode 100644 spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppOutboundGatewayWithChain.java create mode 100644 spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionReconnection.java create mode 100644 spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/DataCodingSpecificationTest.java create mode 100644 spring-integration-smpp/src/test/java/org/springframework/integration/smpp/core/UdhUtilTest.java create mode 100644 spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapterWithChain-context.xml create mode 100644 spring-integration-smpp/src/test/resources/TestSmppOutboundGatewayWithChain-context.xml 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 + * + * + * @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: + * + * + * @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 + *

    + *
  1. If {@link SmppConstants#MAXIMUM_CHARACTERS} is set, use it
  2. + *
  3. If {@link SmppConstants#DATA_CODING} is set, find maximum character for the data coding
  4. + *
  5. Using default maximum character which is 140
  6. + *
+ * @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 + *

    + *
  1. Client connects to SMSC
  2. + *
  3. SMSC shuts down
  4. + *
  5. SMSC restart
  6. + *
  7. Client reconnect to SMSC
  8. + *
+ * + * @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 @@ + + + + + + + + +