aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/traccar/sms/smpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/traccar/sms/smpp')
-rw-r--r--src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java82
-rw-r--r--src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java59
-rw-r--r--src/main/java/org/traccar/sms/smpp/ReconnectionTask.java32
-rw-r--r--src/main/java/org/traccar/sms/smpp/SmppClient.java272
-rw-r--r--src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java37
5 files changed, 482 insertions, 0 deletions
diff --git a/src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java b/src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java
new file mode 100644
index 000000000..6b9de3107
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/ClientSmppSessionHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.traccar.sms.smpp;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.cloudhopper.commons.charset.CharsetUtil;
+import com.cloudhopper.smpp.SmppConstants;
+import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
+import com.cloudhopper.smpp.pdu.DeliverSm;
+import com.cloudhopper.smpp.pdu.PduRequest;
+import com.cloudhopper.smpp.pdu.PduResponse;
+import com.cloudhopper.smpp.util.SmppUtil;
+
+public class ClientSmppSessionHandler extends DefaultSmppSessionHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClientSmppSessionHandler.class);
+
+ private SmppClient smppClient;
+
+ public ClientSmppSessionHandler(SmppClient smppClient) {
+ this.smppClient = smppClient;
+ }
+
+ @Override
+ public void firePduRequestExpired(PduRequest pduRequest) {
+ LOGGER.warn("PDU request expired: " + pduRequest);
+ }
+
+ @Override
+ public PduResponse firePduRequestReceived(PduRequest request) {
+ PduResponse response;
+ try {
+ if (request instanceof DeliverSm) {
+ String sourceAddress = ((DeliverSm) request).getSourceAddress().getAddress();
+ String message = CharsetUtil.decode(((DeliverSm) request).getShortMessage(),
+ smppClient.mapDataCodingToCharset(((DeliverSm) request).getDataCoding()));
+ LOGGER.info("SMS Message Received: " + message.trim() + ", Source Address: " + sourceAddress);
+
+ boolean isDeliveryReceipt;
+ if (smppClient.getDetectDlrByOpts()) {
+ isDeliveryReceipt = request.getOptionalParameters() != null;
+ } else {
+ isDeliveryReceipt = SmppUtil.isMessageTypeAnyDeliveryReceipt(((DeliverSm) request).getEsmClass());
+ }
+
+ if (!isDeliveryReceipt) {
+ TextMessageEventHandler.handleTextMessage(sourceAddress, message);
+ }
+ }
+ response = request.createResponse();
+ } catch (Exception error) {
+ LOGGER.warn("SMS receiving error", error);
+ response = request.createResponse();
+ response.setResultMessage(error.getMessage());
+ response.setCommandStatus(SmppConstants.STATUS_UNKNOWNERR);
+ }
+ return response;
+ }
+
+ @Override
+ public void fireChannelUnexpectedlyClosed() {
+ LOGGER.warn("SMPP session channel unexpectedly closed");
+ smppClient.scheduleReconnect();
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java b/src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java
new file mode 100644
index 000000000..7086709d7
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/EnquireLinkTask.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.traccar.sms.smpp;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.cloudhopper.smpp.SmppSession;
+import com.cloudhopper.smpp.pdu.EnquireLink;
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
+
+public class EnquireLinkTask implements Runnable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EnquireLinkTask.class);
+
+ private SmppClient smppClient;
+ private Integer enquireLinkTimeout;
+
+ public EnquireLinkTask(SmppClient smppClient, Integer enquireLinkTimeout) {
+ this.smppClient = smppClient;
+ this.enquireLinkTimeout = enquireLinkTimeout;
+ }
+
+ @Override
+ public void run() {
+ SmppSession smppSession = smppClient.getSession();
+ if (smppSession != null && smppSession.isBound()) {
+ try {
+ smppSession.enquireLink(new EnquireLink(), enquireLinkTimeout);
+ } catch (SmppTimeoutException | SmppChannelException
+ | RecoverablePduException | UnrecoverablePduException error) {
+ LOGGER.warn("Enquire link failed, executing reconnect: ", error);
+ smppClient.scheduleReconnect();
+ } catch (InterruptedException error) {
+ LOGGER.info("Enquire link interrupted, probably killed by reconnecting");
+ }
+ } else {
+ LOGGER.warn("Enquire link running while session is not connected");
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/ReconnectionTask.java b/src/main/java/org/traccar/sms/smpp/ReconnectionTask.java
new file mode 100644
index 000000000..c009de8e7
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/ReconnectionTask.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.traccar.sms.smpp;
+
+public class ReconnectionTask implements Runnable {
+
+ private final SmppClient smppClient;
+
+ protected ReconnectionTask(SmppClient smppClient) {
+ this.smppClient = smppClient;
+ }
+
+ @Override
+ public void run() {
+ smppClient.reconnect();
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/SmppClient.java b/src/main/java/org/traccar/sms/smpp/SmppClient.java
new file mode 100644
index 000000000..874253d36
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/SmppClient.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.traccar.sms.smpp;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.notification.MessageException;
+import org.traccar.sms.SmsManager;
+
+import com.cloudhopper.commons.charset.CharsetUtil;
+import com.cloudhopper.smpp.SmppBindType;
+import com.cloudhopper.smpp.SmppConstants;
+import com.cloudhopper.smpp.SmppSession;
+import com.cloudhopper.smpp.SmppSessionConfiguration;
+import com.cloudhopper.smpp.impl.DefaultSmppClient;
+import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
+import com.cloudhopper.smpp.pdu.SubmitSm;
+import com.cloudhopper.smpp.pdu.SubmitSmResp;
+import com.cloudhopper.smpp.tlv.Tlv;
+import com.cloudhopper.smpp.type.Address;
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
+
+public class SmppClient implements SmsManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SmppClient.class);
+
+ private SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration();
+ private SmppSession smppSession;
+ private DefaultSmppSessionHandler sessionHandler = new ClientSmppSessionHandler(this);
+ private ExecutorService executorService = Executors.newCachedThreadPool();
+ private DefaultSmppClient clientBootstrap = new DefaultSmppClient();
+
+ private ScheduledExecutorService enquireLinkExecutor;
+ private ScheduledFuture<?> enquireLinkTask;
+ private Integer enquireLinkPeriod;
+ private Integer enquireLinkTimeout;
+
+ private ScheduledExecutorService reconnectionExecutor;
+ private ScheduledFuture<?> reconnectionTask;
+ private Integer reconnectionDelay;
+
+ private String sourceAddress;
+ private String commandSourceAddress;
+ private int submitTimeout;
+ private boolean requestDlr;
+ private boolean detectDlrByOpts;
+ private String notificationsCharsetName;
+ private byte notificationsDataCoding;
+ private String commandsCharsetName;
+ private byte commandsDataCoding;
+
+ private byte sourceTon;
+ private byte sourceNpi;
+ private byte commandSourceTon;
+ private byte commandSourceNpi;
+
+ private byte destTon;
+ private byte destNpi;
+
+ public SmppClient() {
+ sessionConfig.setName("Traccar.smppSession");
+ sessionConfig.setInterfaceVersion(
+ (byte) Context.getConfig().getInteger("sms.smpp.version", SmppConstants.VERSION_3_4));
+ sessionConfig.setType(SmppBindType.TRANSCEIVER);
+ sessionConfig.setHost(Context.getConfig().getString("sms.smpp.host", "localhost"));
+ sessionConfig.setPort(Context.getConfig().getInteger("sms.smpp.port", 2775));
+ sessionConfig.setSystemId(Context.getConfig().getString("sms.smpp.username", "user"));
+ sessionConfig.setSystemType(Context.getConfig().getString("sms.smpp.systemType", null));
+ sessionConfig.setPassword(Context.getConfig().getString("sms.smpp.password", "password"));
+ sessionConfig.getLoggingOptions().setLogBytes(false);
+ sessionConfig.getLoggingOptions().setLogPdu(Context.getConfig().getBoolean("sms.smpp.logPdu"));
+
+ sourceAddress = Context.getConfig().getString("sms.smpp.sourceAddress", "");
+ commandSourceAddress = Context.getConfig().getString("sms.smpp.commandSourceAddress", sourceAddress);
+ submitTimeout = Context.getConfig().getInteger("sms.smpp.submitTimeout", 10000);
+
+ requestDlr = Context.getConfig().getBoolean("sms.smpp.requestDlr");
+ detectDlrByOpts = Context.getConfig().getBoolean("sms.smpp.detectDlrByOpts");
+
+ notificationsCharsetName = Context.getConfig().getString("sms.smpp.notificationsCharset",
+ CharsetUtil.NAME_UCS_2);
+ notificationsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.notificationsDataCoding",
+ SmppConstants.DATA_CODING_UCS2);
+ commandsCharsetName = Context.getConfig().getString("sms.smpp.commandsCharset",
+ CharsetUtil.NAME_GSM);
+ commandsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.commandsDataCoding",
+ SmppConstants.DATA_CODING_DEFAULT);
+
+
+ sourceTon = (byte) Context.getConfig().getInteger("sms.smpp.sourceTon", SmppConstants.TON_ALPHANUMERIC);
+ commandSourceTon = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceTon", sourceTon);
+ sourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.sourceNpi", SmppConstants.NPI_UNKNOWN);
+ commandSourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceNpi", sourceNpi);
+
+ destTon = (byte) Context.getConfig().getInteger("sms.smpp.destTon", SmppConstants.TON_INTERNATIONAL);
+ destNpi = (byte) Context.getConfig().getInteger("sms.smpp.destNpi", SmppConstants.NPI_E164);
+
+ enquireLinkPeriod = Context.getConfig().getInteger("sms.smpp.enquireLinkPeriod", 60000);
+ enquireLinkTimeout = Context.getConfig().getInteger("sms.smpp.enquireLinkTimeout", 10000);
+ enquireLinkExecutor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable);
+ String name = sessionConfig.getName();
+ thread.setName("EnquireLink-" + name);
+ return thread;
+ }
+ });
+
+ reconnectionDelay = Context.getConfig().getInteger("sms.smpp.reconnectionDelay", 10000);
+ reconnectionExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable);
+ String name = sessionConfig.getName();
+ thread.setName("Reconnection-" + name);
+ return thread;
+ }
+ });
+
+ scheduleReconnect();
+ }
+
+ public synchronized SmppSession getSession() {
+ return smppSession;
+ }
+
+ public String mapDataCodingToCharset(byte dataCoding) {
+ switch (dataCoding) {
+ case SmppConstants.DATA_CODING_LATIN1:
+ return CharsetUtil.NAME_ISO_8859_1;
+ case SmppConstants.DATA_CODING_UCS2:
+ return CharsetUtil.NAME_UCS_2;
+ default:
+ return CharsetUtil.NAME_GSM;
+ }
+ }
+
+ public boolean getDetectDlrByOpts() {
+ return detectDlrByOpts;
+ }
+
+ protected synchronized void reconnect() {
+ try {
+ disconnect();
+ smppSession = clientBootstrap.bind(sessionConfig, sessionHandler);
+ stopReconnectionkTask();
+ runEnquireLinkTask();
+ LOGGER.info("SMPP session connected");
+ } catch (SmppTimeoutException | SmppChannelException
+ | UnrecoverablePduException | InterruptedException error) {
+ LOGGER.warn("Unable to connect to SMPP server: ", error);
+ }
+ }
+
+ public void scheduleReconnect() {
+ if (reconnectionTask == null || reconnectionTask.isDone()) {
+ reconnectionTask = reconnectionExecutor.scheduleWithFixedDelay(
+ new ReconnectionTask(this),
+ reconnectionDelay, reconnectionDelay, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private void stopReconnectionkTask() {
+ if (reconnectionTask != null) {
+ reconnectionTask.cancel(false);
+ }
+ }
+
+ private void disconnect() {
+ stopEnquireLinkTask();
+ destroySession();
+ }
+
+ private void runEnquireLinkTask() {
+ enquireLinkTask = enquireLinkExecutor.scheduleWithFixedDelay(
+ new EnquireLinkTask(this, enquireLinkTimeout),
+ enquireLinkPeriod, enquireLinkPeriod, TimeUnit.MILLISECONDS);
+ }
+
+ private void stopEnquireLinkTask() {
+ if (enquireLinkTask != null) {
+ enquireLinkTask.cancel(true);
+ }
+ }
+
+ private void destroySession() {
+ if (smppSession != null) {
+ LOGGER.info("Cleaning up SMPP session... ");
+ smppSession.destroy();
+ smppSession = null;
+ }
+ }
+
+ @Override
+ public synchronized void sendMessageSync(String destAddress, String message, boolean command)
+ throws MessageException, InterruptedException, IllegalStateException {
+ if (getSession() != null && getSession().isBound()) {
+ try {
+ SubmitSm submit = new SubmitSm();
+ byte[] textBytes;
+ textBytes = CharsetUtil.encode(message, command ? commandsCharsetName : notificationsCharsetName);
+ submit.setDataCoding(command ? commandsDataCoding : notificationsDataCoding);
+ if (requestDlr) {
+ submit.setRegisteredDelivery(SmppConstants.REGISTERED_DELIVERY_SMSC_RECEIPT_REQUESTED);
+ }
+
+ if (textBytes != null && textBytes.length > 255) {
+ submit.addOptionalParameter(new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, textBytes,
+ "message_payload"));
+ } else {
+ submit.setShortMessage(textBytes);
+ }
+
+ submit.setSourceAddress(command ? new Address(commandSourceTon, commandSourceNpi, commandSourceAddress)
+ : new Address(sourceTon, sourceNpi, sourceAddress));
+ submit.setDestAddress(new Address(destTon, destNpi, destAddress));
+ SubmitSmResp submitResponce = getSession().submit(submit, submitTimeout);
+ if (submitResponce.getCommandStatus() == SmppConstants.STATUS_OK) {
+ LOGGER.info("SMS submitted, message id: " + submitResponce.getMessageId());
+ } else {
+ throw new IllegalStateException(submitResponce.getResultMessage());
+ }
+ } catch (SmppChannelException | RecoverablePduException
+ | SmppTimeoutException | UnrecoverablePduException error) {
+ throw new MessageException(error);
+ }
+ } else {
+ throw new MessageException(new SmppChannelException("SMPP session is not connected"));
+ }
+ }
+
+ @Override
+ public void sendMessageAsync(final String destAddress, final String message, final boolean command) {
+ executorService.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sendMessageSync(destAddress, message, command);
+ } catch (MessageException | InterruptedException | IllegalStateException error) {
+ LOGGER.warn("SMS sending error", error);
+ }
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java b/src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java
new file mode 100644
index 000000000..37c29972d
--- /dev/null
+++ b/src/main/java/org/traccar/sms/smpp/TextMessageEventHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.traccar.sms.smpp;
+
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+
+public final class TextMessageEventHandler {
+
+ private TextMessageEventHandler() {
+ }
+
+ public static void handleTextMessage(String phone, String message) {
+ Device device = Context.getDeviceManager().getDeviceByPhone(phone);
+ if (device != null && Context.getNotificationManager() != null) {
+ Event event = new Event(Event.TYPE_TEXT_MESSAGE, device.getId());
+ event.set("message", message);
+ Context.getNotificationManager().updateEvent(event, null);
+ }
+ }
+
+}