Merge pull request #37 from otnateos/INTEXT-58
* otnateos-INTEXT-58: INTEXT-58 Support SMPP Sending Using Other Charset and udh * Remove SmppSessionFactoryBean.setMessageReceiverListeners(MessageReceiverListener... listeners) to remove Spring's warning message for ambiguous method access * Allow using DeliveryReceipt id as is if hex parsing fail (tested with HSLSMS) * Add support for session reconnection which will attempt to reconnect every 5 sec by default until the session is destroyed * If additional session state listener is configured by user, it will register it * Update build dependency with Spring Integration 2.2.2 * Update javadoc * Add support for smpp transaction time using transactionTimeout * Support 'request-handler-advice-chain' for outbound channel adapter and gateway * Support sending using data_coding * Support sending using message_payload * Support for sending long sms using UDH * Update mock smpp server to simulate real SMPP error * Change message id reading logging to DEBUG instead of WARN * Wrap SMPPSession with proxy for auto reconnection * Use ExecutorService for reconnection * Fix some code and author
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.smpp.core;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* This is specification for data coding based on SMPP API and Java charset.
|
||||
*
|
||||
* @author Johanes Soetanto
|
||||
* @since 1.0
|
||||
*/
|
||||
public class DataCodingSpecification {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DataCodingSpecification.class);
|
||||
public static final String US_ASCII = "US-ASCII";
|
||||
public static final String ISO_8859_1 = "ISO-8859-1";
|
||||
public static final String ISO_8859_5 = "ISO-8859-5";
|
||||
public static final String ISO_8859_8 = "ISO-8859-8";
|
||||
public static final String UTF_16 = "UTF-16";
|
||||
public static final String UTF_8 = "UTF-8";
|
||||
public static final String EUC_KR = "EUC-KR";
|
||||
public static final String EUC_JP = "EUC-JP";
|
||||
|
||||
/**
|
||||
* Get maximum characters for data coding. Returns
|
||||
* <ul>
|
||||
* <li>160 for data coding 0/1</li>
|
||||
* <li>70 for data coding 5/8/10/13/14</li>
|
||||
* <li>140 for others</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param dataCoding data coding
|
||||
* @return maximum characters can be used for the text with specified data coding
|
||||
*/
|
||||
// reference https://www.cisco.com/en/US/docs/voice_ip_comm/connection/7x/administration/guide/7xcucsag200.pdf
|
||||
public static int getMaxCharacters(byte dataCoding) {
|
||||
switch (dataCoding) {
|
||||
case 0:case 1: return 160; // these are 7bit, return full length
|
||||
// JP and KR are suppose to use multi-byte character. This is probably needed to be tested once we get more
|
||||
// people using those charset so they can maximize their allowed number of characters in single message.
|
||||
// For now assume it is the same one as double byte characters
|
||||
case 5:case 10:case 13: // just to be safe for japanese
|
||||
case 8:
|
||||
case 14: // just to be safe for korean
|
||||
return 70;
|
||||
default: return 140;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get charset name based on data coding. Returns:
|
||||
* <ul>
|
||||
* <li>US-ASCII for data coding 1</li>
|
||||
* <li>ISO-8859-1 for data coding 3</li>
|
||||
* <li>ISO-8859-5 for data coding 6</li>
|
||||
* <li>ISO-8859-8 for data coding 7</li>
|
||||
* <li>UTF-16 for data coding 8</li>
|
||||
* <li>EUC-KR for data coding 14</li>
|
||||
* <li>EUC-JP for data coding 5/10/13</li>
|
||||
* <li>UTF-8 for others</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param dataCoding data coding
|
||||
* @return charset name related to the data coding
|
||||
*/
|
||||
public static String getCharsetName(byte dataCoding) {
|
||||
switch (dataCoding) {
|
||||
case 1: return US_ASCII;
|
||||
case 3: return ISO_8859_1;
|
||||
case 6: return ISO_8859_5;
|
||||
case 7: return ISO_8859_8;
|
||||
case 8: return UTF_16;
|
||||
case 14: return EUC_KR;
|
||||
case 5: case 10: case 13: return EUC_JP;
|
||||
case 2: case 4: // since both 2 and 4 is unspecified binary, use UTF-8 encoding
|
||||
case 0: // since dataCoding 0 is gsm 7bit, it is quite safe to use UTF-8
|
||||
default: return UTF_8;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message in bytes. This will use {@link #getCharsetName(byte)} to get the message in bytes.
|
||||
* @param message short message
|
||||
* @param dataCoding data coding
|
||||
* @return message in bytes based on the data coding
|
||||
*/
|
||||
public static byte[] getMessageInBytes(String message, byte dataCoding) {
|
||||
final String charsetName = getCharsetName(dataCoding);
|
||||
if (!charsetName.equals(UTF_8)) {
|
||||
try {
|
||||
return message.getBytes(charsetName);
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
log.warn("Fail to encode message using charset '{}'", charsetName);
|
||||
}
|
||||
}
|
||||
return message.getBytes();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ public class SmesMessageSpecification {
|
||||
private byte smDefaultMsgId;
|
||||
private byte[] shortMessage;
|
||||
private ClientSession smppSession;
|
||||
private OptionalParameter messagePayloadParameter;
|
||||
|
||||
/**
|
||||
* this method takes an inbound SMS message and converts it to a Spring Integration message
|
||||
@@ -133,15 +134,26 @@ public class SmesMessageSpecification {
|
||||
smsTxt = (String) payload;
|
||||
}
|
||||
}
|
||||
SmesMessageSpecification spec = SmesMessageSpecification.newSmesMessageSpecification(smppSession, srcAddy, dstAddy, smsTxt);
|
||||
final DataCoding dataCodingFromHeader = SmesMessageSpecification.dataCodingFromHeader(msg);
|
||||
final SmesMessageSpecification spec = new SmesMessageSpecification()
|
||||
.reset()
|
||||
.setSmppSession(smppSession)
|
||||
.setSourceAddress(srcAddy)
|
||||
.setDestinationAddress(dstAddy)
|
||||
.setDataCoding(dataCodingFromHeader);
|
||||
spec.setMaxLengthSmsMessages(maximumCharactersFromHeader(msg));
|
||||
spec.setEsmClass(SmesMessageSpecification.esmClassFromHeader(msg));
|
||||
if (msg.getHeaders().containsKey(SmppConstants.USE_MSG_PAYLOAD_PARAM)) {
|
||||
spec.setShortMessageUsingPayload(smsTxt);
|
||||
} else {
|
||||
spec.setShortTextMessage(smsTxt);
|
||||
}
|
||||
spec.setDestinationAddressNumberingPlanIndicator(SmesMessageSpecification.<NumberingPlanIndicator>valueIfHeaderExists(DST_NPI, msg));
|
||||
spec.setSourceAddressNumberingPlanIndicator(SmesMessageSpecification.<NumberingPlanIndicator>valueIfHeaderExists(SRC_NPI, msg));
|
||||
spec.setDestinationAddressTypeOfNumber(SmesMessageSpecification.<TypeOfNumber>valueIfHeaderExists(DST_TON, msg));
|
||||
spec.setSourceAddressTypeOfNumber(SmesMessageSpecification.<TypeOfNumber>valueIfHeaderExists(SRC_TON, msg));
|
||||
spec.setServiceType(SmesMessageSpecification.<String>valueIfHeaderExists(SERVICE_TYPE, msg));
|
||||
spec.setEsmClass(SmesMessageSpecification.esmClassFromHeader(msg));
|
||||
spec.setScheduleDeliveryTime(SmesMessageSpecification.<Date>valueIfHeaderExists(SCHEDULED_DELIVERY_TIME, msg));
|
||||
spec.setDataCoding(SmesMessageSpecification. dataCodingFromHeader( msg));
|
||||
spec.setValidityPeriod(SmesMessageSpecification.<String>valueIfHeaderExists(VALIDITY_PERIOD, msg));
|
||||
|
||||
// byte landmine. autoboxing causes havoc with <em>null</em> bytes.
|
||||
@@ -178,6 +190,33 @@ public class SmesMessageSpecification {
|
||||
return null ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting maximum characters from header. This will allow checking maximum character based on
|
||||
* {@link SmppConstants#MAXIMUM_CHARACTERS} header or determine the maximum character based on data coding
|
||||
* header {@link SmppConstants#DATA_CODING}.
|
||||
* <p/>
|
||||
* The order of the selection is
|
||||
* <ol>
|
||||
* <li>If {@link SmppConstants#MAXIMUM_CHARACTERS} is set, use it</li>
|
||||
* <li>If {@link SmppConstants#DATA_CODING} is set, find maximum character for the data coding</li>
|
||||
* <li>Using default maximum character which is 140</li>
|
||||
* </ol>
|
||||
* @param msg the Spring Integration message
|
||||
* @return maximum character can be sent through the session
|
||||
*/
|
||||
private static int maximumCharactersFromHeader(Message<?> msg) {
|
||||
if (msg.getHeaders().containsKey(MAXIMUM_CHARACTERS))
|
||||
return msg.getHeaders().get(MAXIMUM_CHARACTERS, Integer.class);
|
||||
if (msg.getHeaders().containsKey(DATA_CODING)) {
|
||||
final Object dc = msg.getHeaders().get(DATA_CODING);
|
||||
if (dc instanceof Byte)
|
||||
return DataCodingSpecification.getMaxCharacters((Byte)dc);
|
||||
else
|
||||
return DataCodingSpecification.getMaxCharacters(((DataCoding)dc).toByte());
|
||||
}
|
||||
return 140;
|
||||
}
|
||||
|
||||
/**
|
||||
* need to be a little flexibile about what we take in as {@link SmppConstants#REGISTERED_DELIVERY_MODE}. The value can
|
||||
* be a String or a member of the {@link SMSCDeliveryReceipt} enum.
|
||||
@@ -306,7 +345,9 @@ public class SmesMessageSpecification {
|
||||
*/
|
||||
public String send() throws Exception {
|
||||
validate();
|
||||
String msgId = this.smppSession.submitShortMessage(
|
||||
final String msgId;
|
||||
if (messagePayloadParameter == null) {
|
||||
msgId = this.smppSession.submitShortMessage(
|
||||
this.serviceType,
|
||||
this.sourceAddressTypeOfNumber,
|
||||
this.sourceAddressNumberingPlanIndicator,
|
||||
@@ -326,6 +367,32 @@ public class SmesMessageSpecification {
|
||||
this.dataCoding,
|
||||
this.smDefaultMsgId,
|
||||
this.shortMessage);
|
||||
} else {
|
||||
// SPEC 3.2.3
|
||||
log.debug("Sending message using message_payload");
|
||||
msgId = this.smppSession.submitShortMessage(
|
||||
this.serviceType,
|
||||
this.sourceAddressTypeOfNumber,
|
||||
this.sourceAddressNumberingPlanIndicator,
|
||||
this.sourceAddress,
|
||||
|
||||
this.destinationAddressTypeOfNumber,
|
||||
this.destinationAddressNumberingPlanIndicator,
|
||||
this.destinationAddress,
|
||||
|
||||
this.esmClass,
|
||||
this.protocolId,
|
||||
this.priorityFlag,
|
||||
this.scheduleDeliveryTime,
|
||||
this.validityPeriod,
|
||||
this.registeredDelivery,
|
||||
this.replaceIfPresentFlag,
|
||||
this.dataCoding,
|
||||
this.smDefaultMsgId,
|
||||
new byte[0],
|
||||
this.messagePayloadParameter
|
||||
);
|
||||
}
|
||||
|
||||
return msgId;
|
||||
}
|
||||
@@ -333,7 +400,11 @@ public class SmesMessageSpecification {
|
||||
protected void validate() {
|
||||
Assert.notNull(this.sourceAddress, "the source address must not be null");
|
||||
Assert.notNull(this.destinationAddress, "the destination address must not be null");
|
||||
Assert.isTrue(this.shortMessage != null && this.shortMessage.length > 0, "the message must not be null");
|
||||
final boolean shortMessageSet = this.shortMessage != null && this.shortMessage.length > 0;
|
||||
Assert.isTrue(messagePayloadParameter != null ^ shortMessageSet,
|
||||
"message can only be set in payload or short message. cannot be both");
|
||||
if (messagePayloadParameter == null)
|
||||
Assert.isTrue(shortMessageSet, "the message must not be null");
|
||||
}
|
||||
|
||||
public SmesMessageSpecification setSourceAddress(String sourceAddr) {
|
||||
@@ -475,19 +546,38 @@ public class SmesMessageSpecification {
|
||||
}
|
||||
|
||||
/**
|
||||
* todo it'running not <em>quite</em> true that the payload needs to be 140c. A large message can be split up into smaller messages,
|
||||
* but for now it'running more useful to have this validation in place than not.
|
||||
* Setting short message. This will take into account if {@link #dataCoding} or if {@link #maxLengthSmsMessages}
|
||||
* is set through header to validate the maximum characters can be set.
|
||||
*
|
||||
* @param s the text message body
|
||||
* @return the SmesMessageSpecification
|
||||
*/
|
||||
public SmesMessageSpecification setShortTextMessage(String s) {
|
||||
Assert.notNull(s, "the SMS message payload must not be null");
|
||||
Assert.isTrue(s.length() <= this.maxLengthSmsMessages, "the SMS message payload must be 140 characters or less.");
|
||||
this.shortMessage = s.getBytes();
|
||||
if (esmClass != null && GSMSpecificFeature.UDHI.containedIn(esmClass)) {
|
||||
log.debug("Setting short message with UDH");
|
||||
this.shortMessage = UdhUtil.getMessageWithUdhInBytes(s, dataCoding.toByte());
|
||||
} else {
|
||||
Assert.isTrue(s.length() <= this.maxLengthSmsMessages,
|
||||
"the SMS message payload must be " + maxLengthSmsMessages + " characters or less.");
|
||||
this.shortMessage = DataCodingSpecification.getMessageInBytes(s, dataCoding.toByte());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting short message using message_payload ({@link org.jsmpp.bean.OptionalParameter.Tag#MESSAGE_PAYLOAD})
|
||||
* optional parameter
|
||||
* @param s the text messages body
|
||||
* @return the SmesMessageSpecification
|
||||
*/
|
||||
public SmesMessageSpecification setShortMessageUsingPayload(String s) {
|
||||
final byte[] content = DataCodingSpecification.getMessageInBytes(s, dataCoding.toByte());
|
||||
this.messagePayloadParameter =
|
||||
new OptionalParameter.OctetString(OptionalParameter.Tag.MESSAGE_PAYLOAD.code(), content);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* this is a good value, but not strictly speaking universal. This is intended only for exceptional configuration cases
|
||||
* <p/>
|
||||
@@ -531,6 +621,7 @@ public class SmesMessageSpecification {
|
||||
smDefaultMsgId = 0;
|
||||
shortMessage = null; // the bytes to the 140 character text message
|
||||
smppSession = null;
|
||||
messagePayloadParameter = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,4 +63,20 @@ public abstract class SmppConstants {
|
||||
public static final String REPLY_PATH = "REPLY_PATH";
|
||||
public static final String DEST_ADDRESS = DST_ADDR;
|
||||
public static final String OPTIONAL_PARAMETERS = "OPTIONAL_PARAMETERS";
|
||||
/** Additional support header to allow user to customise the maximum characters can be sent. Unless this header
|
||||
* is set, the default is 140 characters or if {@link #DATA_CODING} header is set, the maximum character will
|
||||
* be based on {@link DataCodingSpecification#getMaxCharacters(byte)}. Setting this header manually may have
|
||||
* unintended consequences.
|
||||
*/
|
||||
public static final String MAXIMUM_CHARACTERS = "MAXIMUM_CHARACTERS";
|
||||
/** Additional support header to send the sms using message_payload instead of setting using short_message.
|
||||
* This can be used when we need to send long sms. (SPEC 3.2.3).
|
||||
* <p/>
|
||||
* Note:
|
||||
* <ul>
|
||||
* <li>That not many SMSC may support payload</li>
|
||||
* <li>The actual short message length which can be transmitted may vary according to the underlying network</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final String USE_MSG_PAYLOAD_PARAM = "USE_MSG_PAYLOAD_PARAM";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.jsmpp.SynchronizedPDUSender;
|
||||
import org.jsmpp.bean.BindType;
|
||||
import org.jsmpp.bean.NumberingPlanIndicator;
|
||||
import org.jsmpp.bean.TypeOfNumber;
|
||||
import org.jsmpp.extra.SessionState;
|
||||
import org.jsmpp.session.MessageReceiverListener;
|
||||
import org.jsmpp.session.SMPPSession;
|
||||
import org.jsmpp.session.SessionStateListener;
|
||||
@@ -29,6 +30,10 @@ import org.jsmpp.session.connection.Connection;
|
||||
import org.jsmpp.session.connection.ConnectionFactory;
|
||||
import org.jsmpp.session.connection.socket.SocketConnection;
|
||||
import org.jsmpp.util.DefaultComposer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.aop.framework.ProxyFactoryBean;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.Lifecycle;
|
||||
@@ -40,9 +45,10 @@ import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Factory bean to create a {@link SMPPSession}. Usually, you need little more than the {@link #host},
|
||||
@@ -52,33 +58,37 @@ import java.util.Set;
|
||||
* <p/>
|
||||
* Here is a breakdown of the supported parameters on this factory bean:
|
||||
* <p/>
|
||||
* host the SMSC host to which the session is bound (think of this as the host of your email server)
|
||||
* port the SMSC port to which the session is bound (think of this as a port on your email server)
|
||||
* bindType values of type {@link org.jsmpp.bean.BindType}. the bind type specifies whether this {@link SMPPSession} can send ({@link org.jsmpp.bean.BindType#BIND_TX}), receive ({@link org.jsmpp.bean.BindType#BIND_RX}), or both send and receive ({@link org.jsmpp.bean.BindType#BIND_TRX}).
|
||||
* systemId the system ID for the server being bound to
|
||||
* password the password for the server being bound to
|
||||
* systemType the SMSC system type
|
||||
* addrTon a value from the {@link org.jsmpp.bean.TypeOfNumber} enumeration. default is {@link org.jsmpp.bean.TypeOfNumber#UNKNOWN}
|
||||
* addrNpi a value from the {@link org.jsmpp.bean.NumberingPlanIndicator} enumeration. Default is {@link org.jsmpp.bean.NumberingPlanIndicator#UNKNOWN}
|
||||
* addressRange can be null. Specifies the address range.
|
||||
* timeout a good default value is 60000 (1 minute)
|
||||
* <ul>
|
||||
* <li>host - the SMSC host to which the session is bound (think of this as the host of your email server)</li>
|
||||
* <li>port - the SMSC port to which the session is bound (think of this as a port on your email server)</li>
|
||||
* <li>bindType - values of type {@link org.jsmpp.bean.BindType}. The bind type specifies whether this
|
||||
* {@link SMPPSession} can send ({@link org.jsmpp.bean.BindType#BIND_TX}),
|
||||
* receive ({@link org.jsmpp.bean.BindType#BIND_RX}), or both send and receive
|
||||
* ({@link org.jsmpp.bean.BindType#BIND_TRX}).</li>
|
||||
* <li>systemId - the system ID for the server being bound to</li>
|
||||
* <li>password - the password for the server being bound to</li>
|
||||
* <li>systemType - the SMSC system type</li>
|
||||
* <li>addrTon - a value from the {@link org.jsmpp.bean.TypeOfNumber} enumeration. Default is
|
||||
* {@link org.jsmpp.bean.TypeOfNumber#UNKNOWN}</li>
|
||||
* <li>addrNpi - a value from the {@link org.jsmpp.bean.NumberingPlanIndicator} enumeration.
|
||||
* Default is {@link org.jsmpp.bean.NumberingPlanIndicator#UNKNOWN}</li>
|
||||
* <li>addressRange - can be null. Specifies the address range.</li>
|
||||
* <li>timeout - a good default value is 60000 (1 minute)</li>
|
||||
* <li>transactionTimeout - timeout for doing work with session. e.g. sending message (default 2 seconds)</li>
|
||||
* <li>reconnect - boolean whether we allow the session to reconnect. (default true)</li>
|
||||
* <li>reconnectInterval - interval between reconnection in milliseconds. (default 5 seconds)</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* @author Josh Long
|
||||
* <p/>
|
||||
* todo support a proxied SMPPSession that automatically recovers from disconnects a la the examples {@link org.jsmpp.examples.gateway.AutoReconnectGateway}
|
||||
* @see org.jsmpp.session.SMPPSession#SMPPSession()
|
||||
* @see org.jsmpp.session.SMPPSession#connectAndBind(String, int, org.jsmpp.session.BindParameter)
|
||||
* @see org.jsmpp.session.SMPPSession#connectAndBind(String, int, org.jsmpp.bean.BindType, String, String, String, org.jsmpp.bean.TypeOfNumber, org.jsmpp.bean.NumberingPlanIndicator, String, long)
|
||||
* @since 1.0
|
||||
*/
|
||||
public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>, SmartLifecycle, InitializingBean {
|
||||
public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>, SmartLifecycle, InitializingBean,
|
||||
DisposableBean {
|
||||
|
||||
/**
|
||||
* impl of {@link Lifecycle} that connects and disconnects respectively in
|
||||
* {@link org.springframework.context.Lifecycle#start()} and {@link org.springframework.context.Lifecycle#stop()}
|
||||
*
|
||||
* @author Josh Long
|
||||
*/
|
||||
private Set<MessageReceiverListener> messageReceiverListeners = new HashSet<MessageReceiverListener>();
|
||||
private boolean autoStartup;
|
||||
private volatile boolean running;
|
||||
@@ -88,6 +98,7 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
private String host = "127.0.0.1";
|
||||
private String addressRange;
|
||||
private long timeout = 60 * 1000;// 1 minute
|
||||
private long transactionTimeout = 2 * 1000; // 2 seconds
|
||||
private int port = 2775; // good default though this has been known to change
|
||||
private BindType bindType = BindType.BIND_TRX; // bind as a 'transceiver' - only 3.4 of the spec <em>requires</em> support for this
|
||||
private String systemId = getClass().getSimpleName().toLowerCase(); // what would typically be called 'user' in a user/pw scheme
|
||||
@@ -95,8 +106,15 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
private String systemType = "cp";
|
||||
private TypeOfNumber addrTon = TypeOfNumber.UNKNOWN;
|
||||
private NumberingPlanIndicator addrNpi = NumberingPlanIndicator.UNKNOWN;
|
||||
private long reconnectInterval = 5 * 1000; // 5 seconds
|
||||
private boolean reconnect = true; // flag whether we want to reconnect
|
||||
private volatile boolean destroyed = false; // flag that this session factory has been disposed
|
||||
|
||||
private ExtendedSmppSessionAdaptingDelegate product;
|
||||
private final ProxyFactoryBean sessionFactoryBean = new ProxyFactoryBean();
|
||||
|
||||
private ExecutorService reconnectingExecutor;
|
||||
private boolean reconnectingExecutorSet;
|
||||
|
||||
public void setSsl(boolean ssl) {
|
||||
this.ssl = ssl;
|
||||
@@ -145,39 +163,82 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
this.addressRange = addressRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting timeout for the session. This value is used to establish connection to SMSC, e.g. trying to establish
|
||||
* connection. (default is 1 minute). This should not be confused with {@link #setTransactionTimeout(long)} which
|
||||
* is the timeout to perform request on the actual session after it has been established.
|
||||
*
|
||||
* @param timeout timeout in milliseconds
|
||||
*/
|
||||
public void setTimeout(long timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public void setSessionStateListener(SessionStateListener sessionStateListener) {
|
||||
this.sessionStateListener = sessionStateListener;
|
||||
}
|
||||
/**
|
||||
* Setting transaction timeout preforming request on the session. ({@link SMPPSession#setTransactionTimer(long)}.
|
||||
* This transaction timeout is similar to the concept of send timeout / request timeout. (default 2 seconds).
|
||||
* If you receive a lot of {@link org.jsmpp.extra.ResponseTimeoutException} for waiting response from your SMSC,
|
||||
* this indicates you need to increase this value.
|
||||
*
|
||||
* @param transactionTimeout transaction timeout in milliseconds
|
||||
*/
|
||||
public void setTransactionTimeout(long transactionTimeout) {
|
||||
this.transactionTimeout = transactionTimeout;
|
||||
}
|
||||
|
||||
public void setMessageReceiverListeners(MessageReceiverListener... listeners) {
|
||||
setMessageReceiverListeners(new HashSet<MessageReceiverListener>(Arrays.asList(listeners)));
|
||||
public void setSessionStateListener(SessionStateListener sessionStateListener) {
|
||||
this.sessionStateListener = sessionStateListener;
|
||||
}
|
||||
|
||||
public void setMessageReceiverListeners(Set<MessageReceiverListener> messageReceiverListeners) {
|
||||
this.messageReceiverListeners = messageReceiverListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating new SMPPSession. This will create default SMPPSession for non-SSL connection or create SMPPSession
|
||||
* using different factory for SSL connection.
|
||||
* @return SMPP session
|
||||
*/
|
||||
private SMPPSession createNewSession() {
|
||||
final SMPPSession newSession;
|
||||
if (!ssl) {
|
||||
newSession = new SMPPSession();
|
||||
} else {
|
||||
newSession = new SMPPSession(new SynchronizedPDUSender(new DefaultPDUSender(
|
||||
new DefaultComposer())), new DefaultPDUReader(), sslConnectionFactory);
|
||||
}
|
||||
newSession.setTransactionTimer(transactionTimeout);
|
||||
return newSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic to build smpp session
|
||||
* @return the configured SMPPSession
|
||||
* @throws Exception should anything go wrong
|
||||
*/
|
||||
private ExtendedSmppSessionAdaptingDelegate buildSmppSession() throws Exception {
|
||||
SMPPSession smppSession = null;
|
||||
if (!ssl) {
|
||||
smppSession = new SMPPSession();
|
||||
} else {
|
||||
smppSession = new SMPPSession(new SynchronizedPDUSender(new DefaultPDUSender(new DefaultComposer())), new DefaultPDUReader(), sslConnectionFactory);
|
||||
}
|
||||
final SMPPSession smppSession = createNewSession();
|
||||
final ExtendedSmppSessionAdaptingDelegate extendedSmppSessionAdaptingDelegate;
|
||||
if (reconnect) {
|
||||
sessionFactoryBean.setAutodetectInterfaces(false);
|
||||
sessionFactoryBean.setTarget(smppSession);
|
||||
final SMPPSession proxiedSession = (SMPPSession)sessionFactoryBean.getObject();
|
||||
|
||||
ExtendedSmppSessionAdaptingDelegate extendedSmppSessionAdaptingDelegate = new ExtendedSmppSessionAdaptingDelegate(smppSession, new ConnectingLifecycle(smppSession));
|
||||
extendedSmppSessionAdaptingDelegate = new ExtendedSmppSessionAdaptingDelegate(
|
||||
proxiedSession, new AutoReconnectLifecycle(proxiedSession));
|
||||
} else {
|
||||
extendedSmppSessionAdaptingDelegate = new ExtendedSmppSessionAdaptingDelegate(
|
||||
smppSession, new ConnectingLifecycle(smppSession));
|
||||
}
|
||||
|
||||
for (MessageReceiverListener mrl : this.messageReceiverListeners)
|
||||
extendedSmppSessionAdaptingDelegate.addMessageReceiverListener(mrl);
|
||||
|
||||
// if session state listener not null, add it
|
||||
if (sessionStateListener != null) {
|
||||
extendedSmppSessionAdaptingDelegate.addSessionStateListener(sessionStateListener);
|
||||
}
|
||||
|
||||
extendedSmppSessionAdaptingDelegate.setBindType(this.bindType);
|
||||
return extendedSmppSessionAdaptingDelegate;
|
||||
}
|
||||
@@ -196,14 +257,10 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void stop(Runnable callback) {
|
||||
try {
|
||||
log.debug("shutting down in " + getClass().getName() + "#stop(Runnable).");
|
||||
callback.run();
|
||||
} catch (Throwable throwable) {
|
||||
log.warn("error when trying to shutdown " + getClass().getName() + ", could not invoke the callback's Runnable#run method");
|
||||
}
|
||||
this.stop();
|
||||
callback.run();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,6 +268,10 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
*/
|
||||
public void start() {
|
||||
log.debug("starting up in " + getClass().getName() + "#start().");
|
||||
if (reconnectingExecutor == null) {
|
||||
this.reconnectingExecutor = Executors.newFixedThreadPool(1);
|
||||
}
|
||||
|
||||
( product).start();
|
||||
this.running = true;
|
||||
}
|
||||
@@ -221,6 +282,12 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
public void stop() {
|
||||
log.debug("shutting down in " + getClass().getName() + "#stop().");
|
||||
( product).stop();
|
||||
|
||||
// if we are running default executor, shut it down
|
||||
if (!reconnectingExecutorSet && reconnectingExecutor != null) {
|
||||
reconnectingExecutor.shutdown();
|
||||
this.reconnectingExecutor = null;
|
||||
}
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
@@ -261,7 +328,34 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Set whether we want to reconnect the session. Default is true.
|
||||
*
|
||||
* @param reconnect true/false
|
||||
*/
|
||||
public void setReconnect(boolean reconnect) {
|
||||
this.reconnect = reconnect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set session reconnection interval. Default is 5 seconds.
|
||||
*
|
||||
* @param reconnectInterval reconnection interval in milliseconds
|
||||
*/
|
||||
public void setReconnectInterval(long reconnectInterval) {
|
||||
this.reconnectInterval = reconnectInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set executor service for performing SMPP reconnection.
|
||||
* @param reconnectingExecutor executor service
|
||||
*/
|
||||
public void setReconnectingExecutor(ExecutorService reconnectingExecutor) {
|
||||
this.reconnectingExecutor = reconnectingExecutor;
|
||||
this.reconnectingExecutorSet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
@@ -276,7 +370,15 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
this.product = buildSmppSession();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* singleton {@link ConnectionFactory} that handles SSL
|
||||
*/
|
||||
final private static ConnectionFactory sslConnectionFactory = new ConnectionFactory() {
|
||||
@@ -311,12 +413,12 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
if (session.getSessionState().isBound()) {
|
||||
try {
|
||||
session.unbindAndClose();
|
||||
} catch (Throwable t) {
|
||||
log.warn("couldn't close and unbind the session", t);
|
||||
} catch (Exception t) {
|
||||
log.warn("Couldn't close and unbind the session", t);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("the smppSession given to close is null");
|
||||
log.warn("The smppSession given to close is null");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,46 +427,149 @@ public class SmppSessionFactoryBean implements FactoryBean<ExtendedSmppSession>,
|
||||
session.connectAndBind(host, port, bindType, systemId, password, systemType, addrTon, addrNpi, addressRange, timeout);
|
||||
this.running = true;
|
||||
} catch (IOException e) {
|
||||
log.error("something happened when trying to connect", e);
|
||||
if (log.isDebugEnabled())
|
||||
log.error("Error happened when trying to connect to " + host + ":" + port, e);
|
||||
else
|
||||
log.error("Error happened when trying to connect to " + host + ":" + port + ". Cause: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* private void reconnectAfter(final long timeInMillis) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("Schedule reconnect after " + timeInMillis + " millis");
|
||||
try {
|
||||
Thread.sleep(timeInMillis);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
/**
|
||||
* Lifecycle implementation that will try to re-establish connection with specific interval. At the start of the
|
||||
* connection.
|
||||
*
|
||||
* @author Johanes Soetanto
|
||||
*/
|
||||
private class AutoReconnectLifecycle implements Lifecycle {
|
||||
|
||||
int attempt = 0;
|
||||
while (session == null || session.getSessionState().equals(SessionState.CLOSED)) {
|
||||
private final Logger log = LoggerFactory.getLogger(AutoReconnectLifecycle.class);
|
||||
private final SMPPSession session;
|
||||
private volatile boolean running;
|
||||
|
||||
/**
|
||||
* Creating auto reconnect lifecycle using SMPP session and reconnect interval in milliseconds
|
||||
* @param smppSession reference to SMPP session
|
||||
*/
|
||||
private AutoReconnectLifecycle(SMPPSession smppSession) {
|
||||
this.session = smppSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (session != null) {
|
||||
if (session.getSessionState().isBound()) {
|
||||
try {
|
||||
logger.info("Reconnecting attempt #" + (++attempt) + "...");
|
||||
session = newSession();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed opening connection and bind to " + remoteIpAddress + ":" + remotePort, e);
|
||||
// wait for a second
|
||||
try { Thread.sleep(1000); } catch (InterruptedException ee) {}
|
||||
session.unbindAndClose();
|
||||
} catch (Exception t) {
|
||||
log.warn("Couldn't close and unbind the session", t);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("The smppSession given to close is null");
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
connect();
|
||||
|
||||
private class SessionStateListenerImpl implements SessionStateListener {
|
||||
public void onStateChange(SessionState newState, SessionState oldState,
|
||||
Object source) {
|
||||
if (newState.equals(SessionState.CLOSED)) {
|
||||
logger.info("Session closed");
|
||||
reconnectAfter(reconnectInterval);
|
||||
if (!running) {
|
||||
log.debug("Try to connect at later time. The delay is {}ms", reconnectInterval);
|
||||
scheduleReconnect();
|
||||
} else {
|
||||
registerSessionCloseListener();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register session state listener to reconnect when session is closed by server.
|
||||
*/
|
||||
private void registerSessionCloseListener() {
|
||||
log.debug("Registering session close listener");
|
||||
session.addSessionStateListener(new SessionStateListener() {
|
||||
@Override
|
||||
public void onStateChange(SessionState newState, SessionState oldState, Object source) {
|
||||
// when session is closed but client session has not been destroyed can indicates client
|
||||
// lose connection to server
|
||||
if (newState.equals(SessionState.CLOSED)) {
|
||||
running = false;
|
||||
if (!destroyed) {
|
||||
log.info("Session to {}:{} has been closed. Try to reconnect later", host, port);
|
||||
|
||||
final SMPPSession newSession = createNewSession();
|
||||
newSession.setMessageReceiverListener(product.getDelegateMessageListener());
|
||||
if (sessionStateListener != null) {
|
||||
session.addSessionStateListener(sessionStateListener);
|
||||
}
|
||||
sessionFactoryBean.setTarget(newSession);
|
||||
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform connection logic.
|
||||
*/
|
||||
private void connect() {
|
||||
try {
|
||||
session.connectAndBind(host, port, bindType, systemId, password, systemType,
|
||||
addrTon, addrNpi, addressRange, timeout);
|
||||
this.running = true;
|
||||
|
||||
} catch (IOException e) {
|
||||
if (log.isDebugEnabled())
|
||||
log.error("Error happened when trying to connect to " + host + ":" + port, e);
|
||||
else
|
||||
log.error("Error happened when trying to connect to {}:{}. Cause: {}",
|
||||
new Object[]{host, port, e.getMessage()});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a session reconnection.
|
||||
*/
|
||||
private void scheduleReconnect() {
|
||||
|
||||
reconnectingExecutor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(reconnectInterval);
|
||||
|
||||
int attempt = 0;
|
||||
// if this session is still not run and the session has not been destroyed, re-connect
|
||||
while (!running && !destroyed) {
|
||||
log.info("Reconnecting attempt #{} ...", ++attempt);
|
||||
connect();
|
||||
|
||||
if (!running) {
|
||||
// if still not running, then perform another sleep
|
||||
Thread.sleep(reconnectInterval);
|
||||
}
|
||||
}
|
||||
|
||||
if (running) {
|
||||
log.info("Successfully reconnect at attempt #{}", attempt);
|
||||
// if finish re-connection loop and session is run we register session close listener
|
||||
registerSessionCloseListener();
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
log.info("Interrupted when trying to connect to {}:{}", host, port);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
@@ -66,6 +66,9 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="smppGatewayType">
|
||||
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
|
||||
</xsd:choice>
|
||||
<xsd:attribute name="error-channel" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
@@ -106,8 +109,10 @@
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:complexType>
|
||||
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||
<xsd:choice minOccurs="0" maxOccurs="2">
|
||||
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
|
||||
<xsd:element name="request-handler-advice-chain"
|
||||
type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="coreSmppComponentAttributes"/>
|
||||
<xsd:attribute name="channel" type="xsd:string">
|
||||
@@ -166,6 +171,11 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="smppGatewayType">
|
||||
<xsd:choice minOccurs="0" maxOccurs="2">
|
||||
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
|
||||
<xsd:element name="request-handler-advice-chain"
|
||||
type="integration:adviceChainType" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:choice>
|
||||
<xsd:attribute name="time-formatter">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
@@ -378,9 +388,6 @@
|
||||
Defines common configuration for gateway adapters.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||
<xsd:element minOccurs="0" maxOccurs="1" ref="session"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="coreSmppComponentAttributes"/>
|
||||
<xsd:attribute name="reply-channel" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
|
||||
@@ -31,6 +31,8 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -46,11 +48,27 @@ import java.util.concurrent.TimeoutException;
|
||||
* @since 1.0
|
||||
*/
|
||||
public class MockSmppServer extends ServerResponseDeliveryAdapter implements Runnable, ServerMessageReceiverListener {
|
||||
|
||||
/** Agreement to make it easy to test some functionality */
|
||||
public static final class Agreement {
|
||||
/** Agreement to throw error when destination set to this value */
|
||||
public static final String THROW_NO_DESTINATION_EXCEPTION = "NoRouteDestination";
|
||||
/** Agreement to delay processing of sms message. Useful to simulate slow connection */
|
||||
public static final String DELAY_PROCESSING = "DelayMe";
|
||||
/** Agreement to delay sending back delivery receipt. Useful for testing Delivery Receipt */
|
||||
public static final String DELAY_DELIVERY_RECEIPT = "DelayDeliveryReceipt";
|
||||
}
|
||||
private static int messageDelay = 3000;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MockSmppServer.class);
|
||||
private String systemId;
|
||||
private String password;
|
||||
private int port;
|
||||
private Map<SMPPServerSession,String> connectionSessionMap = new HashMap<SMPPServerSession,String>();
|
||||
private boolean run = true;
|
||||
private int acceptConnectionTimeout = 5000;
|
||||
private SMPPServerSessionListener sessionListener;
|
||||
private boolean initServerAtStart = true;
|
||||
|
||||
private final ExecutorService execService = Executors.newFixedThreadPool(5);
|
||||
private final ExecutorService execServiceDelReceipt = Executors.newFixedThreadPool(100);
|
||||
@@ -64,14 +82,24 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
SMPPServerSessionListener sessionListener = new SMPPServerSessionListener(port);
|
||||
this.sessionListener = new SMPPServerSessionListener(port);
|
||||
sessionListener.setTimeout(acceptConnectionTimeout);
|
||||
|
||||
logger.info("Listening on port {}", port);
|
||||
while (true) {
|
||||
SMPPServerSession serverSession = sessionListener.accept();
|
||||
logger.info("Accepting connection for session {}", serverSession.getSessionId());
|
||||
serverSession.setMessageReceiverListener(this);
|
||||
serverSession.setResponseDeliveryListener(this);
|
||||
execService.execute(new WaitBindTask(serverSession, systemId, password, connectionSessionMap));
|
||||
while (run) {
|
||||
try {
|
||||
SMPPServerSession serverSession = sessionListener.accept();
|
||||
logger.info("Accepting connection for session {}", serverSession.getSessionId());
|
||||
serverSession.setMessageReceiverListener(this);
|
||||
serverSession.setResponseDeliveryListener(this);
|
||||
execService.execute(new WaitBindTask(serverSession, systemId, password, connectionSessionMap));
|
||||
}
|
||||
catch (SocketTimeoutException ste) {
|
||||
logger.info("SocketTimeoutException: {}", ste.getMessage());
|
||||
}
|
||||
catch (SocketException se) {
|
||||
logger.info("SocketException: {}", se.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
@@ -79,14 +107,44 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcing the server to stop.
|
||||
* @throws InterruptedException
|
||||
* @throws IOException
|
||||
*/
|
||||
public void stop() throws InterruptedException, IOException {
|
||||
run = false;
|
||||
execService.shutdown();
|
||||
sessionListener.close();
|
||||
for (SMPPServerSession server : connectionSessionMap.keySet()) {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
||||
public QuerySmResult onAcceptQuerySm(QuerySm querySm,
|
||||
SMPPServerSession source) throws ProcessRequestException {
|
||||
logger.info("Accepting query sm, but not implemented");
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Perform special handling just to simulate something on the SMSC */
|
||||
private void onSpecialHandling(SubmitSm submitSm,
|
||||
SMPPServerSession source) throws ProcessRequestException {
|
||||
if (submitSm.getDestAddress().equals(Agreement.THROW_NO_DESTINATION_EXCEPTION)) {
|
||||
throw new ProcessRequestException ("Invalid Dest Addr", 0x0B);
|
||||
}
|
||||
if (new String(submitSm.getShortMessage()).equals(Agreement.DELAY_PROCESSING)) {
|
||||
try {
|
||||
logger.debug("Delaying handling for {} ms", messageDelay);
|
||||
Thread.sleep(3000);
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public MessageId onAcceptSubmitSm(SubmitSm submitSm,
|
||||
SMPPServerSession source) throws ProcessRequestException {
|
||||
onSpecialHandling(submitSm, source);
|
||||
|
||||
MessageId messageId = messageIDGenerator.newMessageId();
|
||||
logger.debug("Receiving submit_sm '{}', and will return message id {}",
|
||||
new String(submitSm.getShortMessage()), messageId);
|
||||
@@ -131,7 +189,11 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
|
||||
throws ProcessRequestException {
|
||||
}
|
||||
|
||||
private static class WaitBindTask implements Runnable {
|
||||
public void setAcceptConnectionTimeout(int acceptConnectionTimeout) {
|
||||
this.acceptConnectionTimeout = acceptConnectionTimeout;
|
||||
}
|
||||
|
||||
private static class WaitBindTask implements Runnable {
|
||||
private final SMPPServerSession serverSession;
|
||||
private final String systemId;
|
||||
private final String password;
|
||||
@@ -242,7 +304,12 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
if (new String(shortMessage).equals(Agreement.DELAY_DELIVERY_RECEIPT)) {
|
||||
logger.debug("Receive request to delay sending of delivery receipt");
|
||||
Thread.sleep(messageDelay);
|
||||
} else {
|
||||
Thread.sleep(300); // just give a little bit of delay
|
||||
}
|
||||
} catch (InterruptedException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
@@ -257,6 +324,7 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
|
||||
|
||||
DeliveryReceipt delRec = new DeliveryReceipt(stringValue, totalSubmitted, totalDelivered, new Date(),
|
||||
new Date(), DeliveryReceiptState.DELIVRD, null, new String(shortMessage));
|
||||
logger.debug("Sending delivery receipt for message id " + messageId + ":" + stringValue);
|
||||
session.deliverShortMessage(
|
||||
"mc",
|
||||
sourceAddrTon,
|
||||
@@ -271,7 +339,7 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
|
||||
new RegisteredDelivery(0),
|
||||
DataCodings.ZERO,
|
||||
delRec.toString().getBytes());
|
||||
logger.debug("Sending delivery receipt for message id " + messageId + ":" + stringValue);
|
||||
logger.debug("Delivery receipt sent");
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed sending delivery_receipt for message id " + messageId + ":" + stringValue, e);
|
||||
}
|
||||
@@ -351,13 +419,27 @@ public class MockSmppServer extends ServerResponseDeliveryAdapter implements Run
|
||||
|
||||
@PostConstruct
|
||||
public void onPostConstruct() {
|
||||
logger.debug("Starting mock SMPP server");
|
||||
execService.submit(this);
|
||||
}
|
||||
if (initServerAtStart) {
|
||||
startServer();
|
||||
}
|
||||
}
|
||||
|
||||
public void startServer() {
|
||||
logger.debug("Starting mock SMPP server");
|
||||
execService.submit(this);
|
||||
}
|
||||
|
||||
public void restartServer() throws InterruptedException, IOException {
|
||||
logger.debug("Stopping server");
|
||||
stop();
|
||||
connectionSessionMap.clear();
|
||||
startServer();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void onDestroy() {
|
||||
public void onDestroy() throws InterruptedException, IOException {
|
||||
logger.debug("Destroying mock SMPP server");
|
||||
stop();
|
||||
connectionSessionMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.smpp;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageChannel;
|
||||
import org.springframework.integration.MessagingException;
|
||||
import org.springframework.integration.core.MessagingTemplate;
|
||||
import org.springframework.integration.smpp.core.SmppConstants;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* exercises the outbound adapter.
|
||||
*
|
||||
* @author Johanes Soetanto
|
||||
* @since 1.0
|
||||
*/
|
||||
@ContextConfiguration("classpath:TestSmppOutboundChannelAdapterWithChain-context.xml")
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class TestSmppOutboundChannelAdapterWithChain {
|
||||
|
||||
@Autowired
|
||||
private MessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel outChannel;
|
||||
|
||||
private String smsMessageToSend = "jSMPP is truly a convenient, and powerful API for SMPP " +
|
||||
"on the Java and Spring Integration platforms (sent " + System.currentTimeMillis() + ")";
|
||||
|
||||
@Test
|
||||
public void testSendingGoesToExceptionChannel() throws Throwable {
|
||||
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
|
||||
.setHeader(SmppConstants.SRC_ADDR, "1616")
|
||||
.setHeader(SmppConstants.DST_ADDR, "NoRouteDestination")
|
||||
.build();
|
||||
outChannel.send(smsMsg);
|
||||
|
||||
Thread.sleep(500);
|
||||
Message<?> exception = messagingTemplate.receive("exceptionChannel");
|
||||
Assert.assertNotNull(exception);
|
||||
Assert.assertTrue(exception.getPayload() instanceof MessagingException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSending() throws Throwable {
|
||||
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
|
||||
.setHeader(SmppConstants.SRC_ADDR, "1616")
|
||||
.setHeader(SmppConstants.DST_ADDR, "1616")
|
||||
.build();
|
||||
outChannel.send(smsMsg);
|
||||
|
||||
Thread.sleep(5000);
|
||||
Message<?> exception = messagingTemplate.receive("exceptionChannel");
|
||||
Assert.assertNull(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.integration.smpp;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageChannel;
|
||||
import org.springframework.integration.MessagingException;
|
||||
import org.springframework.integration.core.MessagingTemplate;
|
||||
import org.springframework.integration.smpp.core.SmppConstants;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* Simple tests to make sure that gateway will perform retry and route MessagingException to exception channel when
|
||||
* advice chain is defined with a retry advice and error callback.
|
||||
*
|
||||
* @author Johanes Soetanto
|
||||
* @since 1.0
|
||||
*/
|
||||
@ContextConfiguration("classpath:TestSmppOutboundGatewayWithChain-context.xml")
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class TestSmppOutboundGatewayWithChain {
|
||||
|
||||
@Autowired
|
||||
private MessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel outChannel;
|
||||
|
||||
private String smsMessageToSend = "jSMPP is truly a convenient, and powerful API for SMPP " +
|
||||
"on the Java and Spring Integration platforms (sent " + System.currentTimeMillis() + ")";
|
||||
|
||||
@Test
|
||||
public void testSendingGoesToExceptionChannel() throws Throwable {
|
||||
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
|
||||
.setHeader(SmppConstants.SRC_ADDR, "1616")
|
||||
.setHeader(SmppConstants.DST_ADDR, "NoRouteDestination")
|
||||
.build();
|
||||
outChannel.send(smsMsg);
|
||||
|
||||
Thread.sleep(500);
|
||||
Message<?> exception = messagingTemplate.receive("exceptionChannel");
|
||||
Assert.assertNotNull(exception);
|
||||
Assert.assertTrue(exception.getPayload() instanceof MessagingException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendingGoesToReplyChannel() throws Throwable {
|
||||
Message<String> smsMsg = MessageBuilder.withPayload(this.smsMessageToSend)
|
||||
.setHeader(SmppConstants.SRC_ADDR, "1616")
|
||||
.setHeader(SmppConstants.DST_ADDR, "1616")
|
||||
.build();
|
||||
outChannel.send(smsMsg);
|
||||
|
||||
Thread.sleep(500);
|
||||
Message<?> exception = messagingTemplate.receive("replyChannel");
|
||||
Assert.assertNotNull(exception);
|
||||
Assert.assertTrue(exception.getPayload() instanceof String);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.springframework.integration.smpp;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.integration.smpp.session.SmppSessionFactoryBean;
|
||||
import org.springframework.integration.test.util.SocketUtils;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* This is to test whether reconnection will work. The test procedure is
|
||||
* <ol>
|
||||
* <li>Client connects to SMSC</li>
|
||||
* <li>SMSC shuts down</li>
|
||||
* <li>SMSC restart</li>
|
||||
* <li>Client reconnect to SMSC</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author Johanes Soetanto
|
||||
* @since 1.0
|
||||
*/
|
||||
public class TestSmppSessionReconnection {
|
||||
|
||||
private Logger log = LoggerFactory.getLogger(getClass());
|
||||
int port;
|
||||
String systemId = "pavel";
|
||||
String pass = "wpsd";
|
||||
MockSmppServer server;
|
||||
SmppSessionFactoryBean client;
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
int serverAcceptTimeout = 1500;
|
||||
int clientReconnectInterval = 1000;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
port = SocketUtils.findAvailableServerSocket(13000);
|
||||
|
||||
client = new SmppSessionFactoryBean();
|
||||
client.setPort(port);
|
||||
client.setSystemId(systemId);
|
||||
client.setPassword(pass);
|
||||
client.setReconnectInterval((long)clientReconnectInterval);
|
||||
}
|
||||
|
||||
private void startServer() {
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
log.debug("Starting server");
|
||||
server = new MockSmppServer(port, systemId, pass);
|
||||
server.setAcceptConnectionTimeout(serverAcceptTimeout);
|
||||
server.onPostConstruct();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//@org.junit.Test
|
||||
public void testReconnection() throws Exception {
|
||||
startServer();
|
||||
|
||||
Thread.sleep(500);
|
||||
|
||||
log.debug("Starting client");
|
||||
client.afterPropertiesSet();
|
||||
client.start();
|
||||
Thread.sleep(3000);
|
||||
|
||||
log.debug("Stopping server");
|
||||
server.stop();
|
||||
server.onDestroy();
|
||||
Thread.sleep(3000);
|
||||
|
||||
log.debug("Starting server again");
|
||||
startServer();
|
||||
|
||||
log.debug("The client should reconnect");
|
||||
Thread.sleep(3000);
|
||||
}
|
||||
|
||||
// if client session is destroyed by the container, we don't want to try to re-establish session
|
||||
//@org.junit.Test
|
||||
public void testReconnection_whenClientSessionDestroyed() throws Exception {
|
||||
startServer();
|
||||
|
||||
Thread.sleep(500);
|
||||
|
||||
log.debug("Starting client");
|
||||
client.afterPropertiesSet();
|
||||
client.start();
|
||||
|
||||
Thread.sleep(2000);
|
||||
log.debug("Destroy the client");
|
||||
client.destroy();
|
||||
Thread.sleep(3000);
|
||||
|
||||
log.debug("Stopping server");
|
||||
server.stop();
|
||||
server.onDestroy();
|
||||
Thread.sleep(3000);
|
||||
|
||||
log.debug("Starting server again");
|
||||
startServer();
|
||||
Thread.sleep(3000);
|
||||
}
|
||||
|
||||
// if reconnect is disabled, well do not reconnect
|
||||
//@org.junit.Test
|
||||
public void testDisableReconnect() throws Exception {
|
||||
startServer();
|
||||
|
||||
Thread.sleep(500);
|
||||
client.setReconnect(false);
|
||||
client.afterPropertiesSet();
|
||||
client.start();
|
||||
|
||||
Thread.sleep(2000);
|
||||
log.debug("Stop server");
|
||||
server.stop();
|
||||
server.onDestroy();
|
||||
|
||||
Thread.sleep(2000);
|
||||
log.debug("Starting server again");
|
||||
startServer();
|
||||
Thread.sleep(2000);
|
||||
log.debug("There should be no reconnection from client");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-smpp="http://www.springframework.org/schema/integration/smpp"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/smpp http://www.springframework.org/schema/integration/smpp/spring-integration-smpp.xsd">
|
||||
|
||||
<import resource="classpath:smppConnection-context.xml"/>
|
||||
|
||||
<!-- this is channel for going out -->
|
||||
<int:channel id="outChannel">
|
||||
<int:interceptors>
|
||||
<int:wire-tap channel="outChannelLogger"/>
|
||||
</int:interceptors>
|
||||
</int:channel>
|
||||
<int:logging-channel-adapter id="outChannelLogger" expression="'Outbound Channel Adapter: '+payload"/>
|
||||
|
||||
<!-- this is the gateway for testing -->
|
||||
<int-smpp:outbound-channel-adapter channel="outChannel" smpp-session-ref="session">
|
||||
<int-smpp:request-handler-advice-chain>
|
||||
<ref bean="smppSendingRetryAdvice"/>
|
||||
</int-smpp:request-handler-advice-chain>
|
||||
</int-smpp:outbound-channel-adapter>
|
||||
|
||||
<!-- this is smpp session -->
|
||||
<bean id="session"
|
||||
class="org.springframework.integration.smpp.session.SmppSessionFactoryBean">
|
||||
<property name="host" value="${smpp.host}" />
|
||||
<property name="port" ref="smppPort" />
|
||||
<property name="password" value="${smpp.password}" />
|
||||
<property name="systemId" value="${smpp.systemId}" />
|
||||
<property name="bindType" value="BIND_TRX" />
|
||||
</bean>
|
||||
|
||||
<!-- example of using chain advice to retry and send the error to exception channel -->
|
||||
<bean id="smppSendingRetryAdvice"
|
||||
class="org.springframework.integration.handler.advice.RequestHandlerRetryAdvice">
|
||||
<property name="recoveryCallback">
|
||||
<bean class="org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer">
|
||||
<constructor-arg ref="exceptionChannel" />
|
||||
</bean>
|
||||
</property>
|
||||
<property name="retryTemplate">
|
||||
<bean class="org.springframework.retry.support.RetryTemplate">
|
||||
<property name="retryPolicy">
|
||||
<bean class="org.springframework.retry.policy.NeverRetryPolicy" />
|
||||
</property>
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
<int:channel id="exceptionChannel">
|
||||
<int:queue capacity="10" />
|
||||
<int:interceptors>
|
||||
<int:wire-tap channel="exceptionLogger"/>
|
||||
</int:interceptors>
|
||||
</int:channel>
|
||||
<int:logging-channel-adapter id="exceptionLogger" log-full-message="true" level="ERROR" />
|
||||
|
||||
<!-- messaging template -->
|
||||
<bean class="org.springframework.integration.core.MessagingTemplate">
|
||||
<property name="receiveTimeout" value="1000"/>
|
||||
</bean>
|
||||
</beans>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xmlns:int-smpp="http://www.springframework.org/schema/integration/smpp"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/smpp http://www.springframework.org/schema/integration/smpp/spring-integration-smpp.xsd">
|
||||
|
||||
<import resource="classpath:smppConnection-context.xml"/>
|
||||
|
||||
<!-- this is channel for going out -->
|
||||
<int:channel id="outChannel">
|
||||
<int:interceptors>
|
||||
<int:wire-tap channel="outChannelLogger"/>
|
||||
</int:interceptors>
|
||||
</int:channel>
|
||||
<int:logging-channel-adapter id="outChannelLogger" expression="'Outbound Gateway: ' + payload"/>
|
||||
|
||||
<!-- this is the gateway for testing -->
|
||||
<int-smpp:outbound-gateway request-channel="outChannel"
|
||||
reply-channel="replyChannel"
|
||||
smpp-session-ref="session">
|
||||
<int-smpp:request-handler-advice-chain>
|
||||
<ref bean="smppSendingRetryAdvice"/>
|
||||
</int-smpp:request-handler-advice-chain>
|
||||
</int-smpp:outbound-gateway>
|
||||
|
||||
<!-- this is channel to receive reply -->
|
||||
<int:channel id="replyChannel">
|
||||
<int:queue capacity="10"/>
|
||||
</int:channel>
|
||||
|
||||
<!-- this is smpp session -->
|
||||
<bean id="session"
|
||||
class="org.springframework.integration.smpp.session.SmppSessionFactoryBean">
|
||||
<property name="host" value="${smpp.host}" />
|
||||
<property name="port" ref="smppPort" />
|
||||
<property name="password" value="${smpp.password}" />
|
||||
<property name="systemId" value="${smpp.systemId}" />
|
||||
<property name="bindType" value="BIND_TRX" />
|
||||
</bean>
|
||||
|
||||
<!-- example of using chain advice to retry and send the error to exception channel -->
|
||||
<bean id="smppSendingRetryAdvice"
|
||||
class="org.springframework.integration.handler.advice.RequestHandlerRetryAdvice">
|
||||
<property name="recoveryCallback">
|
||||
<bean class="org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer">
|
||||
<constructor-arg ref="exceptionChannel" />
|
||||
</bean>
|
||||
</property>
|
||||
<property name="retryTemplate">
|
||||
<bean class="org.springframework.retry.support.RetryTemplate">
|
||||
<property name="retryPolicy">
|
||||
<bean class="org.springframework.retry.policy.NeverRetryPolicy" />
|
||||
</property>
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
<int:channel id="exceptionChannel">
|
||||
<int:queue capacity="10" />
|
||||
<int:interceptors>
|
||||
<int:wire-tap channel="exceptionLogger"/>
|
||||
</int:interceptors>
|
||||
</int:channel>
|
||||
<int:logging-channel-adapter id="exceptionLogger" log-full-message="true" level="ERROR"/>
|
||||
|
||||
<!-- messaging template -->
|
||||
<bean class="org.springframework.integration.core.MessagingTemplate">
|
||||
<property name="receiveTimeout" value="1000"/>
|
||||
</bean>
|
||||
</beans>
|
||||
@@ -36,4 +36,11 @@
|
||||
source-address="12345" source-ton="SUBSCRIBER_NUMBER" channel="target">
|
||||
</int-smpp:outbound-channel-adapter>
|
||||
|
||||
<int-smpp:outbound-channel-adapter id="smppOutboundChannelAdapterWithChain"
|
||||
channel="target" smpp-session-ref="session">
|
||||
<int-smpp:request-handler-advice-chain>
|
||||
<bean class="org.springframework.integration.smpp.config.xml.SmppOutboundChannelAdapterParserTests$FooAdvice"/>
|
||||
</int-smpp:request-handler-advice-chain>
|
||||
</int-smpp:outbound-channel-adapter>
|
||||
|
||||
</beans>
|
||||
|
||||
@@ -40,4 +40,15 @@
|
||||
</int-smpp:session>
|
||||
</int-smpp:outbound-gateway>
|
||||
|
||||
<int-smpp:outbound-gateway id="smppOutboundGatewayWithAdvice" request-channel="in" reply-channel="out">
|
||||
<int-smpp:session>
|
||||
<bean
|
||||
class="org.springframework.integration.smpp.config.xml.MockSmppSessionFactory"
|
||||
factory-method="getOutSmppSession" />
|
||||
</int-smpp:session>
|
||||
<int-smpp:request-handler-advice-chain>
|
||||
<bean class="org.springframework.integration.smpp.config.xml.SmppOutboundGatewayParserTests$FooAdvice"/>
|
||||
</int-smpp:request-handler-advice-chain>
|
||||
</int-smpp:outbound-gateway>
|
||||
|
||||
</beans>
|
||||
|
||||
Reference in New Issue
Block a user