[smsmodem] Initial contribution (#12250)

* [smsmodem] Initial contribution

This binding connects to a USB serial GSM modem (or a network exposed one, a.k.a ser2net) and allows openHAB to send and receive SMS through it.

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] README fix

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] build/spotless fix

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] compliance with 3rd party license

And long running thread naming convention
And treated some code warning 

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] i18n

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Small fixes

update channel
rename action to avoid colision with other binding and a too generic name

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Use of standard Thing properties

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Fix sender identifier error with special character

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Add encoding parameter

For non latin character in SMS

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Apply review

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Split local and remote modem in two thing-types

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Apply review

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Apply review

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* [smsmodem] Apply code review (removing unnecessary method)

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
Co-authored-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
This commit is contained in:
Gwendal Roulleau
2022-12-03 21:35:30 +01:00
committed by GitHub
parent 3e068ed431
commit 56728b6091
58 changed files with 7413 additions and 1 deletions

View File

@@ -0,0 +1,49 @@
package org.smslib;
import java.util.BitSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class Capabilities {
BitSet caps = new BitSet();
public enum Caps {
CanSendMessage,
CanSendBinaryMessage,
CanSendUnicodeMessage,
CanSendWapMessage,
CanSendFlashMessage,
CanSendPortInfo,
CanSetSenderId,
CanSplitMessages,
CanRequestDeliveryStatus,
CanQueryDeliveryStatus,
CanQueryCreditBalance,
CanQueryCoverage,
CanSetValidityPeriod
}
public void set(Caps c) {
this.caps.set(c.ordinal());
}
public BitSet getCapabilities() {
return (BitSet) this.caps.clone();
}
@Override
public String toString() {
BitSet bs = (BitSet) getCapabilities().clone();
StringBuffer b = new StringBuffer();
for (Caps c : Caps.values()) {
b.append(String.format("%-30s : ", c.toString()));
b.append(bs.get(c.ordinal()) ? "YES" : "NO");
b.append("\n");
}
return b.toString();
}
}

View File

@@ -0,0 +1,20 @@
package org.smslib;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Wrapper for communication exception
*/
@NonNullByDefault
public class CommunicationException extends Exception {
private static final long serialVersionUID = -5175636461754717860L;
public CommunicationException(String message, Exception cause) {
super(message, cause);
}
public CommunicationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,161 @@
package org.smslib;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.smslib.callback.IDeviceInformationListener;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class DeviceInformation {
@Nullable
private IDeviceInformationListener deviceInformationListener;
public enum Modes {
PDU,
TEXT
}
String manufacturer = "N/A";
String model = "N/A";
String swVersion = "N/A";
String serialNo = "N/A";
String imsi = "N/A";
int rssi = 0;
@Nullable
Modes mode;
int totalSent = 0;
int totalFailed = 0;
int totalReceived = 0;
int totalFailures = 0;
public void setDeviceInformationListener(@Nullable IDeviceInformationListener deviceInformationListener) {
this.deviceInformationListener = deviceInformationListener;
}
public synchronized void increaseTotalSent() {
this.totalSent++;
IDeviceInformationListener dil = deviceInformationListener;
if (dil != null) {
dil.setTotalSent(Integer.toString(totalSent));
}
}
public synchronized void increaseTotalFailed() {
this.totalFailed++;
IDeviceInformationListener dil = deviceInformationListener;
if (dil != null) {
dil.setTotalFailed(Integer.toString(totalFailed));
}
}
public synchronized void increaseTotalReceived() {
this.totalReceived++;
IDeviceInformationListener dil = deviceInformationListener;
if (dil != null) {
dil.setTotalReceived(Integer.toString(totalReceived));
}
}
public synchronized void increaseTotalFailures() {
this.totalFailures++;
IDeviceInformationListener dil = deviceInformationListener;
if (dil != null) {
dil.setTotalFailures(Integer.toString(totalFailures));
}
}
public String getManufacturer() {
return this.manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
if (finalDeviceInformationListener != null) {
finalDeviceInformationListener.setManufacturer(manufacturer);
}
}
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
if (finalDeviceInformationListener != null) {
finalDeviceInformationListener.setModel(model);
}
}
public String getSwVersion() {
return this.swVersion;
}
public void setSwVersion(String swVersion) {
this.swVersion = swVersion;
IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
if (finalDeviceInformationListener != null) {
finalDeviceInformationListener.setSwVersion(swVersion);
}
}
public String getSerialNo() {
return this.serialNo;
}
public void setSerialNo(String serialNo) {
this.serialNo = serialNo;
IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
if (finalDeviceInformationListener != null) {
finalDeviceInformationListener.setSerialNo(serialNo);
}
}
public String getImsi() {
return this.imsi;
}
public void setImsi(String imsi) {
this.imsi = imsi;
IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
if (finalDeviceInformationListener != null) {
finalDeviceInformationListener.setImsi(imsi);
}
}
public int getRssi() {
return this.rssi;
}
public void setRssi(int rssi) {
this.rssi = rssi;
IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
if (finalDeviceInformationListener != null) {
finalDeviceInformationListener.setRssi(Integer.toString(rssi));
}
}
public @Nullable Modes getMode() {
return this.mode;
}
public void setMode(Modes mode) {
this.mode = mode;
IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
if (finalDeviceInformationListener != null) {
finalDeviceInformationListener.setMode(mode.toString());
}
}
@Override
public String toString() {
return String.format("MANUF:%s, MODEL:%s, SERNO:%s, IMSI:%s, SW:%s, RSSI:%ddBm, MODE:%s", getManufacturer(),
getModel(), getSerialNo(), getImsi(), getSwVersion(), getRssi(), getMode());
}
}

View File

@@ -0,0 +1,387 @@
package org.smslib;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.DeviceInformation.Modes;
import org.smslib.Modem.Status;
import org.smslib.message.DeliveryReportMessage;
import org.smslib.message.InboundBinaryMessage;
import org.smslib.message.InboundMessage;
import org.smslib.message.Payload;
import org.smslib.pduUtils.gsm3040.Pdu;
import org.smslib.pduUtils.gsm3040.PduParser;
import org.smslib.pduUtils.gsm3040.PduUtils;
import org.smslib.pduUtils.gsm3040.SmsDeliveryPdu;
import org.smslib.pduUtils.gsm3040.SmsStatusReportPdu;
/**
*
* Poll the modem to check for new received messages
* (sms or delivery report)
*
* Extracted from SMSLib
*/
@NonNullByDefault
public class MessageReader extends Thread {
static Logger logger = LoggerFactory.getLogger(MessageReader.class);
Modem modem;
private static int HOURS_TO_RETAIN_ORPHANED_MESSAGE_PARTS = 72;
public MessageReader(Modem modem) {
this.modem = modem;
}
@Override
public void run() {
logger.debug("Started!");
if (this.modem.getStatus() == Status.Started) {
try {
this.modem.getModemDriver().lock();
ArrayList<InboundMessage> messageList = new ArrayList<InboundMessage>();
try {
for (int i = 0; i < (this.modem.getModemDriver().getMemoryLocations().length() / 2); i++) {
String memLocation = this.modem.getModemDriver().getMemoryLocations().substring((i * 2),
(i * 2) + 2);
String data = this.modem.getModemDriver().atGetMessages(memLocation).getResponseData();
if (data.length() > 0) {
messageList.addAll((this.modem.getDeviceInformation().getMode() == Modes.PDU
? parsePDU(data, memLocation)
: parseTEXT(data, memLocation)));
}
}
} finally {
this.modem.getModemDriver().unlock();
}
for (InboundMessage message : messageList) {
processMessage(message);
}
} catch (CommunicationException | IOException e) {
logger.error("Unhandled exception while trying to read new messages", e);
modem.error();
}
}
logger.debug("Stopped!");
}
private ArrayList<InboundMessage> parsePDU(String data, String memLocation) throws IOException {
ArrayList<InboundMessage> messageList = new ArrayList<>();
List<List<InboundMessage>> mpMsgList = new ArrayList<>();
BufferedReader reader = new BufferedReader(new StringReader(data));
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
PduParser parser = new PduParser();
int i = line.indexOf(':');
int j = line.indexOf(',');
if (j == -1) {
logger.error("Bad PDU announce : {}", line);
continue;
}
int memIndex = Integer.parseInt(line.substring(i + 1, j).trim());
i = line.lastIndexOf(',');
j = line.length();
int pduSize = Integer.parseInt(line.substring(i + 1, j).trim());
String pduString = reader.readLine().trim();
if ((pduSize > 0) && ((pduSize * 2) == pduString.length())) {
pduString = "00" + pduString;
}
Pdu pdu = parser.parsePdu(pduString);
if (pdu instanceof SmsDeliveryPdu) {
logger.debug("PDU = {}", pdu.toString());
InboundMessage msg = null;
if (pdu.isBinary()) {
msg = new InboundBinaryMessage((SmsDeliveryPdu) pdu, memLocation, memIndex);
} else {
msg = new InboundMessage((SmsDeliveryPdu) pdu, memLocation, memIndex);
}
msg.setGatewayId(this.modem.getGatewayId());
msg.setGatewayId(this.modem.getGatewayId());
logger.debug("IN-DTLS: MI:{} REF:{} MAX:{} SEQ:{}", msg.getMemIndex(), msg.getMpRefNo(),
msg.getMpMaxNo(), msg.getMpSeqNo());
if (msg.getMpRefNo() == 0) {
messageList.add(msg);
} else {
// multi-part message
int k, l;
List<InboundMessage> tmpList;
InboundMessage listMsg;
boolean found, duplicate;
found = false;
for (k = 0; k < mpMsgList.size(); k++) {
// List of List<InboundMessage>
tmpList = mpMsgList.get(k);
listMsg = tmpList.get(0);
// check if current message list is for this message
if (listMsg.getMpRefNo() == msg.getMpRefNo()) {
duplicate = false;
// check if the message is already in the message list
for (l = 0; l < tmpList.size(); l++) {
listMsg = tmpList.get(l);
if (listMsg.getMpSeqNo() == msg.getMpSeqNo()) {
duplicate = true;
break;
}
}
if (!duplicate) {
tmpList.add(msg);
}
found = true;
break;
}
}
if (!found) {
// no existing list present for this message
// add one
tmpList = new ArrayList<>();
tmpList.add(msg);
mpMsgList.add(tmpList);
}
}
} else if (pdu instanceof SmsStatusReportPdu) {
DeliveryReportMessage msg;
msg = new DeliveryReportMessage((SmsStatusReportPdu) pdu, memLocation, memIndex);
msg.setGatewayId(this.modem.getGatewayId());
messageList.add(msg);
}
}
checkMpMsgList(messageList, mpMsgList);
List<InboundMessage> tmpList;
for (int k = 0; k < mpMsgList.size(); k++) {
tmpList = mpMsgList.get(k);
tmpList.clear();
}
mpMsgList.clear();
return messageList;
}
private ArrayList<InboundMessage> parseTEXT(String data, String memLocation) throws IOException {
ArrayList<InboundMessage> messageList = new ArrayList<>();
BufferedReader reader;
String line;
Calendar cal1 = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
String myData = data;
myData = myData.replaceAll("\\s+OK\\s+", "\nOK");
myData = myData.replaceAll("$", "\n");
logger.debug(myData);
reader = new BufferedReader(new StringReader(myData));
for (;;) {
line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.length() > 0) {
break;
}
}
while (true) {
if (line == null) {
break;
}
if (line.length() <= 0 || "OK".equalsIgnoreCase(line)) {
break;
}
int i = line.indexOf(':');
int j = line.indexOf(',');
int memIndex = Integer.parseInt(line.substring(i + 1, j).trim());
StringTokenizer tokens = new StringTokenizer(line, ",");
tokens.nextToken();
tokens.nextToken();
String tmpLine = "";
if (Character.isDigit(tokens.nextToken().trim().charAt(0))) {
line = line.replaceAll(",,", ", ,");
tokens = new StringTokenizer(line, ",");
tokens.nextToken();
tokens.nextToken();
tokens.nextToken();
String messageId = tokens.nextToken();
String recipient = tokens.nextToken().replaceAll("\"", "");
String dateStr = tokens.nextToken().replaceAll("\"", "");
if (dateStr.indexOf('/') == -1) {
dateStr = tokens.nextToken().replaceAll("\"", "");
}
cal1.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
cal1.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
cal1.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
dateStr = tokens.nextToken().replaceAll("\"", "");
cal1.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
cal1.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
cal1.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
dateStr = tokens.nextToken().replaceAll("\"", "");
cal2.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
cal2.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
cal2.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
dateStr = tokens.nextToken().replaceAll("\"", "");
cal2.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
cal2.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
cal2.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
DeliveryReportMessage msg;
msg = new DeliveryReportMessage(messageId, recipient, memLocation, memIndex, cal1.getTime(),
cal2.getTime());
msg.setGatewayId(this.modem.getGatewayId());
messageList.add(msg);
} else {
line = line.replaceAll(",,", ", ,");
tokens = new StringTokenizer(line, ",");
tokens.nextToken();
tokens.nextToken();
String originator = tokens.nextToken().replaceAll("\"", "");
tokens.nextToken();
String dateStr = tokens.nextToken().replaceAll("\"", "");
cal1.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
cal1.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
cal1.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
dateStr = tokens.nextToken().replaceAll("\"", "");
cal1.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
cal1.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
cal1.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
String msgText = "";
while (true) {
tmpLine = reader.readLine();
if (tmpLine == null) {
break;
}
if (tmpLine.startsWith("+CMGL")) {
break;
}
if (tmpLine.startsWith("+CMGR")) {
break;
}
msgText += (msgText.length() == 0 ? "" : "\n") + tmpLine;
}
InboundMessage msg = new InboundMessage(originator, msgText.trim(), cal1.getTime(), memLocation,
memIndex);
msg.setGatewayId(this.modem.getGatewayId());
messageList.add(msg);
}
while (true) {
// line = reader.readLine();
line = ((tmpLine == null || tmpLine.length() == 0) ? reader.readLine() : tmpLine);
if (line == null) {
break;
}
line = line.trim();
if (line.length() > 0) {
break;
}
}
}
reader.close();
return messageList;
}
private void checkMpMsgList(Collection<InboundMessage> msgList, List<List<InboundMessage>> mpMsgList) {
int k, l, m;
List<InboundMessage> tmpList;
InboundMessage listMsg, mpMsg;
boolean found;
mpMsg = null;
logger.debug("CheckMpMsgList(): MAINLIST: {}", mpMsgList.size());
for (k = 0; k < mpMsgList.size(); k++) {
tmpList = mpMsgList.get(k);
logger.debug("CheckMpMsgList(): SUBLIST[{}]: ", tmpList.size());
listMsg = tmpList.get(0);
found = false;
if (listMsg.getMpMaxNo() == tmpList.size()) {
found = true;
for (l = 0; l < tmpList.size(); l++) {
for (m = 0; m < tmpList.size(); m++) {
listMsg = tmpList.get(m);
if (listMsg.getMpSeqNo() == (l + 1)) {
if (listMsg.getMpSeqNo() == 1) {
mpMsg = listMsg;
mpMsg.setMpMemIndex(mpMsg.getMemIndex());
if (listMsg.getMpMaxNo() == 1) {
msgList.add(mpMsg);
}
} else {
if (mpMsg != null) {
String textToAdd = listMsg.getPayload().getText();
if (mpMsg.getEndsWithMultiChar()) {
if (textToAdd == null) {
throw new UnrecoverableSmslibException("Cannot add text to message");
}
// adjust first char of textToAdd
logger.debug("Adjusting dangling multi-char: {} --> {}", textToAdd.charAt(0),
PduUtils.getMultiCharFor(textToAdd.charAt(0)));
textToAdd = PduUtils.getMultiCharFor(textToAdd.charAt(0))
+ textToAdd.substring(1);
}
mpMsg.setEndsWithMultiChar(listMsg.getEndsWithMultiChar());
mpMsg.setPayload(new Payload(mpMsg.getPayload().getText() + textToAdd));
// }
mpMsg.setMpSeqNo(listMsg.getMpSeqNo());
mpMsg.setMpMemIndex(listMsg.getMemIndex());
if (listMsg.getMpSeqNo() == listMsg.getMpMaxNo()) {
mpMsg.setMemIndex(-1);
msgList.add(mpMsg);
mpMsg = null;
}
}
}
break;
}
}
}
tmpList.clear();
tmpList = null;
}
if (found) {
mpMsgList.remove(k);
k--;
}
}
// Check the remaining parts for "orphaned" status
for (List<InboundMessage> remainingList : mpMsgList) {
for (InboundMessage msg : remainingList) {
Date sentDate = msg.getSentDate();
if (sentDate == null || getAgeInHours(sentDate) > HOURS_TO_RETAIN_ORPHANED_MESSAGE_PARTS) {
try {
this.modem.delete(msg);
} catch (CommunicationException e) {
logger.error("Could not delete orphaned message: {}", msg.toString(), e);
}
}
}
}
}
private static int getAgeInHours(Date fromDate) {
Calendar cal = Calendar.getInstance();
cal.setTime(new java.util.Date());
long now = cal.getTimeInMillis();
cal.setTime(fromDate);
long past = cal.getTimeInMillis();
return (int) ((now - past) / (60 * 60 * 1000));
}
private void processMessage(InboundMessage message) {
String messageSignature = message.getSignature();
if (!this.modem.getReadMessagesSet().contains(messageSignature)) {
this.modem.getDeviceInformation().increaseTotalReceived();
if (message instanceof DeliveryReportMessage) {
modem.processDeliveryReport((DeliveryReportMessage) message);
} else {
modem.processMessage(message);
}
this.modem.getReadMessagesSet().add(messageSignature);
}
}
}

View File

@@ -0,0 +1,78 @@
package org.smslib;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.message.OutboundMessage;
import org.smslib.message.OutboundMessage.FailureCause;
import org.smslib.message.OutboundMessage.SentStatus;
/**
* Poll the modem queue and send messages
*
* Extracted from SMSLib
*/
@NonNullByDefault
public class MessageSender extends Thread {
static Logger logger = LoggerFactory.getLogger(MessageSender.class);
Queue<OutboundMessage> messageQueue;
Modem modem;
private int gatewayDispatcherYield;
private AtomicBoolean isRunning = new AtomicBoolean(false);
private boolean interrupt = false;
public MessageSender(String name, Queue<OutboundMessage> messageQueue, Modem modem, int gatewayDispatcherYield) {
setName(name);
setDaemon(false);
this.messageQueue = messageQueue;
this.modem = modem;
this.gatewayDispatcherYield = gatewayDispatcherYield;
}
@Override
public void run() {
if (!isRunning.getAndSet(true)) {
interrupt = false; // reset interruption status
try {
logger.debug("Started!");
while (!interrupt && messageQueue.size() > 0) {
try {
OutboundMessage message = messageQueue.poll();
if (message != null) {
try {
this.modem.send(message);
} catch (CommunicationException e) {
logger.error("Send failed!", e);
message.setSentStatus(SentStatus.Failed);
message.setFailureCause(FailureCause.None);
} finally {
this.modem.processMessageSent(message);
sleep(this.gatewayDispatcherYield);
}
}
} catch (InterruptedException e) {
logger.debug("Message dispatcher thread interrupted", e);
}
}
logger.debug("Ended!");
} finally {
this.isRunning.set(false);
}
}
}
public void setInterrupt() {
this.interrupt = true;
}
public boolean isRunning() {
return isRunning.get();
}
}

View File

@@ -0,0 +1,444 @@
package org.smslib;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.Capabilities.Caps;
import org.smslib.DeviceInformation.Modes;
import org.smslib.callback.IDeviceInformationListener;
import org.smslib.callback.IInboundOutboundMessageListener;
import org.smslib.callback.IModemStatusListener;
import org.smslib.driver.AbstractModemDriver;
import org.smslib.driver.IPModemDriver;
import org.smslib.driver.JSerialModemDriver;
import org.smslib.message.DeliveryReportMessage;
import org.smslib.message.InboundMessage;
import org.smslib.message.MsIsdn;
import org.smslib.message.OutboundMessage;
import org.smslib.message.OutboundMessage.FailureCause;
import org.smslib.message.OutboundMessage.SentStatus;
import org.smslib.message.Payload;
import org.smslib.message.Payload.Type;
/**
* The Modem class is an abstraction, central to all operations
*
* Extracted from SMSLib
*/
@NonNullByDefault
public class Modem {
static Logger logger = LoggerFactory.getLogger(Modem.class);
public enum Status {
Starting,
Started,
Stopping,
Stopped,
Error
}
AbstractModemDriver modemDriver;
String simPin;
MsIsdn smscNumber;
protected String operatorId = "";
String gatewayId = "";
String description = "";
private ScheduledExecutorService scheduledService;
@Nullable
ScheduledFuture<?> messageReader;
MessageSender messageSender;
Queue<OutboundMessage> messageQueue = new ConcurrentLinkedQueue<>();
HashSet<String> readMessagesSet;
Status status = Status.Stopped;
Lock startAndStoplock = new ReentrantLock();
int multipartReferenceNo = 0;
Capabilities capabilities = new Capabilities();
DeviceInformation deviceInformation = new DeviceInformation();
private @Nullable IModemStatusListener modemStatusCallback = null;
private @Nullable IInboundOutboundMessageListener messageCallback = null;
private Random randomizer = new Random();
private AtomicBoolean isStopping = new AtomicBoolean(false);
private AtomicBoolean isStarting = new AtomicBoolean(false);
/**
* Time between sending messages (ms)
*/
private int gatewayDispatcherYield = 100;
/**
* Time between polling for new messages (ms)
*/
public int modemPollingInterval = 15;
public Modem(SerialPortManager serialPortManager, String address, int port, String simPin,
ScheduledExecutorService scheduledService, Integer pollingInterval, Integer delayBetweenSend) {
this.gatewayId = address + "-" + port;
this.scheduledService = scheduledService;
this.modemPollingInterval = pollingInterval;
this.gatewayDispatcherYield = delayBetweenSend;
setDescription("GSM Modem " + address + "/" + port);
Capabilities caps = new Capabilities();
caps.set(Caps.CanSendMessage);
caps.set(Caps.CanSendBinaryMessage);
caps.set(Caps.CanSendUnicodeMessage);
caps.set(Caps.CanSendWapMessage);
caps.set(Caps.CanSendFlashMessage);
caps.set(Caps.CanSendPortInfo);
caps.set(Caps.CanSplitMessages);
caps.set(Caps.CanRequestDeliveryStatus);
setCapabilities(caps);
if (isPortAnIpAddress(address)) {
this.modemDriver = new IPModemDriver(this, address, port);
} else {
this.modemDriver = new JSerialModemDriver(serialPortManager, this, address, port);
}
this.simPin = simPin;
this.smscNumber = new MsIsdn();
this.readMessagesSet = new HashSet<>();
this.messageSender = new MessageSender(String.format("Gateway Dispatcher 1 [%s]", this.gatewayId), messageQueue,
this, gatewayDispatcherYield);
}
final public boolean start() {
if (!isStarting.getAndSet(true)) {
this.startAndStoplock.lock();
try {
if ((getStatus() == Status.Stopped) || (getStatus() == Status.Error)) {
try {
setStatus(Status.Starting);
logger.debug("Starting gateway: {}", toShortString());
this.modemDriver.lock();
try {
this.modemDriver.openPort();
this.modemDriver.initializeModem();
ScheduledFuture<?> messageReaderFinal = this.messageReader;
if (messageReaderFinal != null) {
messageReaderFinal.cancel(true);
}
this.messageReader = scheduledService.scheduleWithFixedDelay(new MessageReader(this), 15,
modemPollingInterval, TimeUnit.SECONDS);
this.modemDriver.refreshRssi();
this.messageSender = new MessageSender(
String.format("Gateway Dispatcher 1 [%s]", this.gatewayId), messageQueue, this,
gatewayDispatcherYield);
startSendingQueue();
if (logger.isDebugEnabled()) {
logger.debug("Gateway: {}: {}, SL:{}, SIG: {} / {}", toShortString(),
getDeviceInformation().toString(), this.modemDriver.getMemoryLocations(),
this.modemDriver.getSignature(true), this.modemDriver.getSignature(false));
}
} finally {
this.modemDriver.unlock();
}
setStatus(Status.Started);
} catch (CommunicationException e) {
logger.error("Communication exception when trying to start", e);
try {
stop();
} finally {
setStatus(Status.Error);
}
}
}
} finally {
this.startAndStoplock.unlock();
this.isStarting.set(false);
}
}
return (getStatus() == Status.Started);
}
final public boolean stop() {
if (!isStopping.getAndSet(true)) {
this.startAndStoplock.lock();
try {
if ((getStatus() == Status.Started) || (getStatus() == Status.Error)) {
setStatus(Status.Stopping);
logger.debug("Stopping gateway: {}", toShortString());
if (messageSender.isRunning()) {
this.messageSender.setInterrupt();
}
logger.warn("Gateway stopping, message not delivered : {}", this.messageQueue.size());
ScheduledFuture<?> messageReaderFinal = this.messageReader;
if (messageReaderFinal != null) {
messageReaderFinal.cancel(true);
}
this.modemDriver.lock();
try {
this.modemDriver.closePort();
} finally {
this.modemDriver.unlock();
}
setStatus(Status.Stopped);
}
} finally {
this.startAndStoplock.unlock();
isStopping.set(false);
}
}
return (getStatus() == Status.Stopped);
}
final public void error() {
this.stop();
this.status = Status.Error;
}
final public boolean send(OutboundMessage message) throws CommunicationException {
try {
if (getStatus() != Status.Started) {
logger.debug("Outbound message routed via non-started gateway: {} ({})", message.toShortString(),
getStatus());
return false;
}
this.modemDriver.lock();
try {
if (getDeviceInformation().getMode() == Modes.PDU) {
List<String> pdus = message.getPdus(getSmscNumber(), getNextMultipartReferenceNo());
for (String pdu : pdus) {
int j = pdu.length() / 2 - 1;
int refNo = this.modemDriver.atSendPDUMessage(j, pdu);
if (refNo >= 0) {
message.setGatewayId(getGatewayId());
message.setSentDate(new Date());
message.getOperatorMessageIds().add(String.valueOf(refNo));
message.setSentStatus(SentStatus.Sent);
message.setFailureCause(FailureCause.None);
} else {
message.setSentStatus(SentStatus.Failed);
message.setFailureCause(FailureCause.GatewayFailure);
}
}
} else {
MsIsdn recipientAddress = message.getRecipientAddress();
Payload payload = message.getPayload();
if (recipientAddress == null) {
throw new IllegalArgumentException("Recipient is null");
}
String text = payload.getText();
if (payload.getType() == Type.Binary || text == null) {
throw new IllegalArgumentException("Cannot send sms in binary format");
}
int refNo = this.modemDriver.atSendTEXTMessage(recipientAddress.getAddress(), text);
if (refNo >= 0) {
message.setGatewayId(getGatewayId());
message.setSentDate(new Date());
message.getOperatorMessageIds().add(String.valueOf(refNo));
message.setSentStatus(SentStatus.Sent);
message.setFailureCause(FailureCause.None);
} else {
message.setSentStatus(SentStatus.Failed);
message.setFailureCause(FailureCause.GatewayFailure);
}
}
if (message.getSentStatus() == SentStatus.Sent) {
getDeviceInformation().increaseTotalSent();
} else {
getDeviceInformation().increaseTotalFailed();
}
} finally {
this.modemDriver.unlock();
}
return message.getSentStatus() == SentStatus.Sent;
} catch (CommunicationException e) {
getDeviceInformation().increaseTotalFailures();
throw e;
}
}
final public boolean delete(InboundMessage message) throws CommunicationException {
if (getStatus() != Status.Started) {
if (logger.isDebugEnabled()) {
logger.debug("Delete message via non-started gateway: {} ({})", message.toShortString(), getStatus());
}
return false;
}
this.modemDriver.lock();
try {
this.readMessagesSet.remove(message.getSignature());
if (message.getMemIndex() >= 0) {
return this.modemDriver.atDeleteMessage(message.getMemLocation(), message.getMemIndex()).isResponseOk();
}
if ((message.getMemIndex() == -1) && (message.getMpMemIndex().length() > 0)) {
StringTokenizer tokens = new StringTokenizer(message.getMpMemIndex(), ",");
while (tokens.hasMoreTokens()) {
this.modemDriver.atDeleteMessage(message.getMemLocation(), Integer.valueOf(tokens.nextToken()));
}
return true;
}
return false;
} finally {
this.modemDriver.unlock();
}
}
public boolean queue(OutboundMessage message) {
if (logger.isDebugEnabled()) {
logger.debug("Queue: {}", message.toShortString());
}
boolean added = messageQueue.add(message);
IInboundOutboundMessageListener messageCallbackFinal = messageCallback;
if (messageCallbackFinal != null) {
messageCallbackFinal.messageSent(message);
}
startSendingQueue();
return added;
}
private void startSendingQueue() {
if (messageQueue.size() > 0 && (!this.messageSender.isRunning())) {
this.scheduledService.execute(messageSender);
}
}
public DeviceInformation getDeviceInformation() {
return this.deviceInformation;
}
public AbstractModemDriver getModemDriver() {
return this.modemDriver;
}
public String getSimPin() {
return this.simPin;
}
public MsIsdn getSmscNumber() {
return this.smscNumber;
}
public void setSmscNumber(MsIsdn smscNumber) {
this.smscNumber = smscNumber;
}
public HashSet<String> getReadMessagesSet() {
return this.readMessagesSet;
}
private void setStatus(Status status) {
Status oldStatus = this.status;
this.status = status;
Status newStatus = this.status;
IModemStatusListener modemStatusCallbackFinal = modemStatusCallback;
if (modemStatusCallbackFinal != null) {
modemStatusCallbackFinal.processStatusCallback(oldStatus, newStatus);
}
}
protected int getNextMultipartReferenceNo() {
if (this.multipartReferenceNo == 0) {
this.multipartReferenceNo = this.randomizer.nextInt();
if (this.multipartReferenceNo < 0) {
this.multipartReferenceNo *= -1;
}
this.multipartReferenceNo %= 65536;
}
this.multipartReferenceNo = (this.multipartReferenceNo + 1) % 65536;
return this.multipartReferenceNo;
}
@Override
public String toString() {
StringBuffer b = new StringBuffer(1024);
b.append("== GATEWAY ========================================================================%n");
b.append(String.format("Gateway ID: %s%n", getGatewayId()));
b.append(String.format("-- Capabilities --%n"));
b.append(capabilities.toString());
b.append(String.format("-- Settings --%n"));
b.append("== GATEWAY END ========================================================================%n");
return b.toString();
}
public String toShortString() {
return getGatewayId() + String.format(" [%s]", this.modemDriver.getPortInfo());
}
private boolean isPortAnIpAddress(String address) {
try {
InetAddress.getByName(address);
return true;
} catch (UnknownHostException e) {
return false;
}
}
public void registerStatusListener(@Nullable IModemStatusListener smsModemStatusCallback) {
this.modemStatusCallback = smsModemStatusCallback;
}
public void registerMessageListener(@Nullable IInboundOutboundMessageListener messageCallback) {
this.messageCallback = messageCallback;
}
public void registerInformationListener(@Nullable IDeviceInformationListener deviceInformationListener) {
this.deviceInformation.setDeviceInformationListener(deviceInformationListener);
}
public void processMessage(InboundMessage message) {
IInboundOutboundMessageListener messageCallbackFinal = this.messageCallback;
if (messageCallbackFinal != null) {
messageCallbackFinal.messageReceived(message);
}
}
public void processMessageSent(OutboundMessage message) {
IInboundOutboundMessageListener messageCallbackFinal = this.messageCallback;
if (messageCallbackFinal != null) {
messageCallbackFinal.messageSent(message);
}
}
public void processDeliveryReport(DeliveryReportMessage message) {
IInboundOutboundMessageListener messageCallbackFinal = this.messageCallback;
if (messageCallbackFinal != null) {
messageCallbackFinal.messageDelivered(message);
}
}
public Status getStatus() {
return this.status;
}
public final String getGatewayId() {
return this.gatewayId;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public void setCapabilities(Capabilities capabilities) {
this.capabilities = capabilities;
}
}

View File

@@ -0,0 +1,26 @@
package org.smslib;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class ModemResponse {
String responseData;
boolean responseOk;
public ModemResponse(String responseData, boolean responseOk) {
this.responseData = responseData;
this.responseOk = responseOk;
}
public String getResponseData() {
return this.responseData;
}
public boolean isResponseOk() {
return this.responseOk;
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.smslib;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
*
* Exception class for internal SMSLib unrecoverable error
*
* @author Gwendal ROULLEAU - Initial contribution
*/
@NonNullByDefault
public class UnrecoverableSmslibException extends RuntimeException {
private static final long serialVersionUID = 7649578885702261759L;
public UnrecoverableSmslibException(String message) {
super(message);
}
public UnrecoverableSmslibException(String message, Exception cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,34 @@
package org.smslib.callback;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link IDeviceInformationListener} will receive informations
* and statistics
* Extracted from SMSLib
*/
@NonNullByDefault
public interface IDeviceInformationListener {
void setManufacturer(String manufacturer);
void setModel(String string);
void setSwVersion(String swVersion);
void setSerialNo(String serialNo);
void setImsi(String imsi);
void setRssi(String rssi);
void setMode(String mode);
public void setTotalSent(String totalSent);
public void setTotalFailed(String totalFailed);
public void setTotalReceived(String totalReceived);
public void setTotalFailures(String totalFailure);
}

View File

@@ -0,0 +1,39 @@
package org.smslib.callback;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.smslib.message.DeliveryReportMessage;
import org.smslib.message.InboundMessage;
import org.smslib.message.OutboundMessage;
/**
*
* Interface to implement to get messages and reports
*
* Extracted from SMSLib
*/
@NonNullByDefault
public interface IInboundOutboundMessageListener {
/**
* Implement this method to get incoming messages
*
* @param message The inbound message received
*/
public void messageReceived(InboundMessage message);
/**
* Implement this method to get warned when
* a message is sent on the network
*
* @param message the message sent
*/
public void messageSent(OutboundMessage message);
/**
* Implement this method to get warned when
* a message previously sent is received by the recipient
*
* @param message the delivery report message
*/
public void messageDelivered(DeliveryReportMessage message);
}

View File

@@ -0,0 +1,15 @@
package org.smslib.callback;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.smslib.Modem.Status;
/**
* Implement this interface to get status change
*
* Extracted from SMSLib
*/
@NonNullByDefault
public interface IModemStatusListener {
boolean processStatusCallback(Status oldStatus, Status newStatus);
}

View File

@@ -0,0 +1,540 @@
package org.smslib.driver;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.Capabilities;
import org.smslib.CommunicationException;
import org.smslib.Capabilities.Caps;
import org.smslib.DeviceInformation.Modes;
import org.smslib.Modem;
import org.smslib.ModemResponse;
import org.smslib.UnrecoverableSmslibException;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public abstract class AbstractModemDriver {
static Logger logger = LoggerFactory.getLogger(AbstractModemDriver.class);
private Lock lock = new ReentrantLock();
Properties modemProperties;
@NonNullByDefault({})
InputStream in;
@NonNullByDefault({})
OutputStream out;
StringBuffer buffer = new StringBuffer(4096);
PollReader pollReader = new PollReader(this, "undefined");
Modem modem;
boolean responseOk;
String memoryLocations = "";
int atATHCounter = 0;
public abstract void openPort() throws CommunicationException;
public abstract void closePort();
public abstract String getPortInfo();
public AbstractModemDriver(Modem modem) {
modemProperties = new Properties();
try {
ClassLoader classLoader = this.getClass().getClassLoader();
if (classLoader != null) {
try (InputStream inputStream = classLoader.getResourceAsStream("modem.properties")) {
modemProperties.load(inputStream);
}
}
} catch (IOException e) {
throw new UnrecoverableSmslibException("Cannot instantiate modem driver", e);
}
this.modem = modem;
}
public ModemResponse write(String data) throws CommunicationException {
return write(data, false);
}
public ModemResponse write(String data, boolean skipResponse) throws CommunicationException {
this.lock.lock();
try {
logger.debug("{} <== {}", getPortInfo(), data);
write(data.getBytes());
countSheeps(Integer.valueOf(getModemSettings("command_wait_unit")));
return (new ModemResponse((skipResponse ? "" : getResponse()), (skipResponse ? true : this.responseOk)));
} finally {
this.lock.unlock();
}
}
protected boolean hasData() throws IOException {
return ((this.in != null) && (this.in.available() > 0));
}
protected int read() throws IOException {
return this.in.read();
}
protected void write(byte[] s) throws CommunicationException {
int charDelay = Integer.valueOf(getModemSettings("char_wait_unit"));
try {
if (charDelay == 0) {
this.out.write(s);
} else {
for (int i = 0; i < s.length; i++) {
byte b = s[i];
this.out.write(b);
countSheeps(charDelay);
}
}
} catch (IOException e) {
throw new CommunicationException("Cannot write to device", e);
}
}
protected void write(byte s) throws CommunicationException {
try {
this.out.write(s);
} catch (IOException e) {
throw new CommunicationException("Cannot write data", e);
}
}
private String getResponse() throws CommunicationException {
StringBuffer raw = new StringBuffer(256);
StringBuffer b = new StringBuffer(256);
try {
while (true) {
String line = getLineFromBuffer();
logger.debug("{} >>> {}", getPortInfo(), line);
this.buffer.delete(0, line.length() + 2);
if (line.isBlank()) {
continue;
}
if (line.charAt(0) == '^') {
continue;
}
if (line.charAt(0) == '*') {
continue;
}
if (line.startsWith("RING")) {
continue;
}
if (line.startsWith("+STIN:")) {
continue;
}
if (Integer.valueOf(getModemSettings("cpin_without_ok")) == 1) {
if (line.startsWith("+CPIN:")) {
raw.append(line);
raw.append("$");
b.append(line);
this.responseOk = true;
break;
}
}
if (line.startsWith("+CLIP:")) {
write("+++", true);
countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
write("ATH\r", true);
logger.debug("+++ INCREASE ATH");
this.atATHCounter++;
// no need for a call handler. discard
countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
continue;
}
if (line.indexOf("OK") == 0) {
if (this.atATHCounter > 0) {
logger.debug("--- DECREASE ATH");
this.atATHCounter--;
continue;
}
this.responseOk = true;
break;
}
if ((line.indexOf("ERROR") == 0) || (line.indexOf("+CMS ERROR") == 0)
|| (line.indexOf("+CME ERROR") == 0)) {
logger.warn("{} ERR==> {}", getPortInfo(), line);
this.responseOk = false;
break;
}
if (b.length() > 0) {
b.append('\n');
}
raw.append(line);
raw.append("$");
b.append(line);
}
} catch (IOException | TimeoutException e) {
throw new CommunicationException("Cannot get response", e);
}
logger.debug("{} ==> {}", getPortInfo(), raw.toString());
return b.toString();
}
private String getLineFromBuffer() throws TimeoutException, IOException {
long startTimeout = System.currentTimeMillis();
long endTimeout = startTimeout;
while (this.buffer.indexOf("\r") == -1) {
endTimeout += Integer.valueOf(getModemSettings("wait_unit"));
if ((endTimeout - startTimeout) > Integer.valueOf(getModemSettings("timeout"))) {
throw new TimeoutException("Timeout elapsed for " + getPortInfo());
}
countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
}
BufferedReader r = new BufferedReader(new StringReader(this.buffer.toString()));
String line = r.readLine();
r.close();
return line;
}
public void clearResponses() {
countSheeps(Integer.valueOf(getModemSettings("wait_unit")) * 1);
while (this.buffer.length() > 0) {
this.buffer.delete(0, this.buffer.length());
countSheeps(Integer.valueOf(getModemSettings("wait_unit")) * 1);
}
}
public String getMemoryLocations() {
return this.memoryLocations;
}
public void initializeModem() throws CommunicationException {
int counter = 0;
this.lock.lock();
try {
atAT();
atAT();
atAT();
atAT();
atEchoOff();
clearResponses();
this.modem.getDeviceInformation().setManufacturer(atGetManufacturer().getResponseData());
this.modem.getDeviceInformation().setModel(atGetModel().getResponseData());
countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
atFromModemSettings("init1");
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_after_init1")));
atFromModemSettings("init2");
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_after_init2")));
clearResponses();
atEchoOff();
clearResponses();
atFromModemSettings("pre_pin");
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_after_pre_pin")));
while (true) {
counter++;
if (counter == 5) {
throw new CommunicationException("Modem does not correspond correctly, giving up...");
}
ModemResponse simStatus = atGetSimStatus();
if (simStatus.getResponseData().indexOf("SIM PIN") >= 0) {
if (this.modem.getSimPin().isBlank()) {
throw new CommunicationException("SIM PIN requested but not defined!");
}
atEnterPin(this.modem.getSimPin());
} else if (simStatus.getResponseData().indexOf("READY") >= 0) {
break;
} else if (simStatus.getResponseData().indexOf("OK") >= 0) {
break;
} else if (simStatus.getResponseData().indexOf("ERROR") >= 0) {
logger.error("SIM PIN error!");
}
logger.debug("SIM PIN Not ok, waiting for a while...");
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_on_sim_error")));
}
atFromModemSettings("post_pin");
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_after_post_pin")));
atEnableClip();
if (!atNetworkRegistration().isResponseOk()) {
throw new CommunicationException("Network registration failed!");
}
atVerboseOff();
if (atSetPDUMode().isResponseOk()) {
this.modem.getDeviceInformation().setMode(Modes.PDU);
} else {
logger.debug("Modem does not support PDU, trying to switch to TEXT...");
if (atSetTEXTMode().isResponseOk()) {
Capabilities caps = new Capabilities();
caps.set(Caps.CanSendMessage);
this.modem.setCapabilities(caps);
this.modem.getDeviceInformation().setMode(Modes.TEXT);
} else {
throw new CommunicationException("Neither PDU nor TEXT mode are supported by this modem!");
}
}
atCnmiOff();
retrieveMemoryLocations();
refreshDeviceInformation();
} finally {
this.lock.unlock();
}
}
public void refreshDeviceInformation() throws CommunicationException {
this.modem.getDeviceInformation().setManufacturer(atGetManufacturer().getResponseData());
this.modem.getDeviceInformation().setModel(atGetModel().getResponseData());
this.modem.getDeviceInformation().setSerialNo(atGetSerialNo().getResponseData());
this.modem.getDeviceInformation().setImsi(atGetImsi().getResponseData());
this.modem.getDeviceInformation().setSwVersion(atGetSWVersion().getResponseData());
this.refreshRssi();
}
public void refreshRssi() throws CommunicationException {
String s = atGetSignalStrengh().getResponseData();
if (this.responseOk) {
String s1 = s.split("\\R")[0]; // ensure to get first line only
s1 = s1.substring(s.indexOf(':') + 1).trim();
StringTokenizer tokens = new StringTokenizer(s1, ",");
int rssi = Integer.valueOf(tokens.nextToken().trim());
this.modem.getDeviceInformation().setRssi(rssi == 99 ? 99 : (-113 + 2 * rssi));
}
}
void retrieveMemoryLocations() throws CommunicationException {
if (this.memoryLocations.isBlank()) {
this.memoryLocations = getModemSettings("memory_locations");
if (this.memoryLocations.isBlank()) {
this.memoryLocations = "";
}
if (this.memoryLocations.isBlank()) {
try {
String response = atGetMemoryLocations().getResponseData();
if (response.indexOf("+CPMS:") >= 0) {
int i, j;
i = response.indexOf('(');
while (response.charAt(i) == '(') {
i++;
}
j = i;
while (response.charAt(j) != ')') {
j++;
}
response = response.substring(i, j);
StringTokenizer tokens = new StringTokenizer(response, ",");
while (tokens.hasMoreTokens()) {
String loc = tokens.nextToken().replaceAll("\"", "");
if (!"MT".equalsIgnoreCase(loc) && this.memoryLocations.indexOf(loc) < 0) {
this.memoryLocations += loc;
}
}
} else {
this.memoryLocations = "SM";
logger.debug("CPMS detection failed, proceeding with default memory 'SM'.");
}
} catch (CommunicationException e) {
this.memoryLocations = "SM";
logger.debug("CPMS detection failed, proceeding with default memory 'SM'.", e);
}
}
} else {
logger.debug("Using given memory locations: {}", this.memoryLocations);
}
}
public String getSignature(boolean complete) {
String manufacturer = this.modem.getDeviceInformation().getManufacturer().toLowerCase().replaceAll(" ", "")
.replaceAll(" ", "").replaceAll(" ", "");
String model = this.modem.getDeviceInformation().getModel().toLowerCase().replaceAll(" ", "")
.replaceAll(" ", "").replaceAll(" ", "");
return (complete ? manufacturer + "_" + model : manufacturer);
}
protected ModemResponse atAT() throws CommunicationException {
return write("AT\r", true);
}
protected ModemResponse atATWithResponse() throws CommunicationException {
return write("AT\r");
}
protected ModemResponse atEchoOff() throws CommunicationException {
return write("ATE0\r", true);
}
protected ModemResponse atGetSimStatus() throws CommunicationException {
return write("AT+CPIN?\r");
}
protected ModemResponse atEnterPin(String pin) throws CommunicationException {
return write(String.format("AT+CPIN=\"%s\"\r", pin));
}
protected ModemResponse atNetworkRegistration() throws CommunicationException {
write("AT+CREG=1\r");
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_network_registration")));
return write("AT+CREG?\r");
}
protected ModemResponse atEnableClip() throws CommunicationException {
return write("AT+CLIP=1\r");
}
protected ModemResponse atVerboseOff() throws CommunicationException {
return write("AT+CMEE=0\r");
}
protected ModemResponse atSetPDUMode() throws CommunicationException {
return write("AT+CMGF=0\r");
}
protected ModemResponse atSetTEXTMode() throws CommunicationException {
return write("AT+CMGF=1\r");
}
protected ModemResponse atCnmiOff() throws CommunicationException {
return write("AT+CNMI=2,0,0,0,0\r");
}
protected ModemResponse atGetManufacturer() throws CommunicationException {
return write("AT+CGMI\r");
}
protected ModemResponse atGetModel() throws CommunicationException {
return write("AT+CGMM\r");
}
protected ModemResponse atGetImsi() throws CommunicationException {
return write("AT+CIMI\r");
}
protected ModemResponse atGetSerialNo() throws CommunicationException {
return write("AT+CGSN\r");
}
protected ModemResponse atGetSWVersion() throws CommunicationException {
return write("AT+CGMR\r");
}
protected ModemResponse atGetSignalStrengh() throws CommunicationException {
return write("AT+CSQ\r");
}
public int atSendPDUMessage(int size, String pdu) throws CommunicationException {
write(String.format("AT+CMGS=%d\r", size), true);
while (this.buffer.length() == 0) {
countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
}
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_before_send_pdu")));
clearResponses();
write(pdu, true);
write((byte) 26);
String response = getResponse();
if (this.responseOk && response.contains(":")) {
return Integer.parseInt(response.substring(response.indexOf(":") + 1).trim());
}
return -1;
}
public int atSendTEXTMessage(String recipient, String text) throws CommunicationException {
write(String.format("AT+CSCS=\"%s\"\r", "UTF-8"), true);
if (!this.responseOk) {
throw new CommunicationException("Unsupported encoding: UTF-8");
}
write(String.format("AT+CMGS=\"%s\"\r", recipient), true);
while (this.buffer.length() == 0) {
countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
}
countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
* Integer.valueOf(getModemSettings("delay_before_send_pdu")));
clearResponses();
write(text, true);
write((byte) 26);
String response = getResponse();
if (this.responseOk) {
return Integer.parseInt(response.substring(response.indexOf(":") + 1).trim());
}
return -1;
}
public ModemResponse atGetMemoryLocations() throws CommunicationException {
return write("AT+CPMS=?\r");
}
public ModemResponse atSwitchMemoryLocation(String memoryLocation) throws CommunicationException {
return write(String.format("AT+CPMS=\"%s\"\r", memoryLocation));
}
public ModemResponse atGetMessages(String memoryLocation) throws CommunicationException {
if (atSwitchMemoryLocation(memoryLocation).isResponseOk()) {
return (this.modem.getDeviceInformation().getMode() == Modes.PDU ? write("AT+CMGL=4\r")
: write("AT+CMGL=\"ALL\"\r"));
}
return new ModemResponse("", false);
}
public ModemResponse atDeleteMessage(String memoryLocation, int memoryIndex) throws CommunicationException {
if (atSwitchMemoryLocation(memoryLocation).isResponseOk()) {
return write(String.format("AT+CMGD=%d\r", memoryIndex));
}
return new ModemResponse("", false);
}
public ModemResponse atFromModemSettings(String key) throws CommunicationException {
String atCommand = getModemSettings(key);
if (!atCommand.isBlank()) {
return write(atCommand);
}
return new ModemResponse("", true);
}
public String getModemSettings(String key) {
String fullSignature = getSignature(true);
String shortSignature = getSignature(false);
String value = "";
if (!fullSignature.isBlank()) {
value = modemProperties.getProperty(fullSignature + "." + key);
}
if ((value == null || value.isBlank()) && !shortSignature.isBlank()) {
value = modemProperties.getProperty(shortSignature + "." + key);
}
if (value == null || value.isBlank()) {
value = modemProperties.getProperty("default" + "." + key);
}
return ((value == null || value.isBlank()) ? "" : value);
}
public void lock() {
this.lock.lock();
}
public void unlock() {
this.lock.unlock();
}
protected static void countSheeps(int n) {
try {
Thread.sleep(n);
} catch (InterruptedException e) {
// Nothing here...
}
}
}

View File

@@ -0,0 +1,85 @@
package org.smslib.driver;
import java.io.IOException;
import java.net.Socket;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.CommunicationException;
import org.smslib.Modem;
/**
* Extracted from SMSLib
* Manage communication with ser2net (or equivalent)
*
* Extracted from SMSLib
*/
@NonNullByDefault
public class IPModemDriver extends AbstractModemDriver {
static Logger logger = LoggerFactory.getLogger(IPModemDriver.class);
String address;
int port;
@Nullable
Socket socket;
public IPModemDriver(Modem modem, String address, int port) {
super(modem);
this.address = address;
this.port = port;
}
@Override
public void openPort() throws CommunicationException {
logger.debug("Opening IP port: {}", getPortInfo());
try {
Socket openSocket = new Socket(this.address, this.port);
openSocket.setReceiveBufferSize(Integer.valueOf(getModemSettings("port_buffer")));
openSocket.setSendBufferSize(Integer.valueOf(getModemSettings("port_buffer")));
openSocket.setSoTimeout(30000);
openSocket.setTcpNoDelay(true);
this.in = openSocket.getInputStream();
this.out = openSocket.getOutputStream();
this.socket = openSocket;
} catch (IOException e) {
throw new CommunicationException("Cannot open port", e);
}
countSheeps(Integer.valueOf(getModemSettings("after_ip_connect_wait_unit")));
this.pollReader = new PollReader(this, getPortInfo());
this.pollReader.setDaemon(true);
this.pollReader.start();
}
@Override
public void closePort() {
logger.debug("Closing IP port: {}", getPortInfo());
try {
this.pollReader.cancel();
this.pollReader.join();
if (in != null) {
this.in.close();
this.in = null;
}
if (out != null) {
this.out.close();
this.out = null;
}
Socket finalSocket = socket;
if (finalSocket != null) {
finalSocket.close();
}
} catch (InterruptedException | IOException e) {
logger.debug("Cannot close port");
}
countSheeps(Integer.valueOf(getModemSettings("after_ip_connect_wait_unit")));
}
@Override
public String getPortInfo() {
return this.address + ":" + this.port;
}
}

View File

@@ -0,0 +1,101 @@
package org.smslib.driver;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.CommunicationException;
import org.smslib.Modem;
/**
* Manage communications with a serial modem
* Extracted from SMSLib
*/
@NonNullByDefault
public class JSerialModemDriver extends AbstractModemDriver {
private static final int ONE_STOP_BIT = 1;
static final public int NO_PARITY = 0;
static final public int FLOW_CONTROL_RTS_ENABLED = 0x00000001;
static final public int FLOW_CONTROL_CTS_ENABLED = 0x00000010;
static Logger logger = LoggerFactory.getLogger(JSerialModemDriver.class);
String portName;
int baudRate;
private final SerialPortManager serialPortManager;
@Nullable
SerialPort serialPort;
public JSerialModemDriver(SerialPortManager serialPortManager, Modem modem, String port, int baudRate) {
super(modem);
this.portName = port;
this.baudRate = baudRate;
this.serialPortManager = serialPortManager;
}
@Override
public void openPort() throws CommunicationException {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(portName);
if (portIdentifier == null) {
throw new CommunicationException("SMSModem cannot use serial port " + portName);
}
try {
SerialPort openedSerialPort = portIdentifier.open("org.openhab.binding.smsmodem", 2000);
openedSerialPort.setSerialPortParams(baudRate, 8, ONE_STOP_BIT, NO_PARITY);
openedSerialPort.setFlowControlMode(FLOW_CONTROL_RTS_ENABLED | FLOW_CONTROL_CTS_ENABLED);
this.in = openedSerialPort.getInputStream();
this.out = openedSerialPort.getOutputStream();
serialPort = openedSerialPort;
this.pollReader = new PollReader(this, getPortInfo());
this.pollReader.setDaemon(true);
this.pollReader.start();
} catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
throw new CommunicationException("Cannot open port", e);
}
}
@Override
public void closePort() {
try {
logger.debug("Closing comm port: {}", getPortInfo());
this.pollReader.cancel();
try {
this.pollReader.join();
} catch (InterruptedException ex) {
logger.debug("PollReader closing exception", ex);
}
if (in != null) {
this.in.close();
this.in = null;
}
if (out != null) {
this.out.close();
this.out = null;
}
final SerialPort finalSerialPort = serialPort;
if (finalSerialPort != null) {
finalSerialPort.close();
serialPort = null;
}
} catch (IOException e) {
logger.debug("Closing port exception", e);
}
}
@Override
public String getPortInfo() {
return this.portName + ":" + this.baudRate;
}
}

View File

@@ -0,0 +1,75 @@
package org.smslib.driver;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.CommunicationException;
/**
* Manage communications with a serial modem
* Extracted from SMSLib
*/
public class PollReader extends Thread {
static Logger logger = LoggerFactory.getLogger(AbstractModemDriver.class);
private boolean shouldCancel = false;
private boolean foundClip = false;
public PollReader(AbstractModemDriver modemDriver, String threadId) {
super();
this.modemDriver = modemDriver;
this.threadId = threadId;
}
private AbstractModemDriver modemDriver;
private String threadId;
public void cancel() {
this.shouldCancel = true;
this.interrupt();
}
@Override
public void run() {
logger.debug("Started!");
currentThread().setName("OH-binding-smsmodem-" + threadId);
while (!this.shouldCancel) {
try {
while (modemDriver.hasData()) {
char c = (char) modemDriver.read();
modemDriver.buffer.append(c);
if (modemDriver.buffer.indexOf("+CLIP") >= 0) {
if (!this.foundClip) {
this.foundClip = true;
new ClipReader().start();
}
} else {
this.foundClip = false;
}
}
} catch (IOException e) {
logger.debug("Cannot proceed to poll device", e);
modemDriver.modem.error();
}
AbstractModemDriver.countSheeps(Integer.valueOf(modemDriver.getModemSettings("poll_reader")));
}
logger.debug("Stopped!");
}
public class ClipReader extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
modemDriver.atATWithResponse();
} catch (InterruptedException | CommunicationException e) {
logger.debug("Cannot proceed to read clip", e);
}
}
}
}

View File

@@ -0,0 +1,226 @@
package org.smslib.message;
import java.io.Serializable;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.smslib.UnrecoverableSmslibException;
import org.smslib.message.OutboundMessage.SentStatus;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public abstract class AbstractMessage implements Serializable {
public enum Encoding {
Enc7,
Enc8,
EncUcs2,
EncCustom;
}
public enum DcsClass {
None,
Flash,
Me,
Sim,
Te
}
public enum Type {
Inbound,
Outbound,
StatusReport
}
private static final long serialVersionUID = 1L;
Date creationDate = new Date();
String id = UUID.randomUUID().toString();
MsIsdn originatorAddress = new MsIsdn();
@Nullable
MsIsdn recipientAddress = new MsIsdn();
Payload payload = new Payload("");
Type type = Type.Inbound;
Encoding encoding = Encoding.Enc7;
DcsClass dcsClass = DcsClass.Sim;
String gatewayId = "";
int sourcePort = -1;
int destinationPort = -1;
@Nullable
Date sentDate;
public AbstractMessage() {
}
public AbstractMessage(Type type, MsIsdn originatorAddress, @Nullable MsIsdn recipientAddress,
@Nullable Payload payload) {
this.type = type;
this.originatorAddress = originatorAddress;
this.recipientAddress = recipientAddress;
if (payload != null) {
setPayload(payload);
}
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public Date getCreationDate() {
return this.creationDate;
}
public MsIsdn getOriginatorAddress() {
return this.originatorAddress;
}
public @Nullable MsIsdn getRecipientAddress() {
return this.recipientAddress;
}
public Payload getPayload() {
return this.payload;
}
public void setPayload(Payload payload) {
this.payload = payload;
}
public Type getType() {
return this.type;
}
public Encoding getEncoding() {
return this.encoding;
}
public void setEncoding(Encoding encoding) {
this.encoding = encoding;
}
public DcsClass getDcsClass() {
return this.dcsClass;
}
public void setDcsClass(DcsClass dcsClass) {
this.dcsClass = dcsClass;
}
public String getGatewayId() {
return this.gatewayId;
}
public void setGatewayId(String gatewayId) {
this.gatewayId = gatewayId;
}
public int getSourcePort() {
return this.sourcePort;
}
public void setSourcePort(int sourcePort) {
this.sourcePort = sourcePort;
}
public int getDestinationPort() {
return this.destinationPort;
}
public void setDestinationPort(int destinationPort) {
this.destinationPort = destinationPort;
}
public @Nullable Date getSentDate() {
Date sentDateFinal = this.sentDate;
return (sentDateFinal != null ? (Date) sentDateFinal.clone() : null);
}
public void setSentDate(Date sentDate) {
this.sentDate = new Date(sentDate.getTime());
}
public abstract String getSignature();
public abstract String toShortString();
public String hashSignature(String s) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(s.getBytes(), 0, s.length());
BigInteger i = new BigInteger(1, md.digest());
return String.format("%1$032x", i);
} catch (NoSuchAlgorithmException e) {
throw new UnrecoverableSmslibException("Cannot find hash algorithm", e);
}
}
@Override
public String toString() {
StringBuffer b = new StringBuffer(1024);
b.append(String
.format("%n== MESSAGE START ======================================================================%n"));
b.append(String.format("CLASS: %s%n", this.getClass().toString()));
b.append(String.format("Message ID: %s%n", getId()));
b.append(String.format("Message Signature: %s%n", getSignature()));
b.append(String.format("Via Gateway: %s%n", getGatewayId()));
b.append(String.format("Creation Date: %s%n", getCreationDate()));
b.append(String.format("Type: %s%n", getType()));
b.append(String.format("Encoding: %s%n", getEncoding()));
b.append(String.format("DCS Class: %s%n", getDcsClass()));
b.append(String.format("Source Port: %s%n", getSourcePort()));
b.append(String.format("Destination Port: %s%n", getDestinationPort()));
b.append(String.format("Originator Address: %s%n", getOriginatorAddress()));
b.append(String.format("Recipient Address: %s%n", getRecipientAddress()));
b.append(String.format("Payload Type: %s%n", payload.getType()));
b.append(String.format("Text payload: %s%n", payload.getText() == null ? "null" : payload.getText()));
if (this instanceof InboundMessage) {
b.append(String.format("Sent Date: %s%n", getSentDate()));
b.append(String.format("Memory Storage Location: %s%n", ((InboundMessage) this).getMemLocation()));
b.append(String.format("Memory Index: %d%n", ((InboundMessage) this).getMemIndex()));
b.append(String.format("Memory MP Index: %s%n", ((InboundMessage) this).getMpMemIndex()));
}
if (this instanceof OutboundMessage) {
b.append(String.format("Sent Date: %s%n",
(((OutboundMessage) this).getSentStatus() == SentStatus.Sent ? getSentDate() : "N/A")));
String ids = "";
for (String opId : ((OutboundMessage) this).getOperatorMessageIds()) {
ids += (ids.length() == 0 ? opId : "," + opId);
}
b.append(String.format("Operator Message IDs: %s%n", ids));
b.append(String.format("Status: %s%n", ((OutboundMessage) this).getSentStatus().toString()));
b.append(String.format("Failure: %s%n", ((OutboundMessage) this).getFailureCause().toString()));
b.append(String.format("Request Delivery Reports: %b%n",
((OutboundMessage) this).getRequestDeliveryReport()));
}
if (this instanceof DeliveryReportMessage) {
b.append(String.format("Original Operator Message Id: %s%n",
((DeliveryReportMessage) this).getOriginalOperatorMessageId()));
b.append(String.format("Delivery Date: %s%n", ((DeliveryReportMessage) this).getOriginalReceivedDate()));
b.append(String.format("Delivery Status: %s%n", ((DeliveryReportMessage) this).getDeliveryStatus()));
}
b.append(String
.format("== MESSAGE END ========================================================================%n"));
return b.toString();
}
}

View File

@@ -0,0 +1,122 @@
package org.smslib.message;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.smslib.pduUtils.gsm3040.SmsStatusReportPdu;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class DeliveryReportMessage extends InboundMessage {
private static final long serialVersionUID = 1L;
public enum DeliveryStatus {
Unknown("U"),
Pending("P"),
Failed("F"),
Delivered("D"),
Expired("X"),
Error("E");
private final String shortString;
private DeliveryStatus(String shortString) {
this.shortString = shortString;
}
public String toShortString() {
return this.shortString;
}
}
DeliveryStatus deliveryStatus = DeliveryStatus.Unknown;
@Nullable
String originalOperatorMessageId;
@Nullable
Date originalReceivedDate;
public DeliveryReportMessage() {
super(Type.StatusReport, "", 0);
}
public DeliveryReportMessage(SmsStatusReportPdu pdu, String memLocation, int memIndex) {
super(Type.StatusReport, memLocation, memIndex);
setOriginalOperatorMessageId(String.valueOf(pdu.getMessageReference()));
String address = pdu.getAddress();
if (address == null) {
throw new IllegalArgumentException("Recipient address cannot be null");
}
this.recipientAddress = new MsIsdn(address);
Date timestamp = pdu.getTimestamp();
if (timestamp == null) {
throw new IllegalArgumentException("Cannot get timestamp for delivery report message");
}
setSentDate(timestamp);
Date dischargeTime = pdu.getDischargeTime();
if (dischargeTime == null) {
throw new IllegalArgumentException("Cannot get discharge time for delivery report message");
}
setOriginalReceivedDate(dischargeTime);
int i = pdu.getStatus();
setPayload(new Payload(""));
if ((i & 0x60) == 0) {
this.deliveryStatus = DeliveryStatus.Delivered;
} else if ((i & 0x20) == 0x20) {
this.deliveryStatus = DeliveryStatus.Pending;
} else if ((i & 0x40) == 0x40) {
this.deliveryStatus = DeliveryStatus.Expired;
} else if ((i & 0x60) == 0x60) {
this.deliveryStatus = DeliveryStatus.Expired;
} else {
this.deliveryStatus = DeliveryStatus.Error;
}
}
public DeliveryReportMessage(String messageId, String recipientAddress, String memLocation, int memIndex,
Date originalSentDate, Date receivedDate) {
super(Type.StatusReport, memLocation, memIndex);
setOriginalOperatorMessageId(messageId);
this.recipientAddress = new MsIsdn(recipientAddress);
setSentDate(originalSentDate);
setOriginalReceivedDate(receivedDate);
this.deliveryStatus = DeliveryStatus.Unknown;
}
public DeliveryStatus getDeliveryStatus() {
return this.deliveryStatus;
}
public @Nullable String getOriginalOperatorMessageId() {
return this.originalOperatorMessageId;
}
public void setOriginalOperatorMessageId(String originalOperatorMessageId) {
this.originalOperatorMessageId = originalOperatorMessageId;
}
public @Nullable Date getOriginalReceivedDate() {
Date finalOriginalReceivedDate = originalReceivedDate;
return finalOriginalReceivedDate == null ? null : new Date(finalOriginalReceivedDate.getTime());
}
public void setOriginalReceivedDate(Date originalReceivedDate) {
this.originalReceivedDate = new Date(originalReceivedDate.getTime());
}
@Override
public String getSignature() {
return hashSignature(String.format("%s-%s-%s-%s", getOriginatorAddress(), getOriginalOperatorMessageId(),
getOriginalReceivedDate(), getDeliveryStatus()));
}
@Override
public String toShortString() {
return String.format("[%s @ %s = %s @ %s]", getId(), getRecipientAddress(), getDeliveryStatus(),
getOriginalReceivedDate());
}
}

View File

@@ -0,0 +1,17 @@
package org.smslib.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.smslib.pduUtils.gsm3040.SmsDeliveryPdu;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class InboundBinaryMessage extends InboundMessage {
private static final long serialVersionUID = 1L;
public InboundBinaryMessage(SmsDeliveryPdu pdu, String memLocation, int memIndex) {
super(pdu, memLocation, memIndex);
setPayload(new Payload(pdu.getUserDataAsBytes()));
}
}

View File

@@ -0,0 +1,164 @@
package org.smslib.message;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smslib.pduUtils.gsm3040.PduUtils;
import org.smslib.pduUtils.gsm3040.SmsDeliveryPdu;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class InboundMessage extends AbstractMessage {
static Logger logger = LoggerFactory.getLogger(InboundMessage.class);
private static final long serialVersionUID = 1L;
int memIndex;
String memLocation;
int mpRefNo;
int mpMaxNo;
int mpSeqNo;
String mpMemIndex = "";
@Nullable
MsIsdn smscNumber;
boolean endsWithMultiChar;
public InboundMessage(SmsDeliveryPdu pdu, String memLocation, int memIndex) {
super(Type.Inbound, new MsIsdn(pdu.getAddress()), null, null);
this.memLocation = memLocation;
this.memIndex = memIndex;
this.mpRefNo = 0;
this.mpMaxNo = 0;
this.mpSeqNo = 0;
setMpMemIndex(-1);
int dcsEncoding = PduUtils.extractDcsEncoding(pdu.getDataCodingScheme());
switch (dcsEncoding) {
case PduUtils.DCS_ENCODING_7BIT:
setEncoding(Encoding.Enc7);
break;
case PduUtils.DCS_ENCODING_8BIT:
setEncoding(Encoding.Enc8);
break;
case PduUtils.DCS_ENCODING_UCS2:
setEncoding(Encoding.EncUcs2);
break;
default:
logger.error("Unknown DCS Encoding: {}", dcsEncoding);
}
Date timestamp = pdu.getTimestamp();
if (timestamp != null) {
setSentDate(timestamp);
}
this.smscNumber = new MsIsdn(pdu.getSmscAddress());
setPayload(new Payload(pdu.getDecodedText()));
if (pdu.isConcatMessage()) {
this.mpRefNo = pdu.getMpRefNo();
this.mpMaxNo = pdu.getMpMaxNo();
this.mpSeqNo = pdu.getMpSeqNo();
}
if (pdu.isPortedMessage()) {
setSourcePort(pdu.getSrcPort());
setDestinationPort(pdu.getDestPort());
}
if (getEncoding() == Encoding.Enc7) {
byte[] udData = pdu.getUDData();
if (udData == null) {
throw new IllegalArgumentException("Cannot encode udData to construct message");
}
byte[] temp = PduUtils.encodedSeptetsToUnencodedSeptets(udData);
if (temp.length == 0) {
this.endsWithMultiChar = false;
} else if (temp[temp.length - 1] == 0x1b) {
this.endsWithMultiChar = true;
}
}
}
public InboundMessage(String originator, String text, Date sentDate, String memLocation, int memIndex) {
super(Type.Inbound, new MsIsdn(originator), null, new Payload(text));
this.memLocation = memLocation;
this.memIndex = memIndex;
this.sentDate = new Date(sentDate.getTime());
}
public InboundMessage(Type type, String memLocation, int memIndex) {
super(type, new MsIsdn(), null, null);
this.memIndex = memIndex;
this.memLocation = memLocation;
this.mpRefNo = 0;
this.mpMaxNo = 0;
this.mpSeqNo = 0;
setMpMemIndex(-1);
this.smscNumber = new MsIsdn();
}
public int getMemIndex() {
return this.memIndex;
}
public void setMemIndex(int memIndex) {
this.memIndex = memIndex;
}
public String getMemLocation() {
return this.memLocation;
}
public int getMpMaxNo() {
return this.mpMaxNo;
}
public String getMpMemIndex() {
return this.mpMemIndex;
}
public void setMpMemIndex(int myMpMemIndex) {
if (myMpMemIndex == -1) {
this.mpMemIndex = "";
} else {
this.mpMemIndex += (this.mpMemIndex.length() == 0 ? "" : ",") + myMpMemIndex;
}
}
public int getMpRefNo() {
return this.mpRefNo;
}
public int getMpSeqNo() {
return this.mpSeqNo;
}
public void setMpSeqNo(int myMpSeqNo) {
this.mpSeqNo = myMpSeqNo;
}
public boolean getEndsWithMultiChar() {
return this.endsWithMultiChar;
}
public void setEndsWithMultiChar(boolean b) {
this.endsWithMultiChar = b;
}
@Override
public String getSignature() {
return hashSignature(String.format("%s-%s-%s", getOriginatorAddress(), getSentDate(), payload.getText()));
}
@Override
public String toShortString() {
return String.format("[%s @ %s]", getId(), getOriginatorAddress());
}
}

View File

@@ -0,0 +1,89 @@
package org.smslib.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class MsIsdn {
public enum Type {
National,
International,
Text,
Void
}
String address;
Type type = Type.International;
public MsIsdn() {
this("", Type.Void);
}
public MsIsdn(@Nullable String number) {
if (number == null) {
throw new IllegalArgumentException("Number cannot be null");
}
if (number.length() > 0 && number.charAt(0) == '+') {
this.address = number.substring(1);
this.type = Type.International;
} else {
this.address = number;
this.type = typeOf(number);
}
}
public MsIsdn(String address, Type type) {
this.address = address;
this.type = type;
}
public MsIsdn(MsIsdn msisdn) {
this.type = msisdn.getType();
this.address = msisdn.getAddress();
}
public String getAddress() {
return this.address;
}
public Type getType() {
return this.type;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MsIsdn)) {
return false;
}
return (this.address.equalsIgnoreCase(((MsIsdn) o).getAddress()));
}
@Override
public String toString() {
return String.format("[%s / %s]", getType(), getAddress());
}
@Override
public int hashCode() {
return this.address.hashCode() + (15 * this.type.hashCode());
}
private static Type typeOf(String number) {
if (number.trim().length() == 0) {
return Type.Void;
}
for (int i = 0; i < number.length(); i++) {
if (!Character.isDigit(number.charAt(i))) {
return Type.Text;
}
}
return Type.International;
}
}

View File

@@ -0,0 +1,19 @@
package org.smslib.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class OutboundBinaryMessage extends OutboundMessage {
private static final long serialVersionUID = 1L;
public OutboundBinaryMessage() {
}
public OutboundBinaryMessage(MsIsdn originatorAddress, MsIsdn recipientAddress, byte[] data) {
super(originatorAddress, recipientAddress, new Payload(data));
setEncoding(Encoding.Enc8);
}
}

View File

@@ -0,0 +1,193 @@
package org.smslib.message;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.smslib.UnrecoverableSmslibException;
import org.smslib.pduUtils.gsm3040.PduFactory;
import org.smslib.pduUtils.gsm3040.PduGenerator;
import org.smslib.pduUtils.gsm3040.PduUtils;
import org.smslib.pduUtils.gsm3040.SmsSubmitPdu;
import org.smslib.pduUtils.gsm3040.ie.InformationElementFactory;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class OutboundMessage extends AbstractMessage {
private static final long serialVersionUID = 1L;
public enum SentStatus {
Sent("S"),
Unsent("U"),
Queued("Q"),
Failed("F");
private final String shortString;
private SentStatus(String shortString) {
this.shortString = shortString;
}
public String toShortString() {
return this.shortString;
}
}
public enum FailureCause {
None("00"),
BadNumber("01"),
BadFormat("02"),
GatewayFailure("03"),
AuthFailure("04"),
NoCredit("05"),
OverQuota("06"),
NoRoute("07"),
Unavailable("08"),
HttpError("09"),
UnknownFailure("10"),
Cancelled("11"),
NoService("12"),
MissingParms("13");
private final String shortString;
private FailureCause(String shortString) {
this.shortString = shortString;
}
public String toShortString() {
return this.shortString;
}
}
SentStatus sentStatus = SentStatus.Unsent;
FailureCause failureCause = FailureCause.None;
List<String> operatorMessageIds = new ArrayList<>();
boolean requestDeliveryReport = false;
public OutboundMessage() {
}
public OutboundMessage(MsIsdn originatorAddress, MsIsdn recipientAddress, Payload payload) {
super(Type.Outbound, originatorAddress, recipientAddress, payload);
}
public OutboundMessage(String recipientAddress, String text) {
this(new MsIsdn(""), new MsIsdn(recipientAddress), new Payload(text));
}
public SentStatus getSentStatus() {
return this.sentStatus;
}
public void setSentStatus(SentStatus sentStatus) {
this.sentStatus = sentStatus;
}
public FailureCause getFailureCause() {
return this.failureCause;
}
public void setFailureCause(FailureCause failureCode) {
this.failureCause = failureCode;
}
public List<String> getOperatorMessageIds() {
return this.operatorMessageIds;
}
public boolean getRequestDeliveryReport() {
return this.requestDeliveryReport;
}
public void setRequestDeliveryReport(boolean requestDeliveryReport) {
this.requestDeliveryReport = requestDeliveryReport;
}
@Override
public String toShortString() {
return String.format("[%s @ %s]", getId(), getRecipientAddress());
}
public List<String> getPdus(MsIsdn smscNumber, int mpRefNo) {
PduGenerator pduGenerator = new PduGenerator();
SmsSubmitPdu pdu = createPduObject(getRequestDeliveryReport());
initPduObject(pdu, smscNumber);
return pduGenerator.generatePduList(pdu, mpRefNo);
}
protected SmsSubmitPdu createPduObject(boolean extRequestDeliveryReport) {
return (extRequestDeliveryReport ? PduFactory.newSmsSubmitPdu(PduUtils.TP_SRR_REPORT | PduUtils.TP_VPF_INTEGER)
: PduFactory.newSmsSubmitPdu());
}
protected void initPduObject(SmsSubmitPdu pdu, MsIsdn smscNumber) {
if ((getSourcePort() > -1) && (getDestinationPort() > -1)) {
pdu.addInformationElement(
InformationElementFactory.generatePortInfo(getDestinationPort(), getSourcePort()));
}
String smscNumberForLengthCheck = smscNumber.getAddress();
pdu.setSmscInfoLength(
1 + (smscNumberForLengthCheck.length() / 2) + ((smscNumberForLengthCheck.length() % 2 == 1) ? 1 : 0));
pdu.setSmscAddress(smscNumber.getAddress());
pdu.setSmscAddressType(PduUtils.getAddressTypeFor(smscNumber));
pdu.setMessageReference(0);
MsIsdn finalRecipientAddress = recipientAddress;
if (finalRecipientAddress == null) {
throw new UnrecoverableSmslibException("Recipient adress cannot be null");
}
pdu.setAddress(finalRecipientAddress);
MsIsdn recipientAddressFinal = this.recipientAddress;
if (recipientAddressFinal == null) {
throw new UnrecoverableSmslibException("Cannot set address type with no recipient");
}
pdu.setAddressType(PduUtils.getAddressTypeFor(recipientAddressFinal));
pdu.setProtocolIdentifier(0);
if (!pdu.isBinary()) {
int dcs = 0;
if (getEncoding() == Encoding.Enc7) {
dcs = PduUtils.DCS_ENCODING_7BIT;
} else if (getEncoding() == Encoding.Enc8) {
dcs = PduUtils.DCS_ENCODING_8BIT;
} else if (getEncoding() == Encoding.EncUcs2) {
dcs = PduUtils.DCS_ENCODING_UCS2;
} else if (getEncoding() == Encoding.EncCustom) {
dcs = PduUtils.DCS_ENCODING_7BIT;
}
if (getDcsClass() == DcsClass.Flash) {
dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_FLASH;
} else if (getDcsClass() == DcsClass.Me) {
dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_ME;
} else if (getDcsClass() == DcsClass.Sim) {
dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_SIM;
} else if (getDcsClass() == DcsClass.Te) {
dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_TE;
}
pdu.setDataCodingScheme(dcs);
}
pdu.setValidityPeriod(0);
if (getEncoding() == Encoding.Enc8) {
byte[] bytes = getPayload().getBytes();
if (bytes == null) {
throw new UnrecoverableSmslibException("Cannot init pdu object, wrong payload");
}
pdu.setDataBytes(bytes);
} else {
String text = getPayload().getText();
if (text == null) {
throw new UnrecoverableSmslibException("Cannot init pdu object, wrong payload");
}
pdu.setDecodedText(text);
}
}
@Override
public String getSignature() {
return hashSignature(String.format("%s-%s", getRecipientAddress(), getId()));
}
}

View File

@@ -0,0 +1,54 @@
package org.smslib.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class Payload {
public enum Type {
Text,
Binary
}
private @Nullable String textData;
private byte @Nullable [] binaryData;
private Type type;
public Payload(String data) {
this.type = Type.Text;
this.textData = data;
}
public Payload(byte[] data) {
this.type = Type.Binary;
this.binaryData = data.clone();
}
public Payload(Payload p) {
this.type = p.getType();
this.textData = (this.type == Type.Text ? p.getText() : "");
byte[] bytes = p.getBytes();
this.binaryData = (this.type == Type.Binary && bytes != null ? bytes.clone() : null);
}
public Type getType() {
return this.type;
}
public @Nullable String getText() {
return (this.type == Type.Text ? this.textData : null);
}
public byte @Nullable [] getBytes() {
return (this.type == Type.Binary ? this.binaryData : null);
}
public boolean isMultipart() {
return false;
}
}

View File

@@ -0,0 +1,520 @@
package org.smslib.pduUtils.gsm3040;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.smslib.UnrecoverableSmslibException;
import org.smslib.message.MsIsdn;
import org.smslib.message.MsIsdn.Type;
import org.smslib.pduUtils.gsm3040.ie.ConcatInformationElement;
import org.smslib.pduUtils.gsm3040.ie.InformationElement;
import org.smslib.pduUtils.gsm3040.ie.PortInformationElement;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public abstract class Pdu {
// PDU class
// this class holds directly usable data only
// - all lengths are ints
// - dates and strings are already decoded
// - byte[] for binary data that can interpreted later
// an object of this type is created via a PduParser
// or created raw, has its field set and supplied to a PduGenerator
// ==================================================
// SMSC INFO
// ==================================================
private int smscInfoLength;
private int smscAddressType;
@Nullable
private String smscAddress;
public int getSmscInfoLength() {
return this.smscInfoLength;
}
public void setSmscInfoLength(int smscInfoLength) {
this.smscInfoLength = smscInfoLength;
}
public void setSmscAddressType(int smscAddressType) {
this.smscAddressType = PduUtils.createAddressType(smscAddressType);
}
public int getSmscAddressType() {
return this.smscAddressType;
}
public void setSmscAddress(@Nullable String smscAddress) {
if (smscAddress == null || "".equals(smscAddress)) {
this.smscAddress = null;
this.smscAddressType = 0;
this.smscInfoLength = 0;
return;
}
// strip the + since it is not needed
if (smscAddress.startsWith("+")) {
this.smscAddress = smscAddress.substring(1);
} else {
this.smscAddress = smscAddress;
}
}
public @Nullable String getSmscAddress() {
return this.smscAddress;
}
// ==================================================
// FIRST OCTET
// ==================================================
private int firstOctet = 0;
public int getFirstOctet() {
return this.firstOctet;
}
public void setFirstOctet(int value) {
this.firstOctet = value;
}
protected void setFirstOctetField(int fieldName, int fieldValue, int[] allowedValues) {
for (int value : allowedValues) {
if (value == fieldValue) {
// clear the bits for this field
this.firstOctet &= fieldName;
// copy the new bits on to it
this.firstOctet |= fieldValue;
return;
}
}
throw new IllegalArgumentException("Invalid value for fieldName.");
}
protected int getFirstOctetField(int fieldName) {
return this.firstOctet & ~fieldName;
}
protected void checkTpMti(int[] allowedTypes) {
int tpMti = getTpMti();
for (int type : allowedTypes) {
if (tpMti == type) {
return;
}
}
throw new IllegalArgumentException("Invalid message type : " + getTpMti());
}
public int getTpMti() {
return getFirstOctetField(PduUtils.TP_MTI_MASK);
}
public void setTpUdhi(int value) {
setFirstOctetField(PduUtils.TP_UDHI_MASK, value,
new int[] { PduUtils.TP_UDHI_NO_UDH, PduUtils.TP_UDHI_WITH_UDH });
}
public boolean hasTpUdhi() {
return getFirstOctetField(PduUtils.TP_UDHI_MASK) == PduUtils.TP_UDHI_WITH_UDH;
}
// ==================================================
// PROTOCOL IDENTIFIER
// ==================================================
// usually just 0x00 for regular SMS
private int protocolIdentifier = 0x00;
public void setProtocolIdentifier(int protocolIdentifier) {
this.protocolIdentifier = protocolIdentifier;
}
public int getProtocolIdentifier() {
return this.protocolIdentifier;
}
// ==================================================
// DATA CODING SCHEME
// ==================================================
// usually just 0x00 for default GSM alphabet, phase 2
private int dataCodingScheme = 0x00;
public void setDataCodingScheme(int encoding) {
switch (encoding & ~PduUtils.DCS_ENCODING_MASK) {
case PduUtils.DCS_ENCODING_7BIT:
case PduUtils.DCS_ENCODING_8BIT:
case PduUtils.DCS_ENCODING_UCS2:
break;
default:
throw new IllegalArgumentException("Invalid encoding value: " + PduUtils.byteToPdu(encoding));
}
this.dataCodingScheme = encoding;
}
public int getDataCodingScheme() {
return this.dataCodingScheme;
}
// ==================================================
// TYPE-OF-ADDRESS
// ==================================================
private int addressType;
public int getAddressType() {
return this.addressType;
}
public void setAddressType(int addressType) {
// insure last bit is always set
this.addressType = PduUtils.createAddressType(addressType);
}
// ==================================================
// ADDRESS
// ==================================================
// swapped BCD-format for numbers
// 7-bit GSM string for alphanumeric
private @Nullable String address;
public void setAddress(MsIsdn address) {
if (address.getType() == Type.Void) {
this.address = "";
} else {
this.address = address.getAddress();
}
setAddressType(PduUtils.getAddressTypeFor(address));
}
public @Nullable String getAddress() {
return this.address;
}
// ==================================================
// USER DATA SECTION
// ==================================================
// this is still needs to be stored since it does not always represent
// length in octets, for 7-bit encoding this is length in SEPTETS
// NOTE: udData.length may not equal udLength if 7-bit encoding is used
private int udLength;
private byte @Nullable [] udData;
public int getUDLength() {
return this.udLength;
}
public void setUDLength(int udLength) {
this.udLength = udLength;
}
public byte @Nullable [] getUDData() {
return this.udData;
}
// NOTE: udData DOES NOT include the octet with the length
public void setUDData(byte[] udData) {
this.udData = udData;
}
// ==================================================
// USER DATA HEADER
// ==================================================
// all methods accessing UDH specific methods require the UDHI to be set
// or else an exception will result
private static final int UDH_CHECK_MODE_ADD_IF_NONE = 0;
private static final int UDH_CHECK_MODE_EXCEPTION_IF_NONE = 1;
private static final int UDH_CHECK_MODE_IGNORE_IF_NONE = 2;
private void checkForUDHI(int udhCheckMode) {
if (!hasTpUdhi()) {
switch (udhCheckMode) {
case UDH_CHECK_MODE_EXCEPTION_IF_NONE:
throw new IllegalStateException("PDU does not have a UDHI in the first octet");
case UDH_CHECK_MODE_ADD_IF_NONE:
setTpUdhi(PduUtils.TP_UDHI_WITH_UDH);
break;
case UDH_CHECK_MODE_IGNORE_IF_NONE:
break;
default:
throw new IllegalArgumentException("Invalid UDH check mode");
}
}
}
public int getTotalUDHLength() {
int udhLength = getUDHLength();
if (udhLength == 0) {
return 0;
}
// also takes into account the field holding the length
// it self
return udhLength + 1;
}
public int getUDHLength() {
// compute based on the IEs
int udhLength = 0;
for (InformationElement ie : this.ieMap.values()) {
// length + 2 to account for the octet holding the IE length and id
udhLength = udhLength + ie.getLength() + 2;
}
return udhLength;
}
public byte @Nullable [] getUDHData() {
checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
int totalUdhLength = getTotalUDHLength();
if (totalUdhLength == 0) {
return null;
}
byte[] retVal = new byte[totalUdhLength];
byte[] finalUdData = this.udData;
if (finalUdData != null) {
System.arraycopy(finalUdData, 0, retVal, 0, totalUdhLength);
} else {
throw new IllegalArgumentException("Cannot get udhd data because udData is null");
}
return retVal;
}
// UDH portion of UD if UDHI is present
// only Concat and Port info will be treated specially
// other IEs will have to get extracted from the map manually and parsed
private HashMap<Integer, InformationElement> ieMap = new HashMap<>();
private ArrayList<InformationElement> ieList = new ArrayList<>();
public void addInformationElement(InformationElement ie) {
checkForUDHI(UDH_CHECK_MODE_ADD_IF_NONE);
this.ieMap.put(ie.getIdentifier(), ie);
this.ieList.add(ie);
}
public @Nullable InformationElement getInformationElement(int iei) {
checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
return this.ieMap.get(iei);
}
// this is only used in the parser generator
public Iterator<InformationElement> getInformationElements() {
checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
return this.ieList.iterator();
}
// ==================================================
// CONCAT INFO
// ==================================================
public boolean isConcatMessage() {
// check if iei 0x00 or 0x08 is present
return (getConcatInfo() != null);
}
public @Nullable ConcatInformationElement getConcatInfo() {
checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
ConcatInformationElement concat = (ConcatInformationElement) getInformationElement(
ConcatInformationElement.CONCAT_8BIT_REF);
if (concat == null) {
concat = (ConcatInformationElement) getInformationElement(ConcatInformationElement.CONCAT_16BIT_REF);
}
return concat;
}
public int getMpRefNo() {
ConcatInformationElement concat = getConcatInfo();
if (concat != null) {
return concat.getMpRefNo();
}
return 0;
}
public int getMpMaxNo() {
ConcatInformationElement concat = getConcatInfo();
if (concat != null) {
return concat.getMpMaxNo();
}
return 1;
}
public int getMpSeqNo() {
ConcatInformationElement concat = getConcatInfo();
if (concat != null) {
return concat.getMpSeqNo();
}
return 0;
}
// ==================================================
// PORT DATA
// ==================================================
public boolean isPortedMessage() {
// check if iei 0x05 is present
return (getPortInfo() != null);
}
private @Nullable PortInformationElement getPortInfo() {
checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
return (PortInformationElement) getInformationElement(PortInformationElement.PORT_16BIT);
}
public int getDestPort() {
PortInformationElement portIe = getPortInfo();
if (portIe == null) {
return -1;
}
return portIe.getDestPort();
}
public int getSrcPort() {
PortInformationElement portIe = getPortInfo();
if (portIe == null) {
return -1;
}
return portIe.getSrcPort();
}
// ==================================================
// NON-UDH DATA
// ==================================================
// UD minus the UDH portion, same as userData if
// no UDH
// these fields store data for the generation step
private @Nullable String decodedText;
private byte @Nullable [] dataBytes;
public void setDataBytes(byte[] dataBytes) {
this.dataBytes = dataBytes;
this.decodedText = null;
// clear the encoding bits for this field 8-bit/data
// this.dataCodingScheme &= PduUtils.DCS_ENCODING_MASK;
// this.dataCodingScheme |= PduUtils.DCS_ENCODING_8BIT;
// this.dataCodingScheme |= PduUtils.DCS_CODING_GROUP_DATA;
}
public byte @Nullable [] getDataBytes() {
return this.dataBytes;
}
public boolean isBinary() {
// use the DCS coding group or 8bit encoding
// Changed following line according to http://code.google.com/p/smslib/issues/detail?id=187
// return ((this.dataCodingScheme & PduUtils.DCS_CODING_GROUP_DATA) == PduUtils.DCS_CODING_GROUP_DATA ||
// (this.dataCodingScheme & PduUtils.DCS_ENCODING_8BIT) == PduUtils.DCS_ENCODING_8BIT);
if ((this.dataCodingScheme & PduUtils.DCS_CODING_GROUP_DATA) == PduUtils.DCS_CODING_GROUP_DATA
|| (this.dataCodingScheme & PduUtils.DCS_ENCODING_8BIT) == PduUtils.DCS_ENCODING_8BIT) {
if ((this.dataCodingScheme & PduUtils.DCS_ENCODING_8BIT) == PduUtils.DCS_ENCODING_8BIT) {
return (true);
}
}
return (false);
}
public void setDecodedText(String decodedText) {
this.decodedText = decodedText;
this.dataBytes = null;
// check if existing DCS indicates a flash message
boolean flash = false;
if (PduUtils.extractDcsFlash(this.dataCodingScheme) == PduUtils.DCS_MESSAGE_CLASS_FLASH) {
flash = true;
}
// clears the coding group to be text again in case it was originally binary
this.dataCodingScheme &= PduUtils.DCS_CODING_GROUP_MASK;
// set the flash bit back since the above would clear it
if (flash) {
this.dataCodingScheme = this.dataCodingScheme | PduUtils.DCS_MESSAGE_CLASS_FLASH;
}
}
public String getDecodedText() {
// this should be try-catched in case the ud data is
// actually binary and might cause a decoding exception
String decodedTextFinal = this.decodedText;
if (decodedTextFinal != null) {
return decodedTextFinal;
}
if (this.udData == null) {
throw new UnrecoverableSmslibException("No udData to decode");
}
return decodeNonUDHDataAsString();
}
public byte[] getUserDataAsBytes() {
byte[] udDataFinal = this.udData;
if (udDataFinal == null) {
throw new UnrecoverableSmslibException("udData cannot be null");
}
int remainingLength = udDataFinal.length - (getTotalUDHLength());
byte[] retVal = new byte[remainingLength];
byte[] finalUdData = udData;
if (finalUdData != null) {
System.arraycopy(finalUdData, getTotalUDHLength(), retVal, 0, remainingLength);
} else {
throw new UnrecoverableSmslibException("Cannot get user data because udData is null");
}
return retVal;
}
private String decodeNonUDHDataAsString() {
// convert PDU to text depending on the encoding
// must also take into account the octet holding the length
byte[] udhDataFinal = getUDHData();
byte[] udDataFinal = this.udData;
if (udDataFinal == null) {
throw new UnrecoverableSmslibException("Cannot decode with udData null");
}
switch (PduUtils.extractDcsEncoding(getDataCodingScheme())) {
case PduUtils.DCS_ENCODING_7BIT:
// unpack all septets to octets with MSB holes
byte[] septets = PduUtils.encodedSeptetsToUnencodedSeptets(udDataFinal);
int septetUDHLength = 0;
if (udhDataFinal != null) {
// work out how much of the UD is UDH
septetUDHLength = udhDataFinal.length * 8 / 7;
if (udhDataFinal.length * 8 % 7 > 0) {
septetUDHLength++;
}
}
byte[] septetsNoUDH = new byte[this.udLength - septetUDHLength];
// src, srcStart, dest, destStart, length
System.arraycopy(septets, septetUDHLength, septetsNoUDH, 0, septetsNoUDH.length);
return PduUtils.unencodedSeptetsToString(septetsNoUDH);
case PduUtils.DCS_ENCODING_8BIT:
return PduUtils.decode8bitEncoding(udhDataFinal, udDataFinal);
case PduUtils.DCS_ENCODING_UCS2:
return PduUtils.decodeUcs2Encoding(udhDataFinal, udDataFinal);
}
throw new IllegalArgumentException("Invalid dataCodingScheme: " + getDataCodingScheme());
}
protected String formatTimestamp(Calendar timestamp) {
SimpleDateFormat sdf = new SimpleDateFormat();
sdf.applyPattern("EEE dd-MMM-yyyy HH:mm:ss z");
sdf.setTimeZone(timestamp.getTimeZone());
return sdf.format(timestamp.getTime());
}
}

View File

@@ -0,0 +1,66 @@
package org.smslib.pduUtils.gsm3040;
import org.eclipse.jdt.annotation.NonNullByDefault;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class PduFactory {
public static SmsSubmitPdu newSmsSubmitPdu() {
// apply defaults
int additionalFields = PduUtils.TP_RD_ACCEPT_DUPLICATES | PduUtils.TP_VPF_INTEGER;
return newSmsSubmitPdu(additionalFields);
}
public static SmsSubmitPdu newSmsSubmitPdu(int additionalFields) {
// remove any TP_MTI values
int firstOctet = PduUtils.TP_MTI_SMS_SUBMIT | additionalFields;
return (SmsSubmitPdu) createPdu(firstOctet);
}
private static int getFirstOctetField(int firstOctet, int fieldName) {
return firstOctet & ~fieldName;
}
// used to determine what Pdu to use based on the first octet
// this is the only way to instantiate a Pdu object
public static Pdu createPdu(int firstOctet) {
Pdu pdu = null;
int messageType = getFirstOctetField(firstOctet, PduUtils.TP_MTI_MASK);
switch (messageType) {
case PduUtils.TP_MTI_SMS_DELIVER:
pdu = new SmsDeliveryPdu();
break;
case PduUtils.TP_MTI_SMS_STATUS_REPORT:
pdu = new SmsStatusReportPdu();
break;
case PduUtils.TP_MTI_SMS_SUBMIT:
pdu = new SmsSubmitPdu();
break;
default:
throw new IllegalArgumentException("Invalid TP-MTI value: " + messageType);
}
// once set, you can't change it
// this method is only available in this package
pdu.setFirstOctet(firstOctet);
return pdu;
}
}

View File

@@ -0,0 +1,545 @@
package org.smslib.pduUtils.gsm3040;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.smslib.UnrecoverableSmslibException;
import org.smslib.pduUtils.gsm3040.ie.ConcatInformationElement;
import org.smslib.pduUtils.gsm3040.ie.InformationElement;
import org.smslib.pduUtils.gsm3040.ie.InformationElementFactory;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class PduGenerator {
private ByteArrayOutputStream baos = new ByteArrayOutputStream();
private int firstOctetPosition = -1;
private boolean updateFirstOctet = false;
protected void writeSmscInfo(Pdu pdu) {
String smscAddress = pdu.getSmscAddress();
if (smscAddress != null) {
writeBCDAddress(smscAddress, pdu.getSmscAddressType(), pdu.getSmscInfoLength());
} else {
writeByte(0);
}
}
protected void writeFirstOctet(Pdu pdu) {
// store the position in case it will need to be updated later
this.firstOctetPosition = pdu.getSmscInfoLength() + 1;
writeByte(pdu.getFirstOctet());
}
// validity period conversion from hours to the proper integer
protected void writeValidityPeriodInteger(int validityPeriod) {
if (validityPeriod == -1) {
this.baos.write(0xFF);
} else {
int validityInt;
if (validityPeriod <= 12) {
validityInt = (validityPeriod * 12) - 1;
} else if (validityPeriod <= 24) {
validityInt = (((validityPeriod - 12) * 2) + 143);
} else if (validityPeriod <= 720) {
validityInt = (validityPeriod / 24) + 166;
} else {
validityInt = (validityPeriod / 168) + 192;
}
this.baos.write(validityInt);
}
}
protected void writeTimeStampStringForDate(Date timestamp) {
Calendar cal = Calendar.getInstance();
cal.setTime(timestamp);
int year = cal.get(Calendar.YEAR) - 2000;
int month = cal.get(Calendar.MONTH) + 1;
int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
int minute = cal.get(Calendar.MINUTE);
int sec = cal.get(Calendar.SECOND);
TimeZone tz = cal.getTimeZone();
int offset = tz.getOffset(timestamp.getTime());
int minOffset = offset / 60000;
int tzValue = minOffset / 15;
// for negative offsets, add 128 to the absolute value
if (tzValue < 0) {
tzValue = 128 - tzValue;
}
// note: the nibbles are written as BCD style
this.baos.write(PduUtils.createSwappedBCD(year));
this.baos.write(PduUtils.createSwappedBCD(month));
this.baos.write(PduUtils.createSwappedBCD(dayOfMonth));
this.baos.write(PduUtils.createSwappedBCD(hourOfDay));
this.baos.write(PduUtils.createSwappedBCD(minute));
this.baos.write(PduUtils.createSwappedBCD(sec));
this.baos.write(PduUtils.createSwappedBCD(tzValue));
}
protected void writeAddress(String address, int addressType, int addressLength) throws IOException {
switch (PduUtils.extractAddressType(addressType)) {
case PduUtils.ADDRESS_TYPE_ALPHANUMERIC:
byte[] textSeptets = PduUtils.stringToUnencodedSeptets(address);
byte[] alphaNumBytes = PduUtils.encode7bitUserData(null, textSeptets);
// ADDRESS LENGTH - should be the semi-octet count
// - this type is not used for SMSCInfo
this.baos.write(alphaNumBytes.length * 2);
// ADDRESS TYPE
this.baos.write(addressType);
// ADDRESS TEXT
this.baos.write(alphaNumBytes);
break;
default:
// BCD-style
writeBCDAddress(address, addressType, addressLength);
}
}
protected void writeBCDAddress(String address, int addressType, int addressLength) {
// BCD-style
// ADDRESS LENGTH - either an octet count or semi-octet count
this.baos.write(addressLength);
// ADDRESS TYPE
this.baos.write(addressType);
// ADDRESS NUMBERS
// if address.length is not even, pad the string an with F at the end
String myaddress = address;
if (myaddress.length() % 2 == 1) {
myaddress = myaddress + "F";
}
int digit = 0;
for (int i = 0; i < myaddress.length(); i++) {
char c = myaddress.charAt(i);
if (i % 2 == 1) {
digit |= ((Integer.parseInt(Character.toString(c), 16)) << 4);
this.baos.write(digit);
// clear it
digit = 0;
} else {
digit |= (Integer.parseInt(Character.toString(c), 16) & 0x0F);
}
}
}
protected void writeUDData(Pdu pdu, int mpRefNo, int partNo) {
int dcs = pdu.getDataCodingScheme();
try {
switch (PduUtils.extractDcsEncoding(dcs)) {
case PduUtils.DCS_ENCODING_7BIT:
writeUDData7bit(pdu, mpRefNo, partNo);
break;
case PduUtils.DCS_ENCODING_8BIT:
writeUDData8bit(pdu, mpRefNo, partNo);
break;
case PduUtils.DCS_ENCODING_UCS2:
writeUDDataUCS2(pdu, mpRefNo, partNo);
break;
default:
throw new IllegalArgumentException("Invalid DCS encoding: " + PduUtils.extractDcsEncoding(dcs));
}
} catch (IOException e) {
throw new UnrecoverableSmslibException("Cannot write uddata", e);
}
}
protected void writeUDH(Pdu pdu) throws IOException {
// stream directly into the internal baos
writeUDH(pdu, this.baos);
}
protected void writeUDH(Pdu pdu, ByteArrayOutputStream udhBaos) throws IOException {
// need to insure that proper concat info is inserted
// before writing if needed
// i.e. the reference number, maxseq and seq have to be set from
// outside (OutboundMessage)
udhBaos.write(pdu.getUDHLength());
for (Iterator<InformationElement> ieIterator = pdu.getInformationElements(); ieIterator.hasNext();) {
InformationElement ie = ieIterator.next();
udhBaos.write(ie.getIdentifier());
udhBaos.write(ie.getLength());
udhBaos.write(ie.getData());
}
}
protected int computeOffset(Pdu pdu, int maxMessageLength, int partNo) {
// computes offset to which part of the string is to be encoded into the PDU
// also sets the MpMaxNo field of the concatInfo if message is multi-part
int offset;
int maxParts = 1;
if (!pdu.isBinary()) {
maxParts = pdu.getDecodedText().length() / maxMessageLength + 1;
} else {
byte[] pduDataBytes = pdu.getDataBytes();
if (pduDataBytes == null) {
throw new UnrecoverableSmslibException("Cannot compute offset for empty data bytes");
}
maxParts = pduDataBytes.length / maxMessageLength + 1;
}
if (pdu.hasTpUdhi()) {
ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
if (concatInfoFinal != null) {
if (partNo > 0) {
concatInfoFinal.setMpMaxNo(maxParts);
}
}
}
if ((maxParts > 1) && (partNo > 0)) {
// - if partNo > maxParts
// - error
if (partNo > maxParts) {
throw new IllegalArgumentException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
}
offset = ((partNo - 1) * maxMessageLength);
} else {
// just get from the start
offset = 0;
}
return offset;
}
protected void checkForConcat(Pdu pdu, int lengthOfText, int maxLength, int maxLengthWithUdh, int mpRefNo,
int partNo) {
if ((lengthOfText <= maxLengthWithUdh) || ((lengthOfText > maxLengthWithUdh) && (lengthOfText <= maxLength))) {
} else {
// need concat
ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
if (concatInfoFinal != null) {
// if concatInfo is already present then just replace the values with the supplied
concatInfoFinal.setMpRefNo(mpRefNo);
concatInfoFinal.setMpSeqNo(partNo);
} else {
// add concat info with the specified mpRefNo, bogus maxSeqNo, and partNo
// bogus maxSeqNo will be replaced once it is known in the later steps
// this just needs to be added since its presence is needed to compute
// the UDH length
ConcatInformationElement concatInfo = InformationElementFactory.generateConcatInfo(mpRefNo, partNo);
pdu.addInformationElement(concatInfo);
this.updateFirstOctet = true;
}
}
}
protected int computePotentialUdhLength(Pdu pdu) {
int currentUdhLength = pdu.getTotalUDHLength();
if (currentUdhLength == 0) {
// add 1 for the UDH Length field
return ConcatInformationElement.getDefaultConcatLength() + 1;
}
// this already has the UDH Length field, no need to add 1
return currentUdhLength + ConcatInformationElement.getDefaultConcatLength();
}
protected void writeUDData7bit(Pdu pdu, int mpRefNo, int partNo) throws IOException {
String decodedText = pdu.getDecodedText();
// partNo states what part of the unencoded text will be used
// - max length is based on the size of the UDH
// for 7bit => maxLength = 160 - total UDH septets
// check if this message needs a concat
byte[] textSeptetsForDecodedText = PduUtils.stringToUnencodedSeptets(decodedText);
int potentialUdhLength = PduUtils.getNumSeptetsForOctets(computePotentialUdhLength(pdu));
checkForConcat(pdu, textSeptetsForDecodedText.length,
160 - PduUtils.getNumSeptetsForOctets(pdu.getTotalUDHLength()), // CHANGED
160 - potentialUdhLength, mpRefNo, partNo);
// given the IEs in the pdu derive the max message body length
// this length will include the potential concat added in the previous step
int totalUDHLength = pdu.getTotalUDHLength();
int maxMessageLength = 160 - PduUtils.getNumSeptetsForOctets(totalUDHLength);
// get septets for part
byte[] textSeptets = getUnencodedSeptetsForPart(pdu, maxMessageLength, partNo);
// udlength is the sum of udh septet length and the text septet length
int udLength = PduUtils.getNumSeptetsForOctets(totalUDHLength) + textSeptets.length;
this.baos.write(udLength);
// generate UDH byte[]
// UDHL (sum of all IE lengths)
// IE list
byte[] udhBytes = null;
if (pdu.hasTpUdhi()) {
ByteArrayOutputStream udhBaos = new ByteArrayOutputStream();
writeUDH(pdu, udhBaos);
// buffer the udh since this needs to be 7-bit encoded with the text
udhBytes = udhBaos.toByteArray();
}
// encode both as one unit
byte[] udBytes = PduUtils.encode7bitUserData(udhBytes, textSeptets);
// write combined encoded array
this.baos.write(udBytes);
}
private byte[] getUnencodedSeptetsForPart(Pdu pdu, int maxMessageLength, int partNo) {
// computes offset to which part of the string is to be encoded into the PDU
// also sets the MpMaxNo field of the concatInfo if message is multi-part
int offset;
int maxParts = 1;
// must use the unencoded septets not the actual string since
// it is possible that some special characters in string are multi-septet
byte[] unencodedSeptets = PduUtils.stringToUnencodedSeptets(pdu.getDecodedText());
maxParts = (unencodedSeptets.length / maxMessageLength) + 1;
if (pdu.hasTpUdhi()) {
ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
if (concatInfoFinal != null) {
if (partNo > 0) {
concatInfoFinal.setMpMaxNo(maxParts);
}
}
}
if ((maxParts > 1) && (partNo > 0)) {
// - if partNo > maxParts
// - error
if (partNo > maxParts) {
throw new UnrecoverableSmslibException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
}
offset = ((partNo - 1) * maxMessageLength);
} else {
// just get from the start
offset = 0;
}
// copy the portion of the full unencoded septet array for this part
byte[] septetsForPart = new byte[Math.min(maxMessageLength, unencodedSeptets.length - offset)];
System.arraycopy(unencodedSeptets, offset, septetsForPart, 0, septetsForPart.length);
return septetsForPart;
}
protected void writeUDData8bit(Pdu pdu, int mpRefNo, int partNo) throws IOException {
// NOTE: binary messages are also handled here
byte[] data;
if (pdu.isBinary()) {
// use the supplied bytes
byte[] dataBytesFinal = pdu.getDataBytes();
if (dataBytesFinal == null) {
throw new UnrecoverableSmslibException("Data cannot be null");
}
data = dataBytesFinal;
} else {
// encode the text
data = PduUtils.encode8bitUserData(pdu.getDecodedText());
}
// partNo states what part of the unencoded text will be used
// - max length is based on the size of the UDH
// for 8bit => maxLength = 140 - the total UDH bytes
// check if this message needs a concat
int potentialUdhLength = computePotentialUdhLength(pdu);
checkForConcat(pdu, data.length, 140 - pdu.getTotalUDHLength(), // CHANGED
140 - potentialUdhLength, mpRefNo, partNo);
// given the IEs in the pdu derive the max message body length
// this length will include the potential concat added in the previous step
int totalUDHLength = pdu.getTotalUDHLength();
int maxMessageLength = 140 - totalUDHLength;
// compute which portion of the message will be part of the message
int offset = computeOffset(pdu, maxMessageLength, partNo);
byte[] dataToWrite = new byte[Math.min(maxMessageLength, data.length - offset)];
System.arraycopy(data, offset, dataToWrite, 0, dataToWrite.length);
// generate udlength
// based on partNo
// udLength is an octet count for 8bit/ucs2
int udLength = totalUDHLength + dataToWrite.length;
// write udlength
this.baos.write(udLength);
// write UDH to the stream directly
if (pdu.hasTpUdhi()) {
writeUDH(pdu, this.baos);
}
// write data
this.baos.write(dataToWrite);
}
protected void writeUDDataUCS2(Pdu pdu, int mpRefNo, int partNo) throws IOException {
String decodedText = pdu.getDecodedText();
// partNo states what part of the unencoded text will be used
// - max length is based on the size of the UDH
// for ucs2 => maxLength = (140 - the total UDH bytes)/2
// check if this message needs a concat
int potentialUdhLength = computePotentialUdhLength(pdu);
checkForConcat(pdu, decodedText.length(), (140 - pdu.getTotalUDHLength()) / 2, // CHANGED
(140 - potentialUdhLength) / 2, mpRefNo, partNo);
// given the IEs in the pdu derive the max message body length
// this length will include the potential concat added in the previous step
int totalUDHLength = pdu.getTotalUDHLength();
int maxMessageLength = (140 - totalUDHLength) / 2;
// compute which portion of the message will be part of the message
int offset = computeOffset(pdu, maxMessageLength, partNo);
String textToEncode = decodedText.substring(offset, Math.min(offset + maxMessageLength, decodedText.length()));
// generate udlength
// based on partNo
// udLength is an octet count for 8bit/ucs2
int udLength = totalUDHLength + (textToEncode.length() * 2);
// write udlength
this.baos.write(udLength);
// write UDH to the stream directly
if (pdu.hasTpUdhi()) {
writeUDH(pdu, this.baos);
}
// write encoded text
this.baos.write(PduUtils.encodeUcs2UserData(textToEncode));
}
protected void writeByte(int i) {
this.baos.write(i);
}
protected void writeBytes(byte[] b) throws IOException {
this.baos.write(b);
}
public List<String> generatePduList(Pdu pdu, int mpRefNo) {
// generate all required PDUs for a given message
// mpRefNo comes from the ModemGateway
ArrayList<String> pduList = new ArrayList<>();
for (int i = 1; i <= pdu.getMpMaxNo(); i++) {
String pduString = generatePduString(pdu, mpRefNo, i);
pduList.add(pduString);
}
return pduList;
}
// NOTE: partNo indicates which part of a multipart message to generate
// assuming that the message is multipart, this will be ignored if the
// message is not a concat message
public String generatePduString(Pdu pdu, int mpRefNo, int partNo) {
try {
this.baos = new ByteArrayOutputStream();
this.firstOctetPosition = -1;
this.updateFirstOctet = false;
// process the PDU
switch (pdu.getTpMti()) {
case PduUtils.TP_MTI_SMS_DELIVER:
generateSmsDeliverPduString((SmsDeliveryPdu) pdu, mpRefNo, partNo);
break;
case PduUtils.TP_MTI_SMS_SUBMIT:
generateSmsSubmitPduString((SmsSubmitPdu) pdu, mpRefNo, partNo);
break;
case PduUtils.TP_MTI_SMS_STATUS_REPORT:
generateSmsStatusReportPduString((SmsStatusReportPdu) pdu);
break;
}
// in case concat is detected in the writeUD() method
// and there was no UDHI at the time of detection
// the old firstOctet must be overwritten with the new value
byte[] pduBytes = this.baos.toByteArray();
if (this.updateFirstOctet) {
pduBytes[this.firstOctetPosition] = (byte) (pdu.getFirstOctet() & 0xFF);
}
return PduUtils.bytesToPdu(pduBytes);
} catch (IOException e) {
throw new UnrecoverableSmslibException("Cannot generate pdu", e);
}
}
protected void generateSmsSubmitPduString(SmsSubmitPdu pdu, int mpRefNo, int partNo) throws IOException {
String address = pdu.getAddress();
if (address == null) {
throw new IllegalArgumentException("adress cannot be null");
}
// SMSC address info
writeSmscInfo(pdu);
// first octet
writeFirstOctet(pdu);
// message reference
writeByte(pdu.getMessageReference());
// destination address info
writeAddress(address, pdu.getAddressType(), address.length());
// protocol id
writeByte(pdu.getProtocolIdentifier());
// data coding scheme
writeByte(pdu.getDataCodingScheme());
// validity period
switch (pdu.getTpVpf()) {
case PduUtils.TP_VPF_INTEGER:
writeValidityPeriodInteger(pdu.getValidityPeriod());
break;
case PduUtils.TP_VPF_TIMESTAMP:
Date validityDate = pdu.getValidityDate();
if (validityDate == null) {
throw new IllegalArgumentException("Cannot get validity date for pdu");
}
writeTimeStampStringForDate(validityDate);
break;
}
// user data
// headers
writeUDData(pdu, mpRefNo, partNo);
}
// NOTE: the following are just for validation of the PduParser
// - there is no normal scenario where these are used
protected void generateSmsDeliverPduString(SmsDeliveryPdu pdu, int mpRefNo, int partNo) throws IOException {
// SMSC address info
writeSmscInfo(pdu);
// first octet
writeFirstOctet(pdu);
// originator address info
String address = pdu.getAddress();
if (address == null) {
throw new IllegalArgumentException("Address cannot be null");
}
writeAddress(address, pdu.getAddressType(), address.length());
// protocol id
writeByte(pdu.getProtocolIdentifier());
// data coding scheme
writeByte(pdu.getDataCodingScheme());
// timestamp
Date timestamp = pdu.getTimestamp();
if (timestamp != null) {
writeTimeStampStringForDate(timestamp);
}
// user data
// headers
writeUDData(pdu, mpRefNo, partNo);
}
protected void generateSmsStatusReportPduString(SmsStatusReportPdu pdu) throws IOException {
// SMSC address info
writeSmscInfo(pdu);
// first octet
writeFirstOctet(pdu);
// message reference
writeByte(pdu.getMessageReference());
// destination address info
String address = pdu.getAddress();
if (address == null) {
throw new IllegalArgumentException("Address cannot be null");
}
writeAddress(address, pdu.getAddressType(), address.length());
// timestamp
Date timestamp = pdu.getTimestamp();
if (timestamp == null) {
throw new IllegalArgumentException("cannot write null timestamp");
}
writeTimeStampStringForDate(timestamp);
// discharge time(timestamp)
Date dischargeTime = pdu.getDischargeTime();
if (dischargeTime == null) {
throw new IllegalArgumentException("cannot write null dischargeTime");
}
writeTimeStampStringForDate(dischargeTime);
// status
writeByte(pdu.getStatus());
}
}

View File

@@ -0,0 +1,343 @@
package org.smslib.pduUtils.gsm3040;
import java.util.Calendar;
import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.smslib.UnrecoverableSmslibException;
import org.smslib.message.MsIsdn;
import org.smslib.pduUtils.gsm3040.ie.InformationElement;
import org.smslib.pduUtils.gsm3040.ie.InformationElementFactory;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class PduParser {
// ==================================================
// RAW PDU PARSER
// ==================================================
// increments as methods are called
private int position;
private byte @Nullable [] pduByteArray;
// possible types of data
// BCD digits
// byte
// gsm-septets
// timestamp info
private int readByte() {
// read 8-bits forward
byte[] pduByteArrayFinal = this.pduByteArray;
if (pduByteArrayFinal == null) {
throw new UnrecoverableSmslibException("Cannot read byte from null data");
}
int retVal = pduByteArrayFinal[this.position] & 0xFF;
this.position++;
return retVal;
}
private int readSwappedNibbleBCDByte() {
// read 8-bits forward, swap the nibbles
int data = readByte();
data = PduUtils.swapNibbles((byte) data);
int retVal = 0;
retVal += ((data >>> 4) & 0xF) * 10;
retVal += ((data & 0xF));
return retVal;
}
private Calendar readTimeStamp() {
// reads timestamp info
// 7 bytes in semi-octet(BCD) style
int year = readSwappedNibbleBCDByte();
int month = readSwappedNibbleBCDByte();
int day = readSwappedNibbleBCDByte();
int hour = readSwappedNibbleBCDByte();
int minute = readSwappedNibbleBCDByte();
int second = readSwappedNibbleBCDByte();
// special treatment for timezone due to sign bit
// swap nibbles, clear the sign bit, convert remaining bits to BCD
int timestamp = readByte();
boolean negative = (timestamp & 0x08) == 0x08; // check bit 3
int timezone = PduUtils.swapNibbles(timestamp) & 0x7F; // remove last bit since this is just a sign
// time zone computation
TimeZone tz = null;
if (negative) {
// bit 3 of unswapped value represents the sign (1 == negative, 0 == positive)
// when swapped this will now be bit 7 (128)
int bcdTimeZone = 0;
bcdTimeZone += (((timezone >>> 4) & 0xF) * 10);
bcdTimeZone += ((timezone & 0xF));
timezone = bcdTimeZone;
int totalMinutes = timezone * 15;
int hours = totalMinutes / 60;
int minutes = totalMinutes % 60;
String gmtString = "GMT-" + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
// System.out.println(gmtString);
tz = TimeZone.getTimeZone(gmtString);
} else {
int bcdTimeZone = 0;
bcdTimeZone += ((timezone >>> 4) & 0xF) * 10;
bcdTimeZone += ((timezone & 0xF));
timezone = bcdTimeZone;
int totalMinutes = timezone * 15;
int hours = totalMinutes / 60;
int minutes = totalMinutes % 60;
String gmtString = "GMT+" + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
// System.out.println(gmtString);
tz = TimeZone.getTimeZone(gmtString);
}
Calendar cal = Calendar.getInstance(tz);
cal.set(Calendar.YEAR, year + 2000);
cal.set(Calendar.MONTH, month - 1);
cal.set(Calendar.DAY_OF_MONTH, day);
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, second);
return cal;
}
private @Nullable String readAddress(int addressLength, int addressType) {
// NOTE: the max number of octets on an address is 12 octets
// this means that an address field need only be 12 octets long
// what about for 7-bit? This would be 13 chars at 12 octets?
if (addressLength > 0) {
// length is a semi-octet count
int addressDataOctetLength = addressLength / 2 + ((addressLength % 2 == 1) ? 1 : 0);
// extract data and increment position
byte[] addressData = new byte[addressDataOctetLength];
byte[] pduByteArrayFinal = this.pduByteArray;
if (pduByteArrayFinal != null) {
System.arraycopy(pduByteArrayFinal, this.position, addressData, 0, addressDataOctetLength);
} else {
throw new UnrecoverableSmslibException("Cannot read address because pdu data is null");
}
this.position = this.position + addressDataOctetLength;
switch (PduUtils.extractAddressType(addressType)) {
case PduUtils.ADDRESS_TYPE_ALPHANUMERIC:
// extract and process encoded bytes
byte[] uncompressed = PduUtils.encodedSeptetsToUnencodedSeptets(addressData);
int septets = addressLength * 4 / 7;
byte[] choppedAddressData = new byte[septets];
System.arraycopy(uncompressed, 0, choppedAddressData, 0, septets);
return PduUtils.unencodedSeptetsToString(choppedAddressData);
default:
// process BCD style data any other
return PduUtils.readBCDNumbers(addressLength, addressData);
}
}
return null;
}
private int readValidityPeriodInt() {
// this will convert the VP to #MINUTES
int validity = readByte();
int minutes = 0;
if ((validity > 0) && (validity <= 143)) {
// groups of 5 min
minutes = (validity + 1) * 5;
} else if ((validity > 143) && (validity <= 167)) {
// groups of 30 min + 12 hrs
minutes = (12 * 60) + (validity - 143) * 30;
} else if ((validity > 167) && (validity <= 196)) {
// days
minutes = (validity - 166) * 24 * 60;
} else if ((validity > 197) && (validity <= 255)) {
// weeks
minutes = (validity - 192) * 7 * 24 * 60;
}
return minutes;
}
public Pdu parsePdu(String rawPdu) {
// encode pdu to byte[] for easier processing
this.pduByteArray = PduUtils.pduToBytes(rawPdu);
this.position = 0;
// parse start and determine what type of pdu it is
Pdu pdu = parseStart();
// parse depending on the pdu type
switch (pdu.getTpMti()) {
case PduUtils.TP_MTI_SMS_DELIVER:
parseSmsDeliverMessage((SmsDeliveryPdu) pdu);
break;
case PduUtils.TP_MTI_SMS_SUBMIT:
parseSmsSubmitMessage((SmsSubmitPdu) pdu);
break;
case PduUtils.TP_MTI_SMS_STATUS_REPORT:
parseSmsStatusReportMessage((SmsStatusReportPdu) pdu);
break;
}
return pdu;
}
private Pdu parseStart() {
// SMSC info
// length
// address type
// smsc data
int addressLength = readByte();
Pdu pdu = null;
if (addressLength > 0) {
int addressType = readByte();
String smscAddress = readAddress((addressLength - 1) * 2, addressType);
// first octet - determine how to parse and how to store
int firstOctet = readByte();
pdu = PduFactory.createPdu(firstOctet);
// generic methods
pdu.setSmscAddressType(addressType);
pdu.setSmscAddress(smscAddress);
pdu.setSmscInfoLength(addressLength);
} else {
// first octet - determine how to parse and how to store
int firstOctet = readByte();
pdu = PduFactory.createPdu(firstOctet);
}
return pdu;
}
private void parseUserData(Pdu pdu) {
// ud length
// NOTE: - the udLength value is just stored, it is not used to determine the length
// of the remaining data (it may be a septet length not an octet length)
// - parser just assumes that the remaining PDU data is for the User-Data field
int udLength = readByte();
pdu.setUDLength(udLength);
// user data
// NOTE: UD Data does not contain the length octet
byte[] pduByteArrayFinal = this.pduByteArray;
if (pduByteArrayFinal != null) {
int udOctetLength = pduByteArrayFinal.length - this.position;
byte[] udData = new byte[udOctetLength];
System.arraycopy(pduByteArrayFinal, this.position, udData, 0, udOctetLength);
// save the UD data
pdu.setUDData(udData);
} else {
throw new UnrecoverableSmslibException("Cannot parse user data because pdu data is null");
}
// user data header (if present)
// position is still at the start of the UD
if (pdu.hasTpUdhi()) {
// udh length
int udhLength = readByte();
// udh data (iterate till udh is consumed)
// iei id
// iei data length
// iei data
int endUdh = this.position + udhLength;
while (this.position < endUdh) {
int iei = readByte();
int iedl = readByte();
byte[] ieData = new byte[iedl];
System.arraycopy(pduByteArrayFinal, this.position, ieData, 0, iedl);
InformationElement ie = InformationElementFactory.createInformationElement(iei, ieData);
pdu.addInformationElement(ie);
this.position = this.position + iedl;
if (this.position > endUdh) {
// at the end, position after adding should be exactly at endUdh
throw new UnrecoverableSmslibException(
"UDH is shorter than expected endUdh=" + endUdh + ", position=" + this.position);
}
}
}
}
private void parseSmsDeliverMessage(SmsDeliveryPdu pdu) {
// originator address info
// address length
// type of address
// address data
int addressLength = readByte();
int addressType = readByte();
String originatorAddress = readAddress(addressLength, addressType);
pdu.setAddressType(addressType);
if (originatorAddress != null) {
pdu.setAddress(new MsIsdn(originatorAddress));
}
// protocol id
int protocolId = readByte();
pdu.setProtocolIdentifier(protocolId);
// data coding scheme
int dcs = readByte();
pdu.setDataCodingScheme(dcs);
// timestamp
Calendar timestamp = readTimeStamp();
pdu.setTimestamp(timestamp);
// user data
parseUserData(pdu);
}
private void parseSmsStatusReportMessage(SmsStatusReportPdu pdu) {
// message reference
int messageReference = readByte();
pdu.setMessageReference(messageReference);
// destination address info
int addressLength = readByte();
int addressType = readByte();
String destinationAddress = readAddress(addressLength, addressType);
pdu.setAddressType(addressType);
pdu.setAddress(new MsIsdn(destinationAddress));
// timestamp
Calendar timestamp = readTimeStamp();
pdu.setTimestamp(timestamp);
// discharge time(timestamp)
Calendar timestamp2 = readTimeStamp();
pdu.setDischargeTime(timestamp2);
// status
int status = readByte();
pdu.setStatus(status);
}
// NOTE: the following is just for validation of the PduGenerator
// - there is no normal scenario where this is used
private void parseSmsSubmitMessage(SmsSubmitPdu pdu) {
// message reference
int messageReference = readByte();
pdu.setMessageReference(messageReference);
// destination address info
int addressLength = readByte();
int addressType = readByte();
String destinationAddress = readAddress(addressLength, addressType);
pdu.setAddressType(addressType);
pdu.setAddress(new MsIsdn(destinationAddress));
// protocol id
int protocolId = readByte();
pdu.setProtocolIdentifier(protocolId);
// data coding scheme
int dcs = readByte();
pdu.setDataCodingScheme(dcs);
// validity period
switch (pdu.getTpVpf()) {
case PduUtils.TP_VPF_NONE:
break;
case PduUtils.TP_VPF_INTEGER:
int validityInt = readValidityPeriodInt();
pdu.setValidityPeriod(validityInt / 60); // pdu assumes hours
break;
case PduUtils.TP_VPF_TIMESTAMP:
Calendar validityDate = readTimeStamp();
pdu.setValidityTimestamp(validityDate);
break;
}
parseUserData(pdu);
}
}

View File

@@ -0,0 +1,761 @@
package org.smslib.pduUtils.gsm3040;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.BitSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.smslib.UnrecoverableSmslibException;
import org.smslib.message.MsIsdn;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class PduUtils {
// ==================================================
// GSM ALPHABET
// ==================================================
private static final char[][] grcAlphabetRemapping = { { '\u0386', '\u0041' }, // GREEK CAPITAL LETTER ALPHA WITH
// TONOS
{ '\u0388', '\u0045' }, // GREEK CAPITAL LETTER EPSILON WITH TONOS
{ '\u0389', '\u0048' }, // GREEK CAPITAL LETTER ETA WITH TONOS
{ '\u038A', '\u0049' }, // GREEK CAPITAL LETTER IOTA WITH TONOS
{ '\u038C', '\u004F' }, // GREEK CAPITAL LETTER OMICRON WITH TONOS
{ '\u038E', '\u0059' }, // GREEK CAPITAL LETTER UPSILON WITH TONOS
{ '\u038F', '\u03A9' }, // GREEK CAPITAL LETTER OMEGA WITH TONOS
{ '\u0390', '\u0049' }, // GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS
{ '\u0391', '\u0041' }, // GREEK CAPITAL LETTER ALPHA
{ '\u0392', '\u0042' }, // GREEK CAPITAL LETTER BETA
{ '\u0393', '\u0393' }, // GREEK CAPITAL LETTER GAMMA
{ '\u0394', '\u0394' }, // GREEK CAPITAL LETTER DELTA
{ '\u0395', '\u0045' }, // GREEK CAPITAL LETTER EPSILON
{ '\u0396', '\u005A' }, // GREEK CAPITAL LETTER ZETA
{ '\u0397', '\u0048' }, // GREEK CAPITAL LETTER ETA
{ '\u0398', '\u0398' }, // GREEK CAPITAL LETTER THETA
{ '\u0399', '\u0049' }, // GREEK CAPITAL LETTER IOTA
{ '\u039A', '\u004B' }, // GREEK CAPITAL LETTER KAPPA
{ '\u039B', '\u039B' }, // GREEK CAPITAL LETTER LAMDA
{ '\u039C', '\u004D' }, // GREEK CAPITAL LETTER MU
{ '\u039D', '\u004E' }, // GREEK CAPITAL LETTER NU
{ '\u039E', '\u039E' }, // GREEK CAPITAL LETTER XI
{ '\u039F', '\u004F' }, // GREEK CAPITAL LETTER OMICRON
{ '\u03A0', '\u03A0' }, // GREEK CAPITAL LETTER PI
{ '\u03A1', '\u0050' }, // GREEK CAPITAL LETTER RHO
{ '\u03A3', '\u03A3' }, // GREEK CAPITAL LETTER SIGMA
{ '\u03A4', '\u0054' }, // GREEK CAPITAL LETTER TAU
{ '\u03A5', '\u0059' }, // GREEK CAPITAL LETTER UPSILON
{ '\u03A6', '\u03A6' }, // GREEK CAPITAL LETTER PHI
{ '\u03A7', '\u0058' }, // GREEK CAPITAL LETTER CHI
{ '\u03A8', '\u03A8' }, // GREEK CAPITAL LETTER PSI
{ '\u03A9', '\u03A9' }, // GREEK CAPITAL LETTER OMEGA
{ '\u03AA', '\u0049' }, // GREEK CAPITAL LETTER IOTA WITH DIALYTIKA
{ '\u03AB', '\u0059' }, // GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA
{ '\u03AC', '\u0041' }, // GREEK SMALL LETTER ALPHA WITH TONOS
{ '\u03AD', '\u0045' }, // GREEK SMALL LETTER EPSILON WITH TONOS
{ '\u03AE', '\u0048' }, // GREEK SMALL LETTER ETA WITH TONOS
{ '\u03AF', '\u0049' }, // GREEK SMALL LETTER IOTA WITH TONOS
{ '\u03B0', '\u0059' }, // GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS
{ '\u03B1', '\u0041' }, // GREEK SMALL LETTER ALPHA
{ '\u03B2', '\u0042' }, // GREEK SMALL LETTER BETA
{ '\u03B3', '\u0393' }, // GREEK SMALL LETTER GAMMA
{ '\u03B4', '\u0394' }, // GREEK SMALL LETTER DELTA
{ '\u03B5', '\u0045' }, // GREEK SMALL LETTER EPSILON
{ '\u03B6', '\u005A' }, // GREEK SMALL LETTER ZETA
{ '\u03B7', '\u0048' }, // GREEK SMALL LETTER ETA
{ '\u03B8', '\u0398' }, // GREEK SMALL LETTER THETA
{ '\u03B9', '\u0049' }, // GREEK SMALL LETTER IOTA
{ '\u03BA', '\u004B' }, // GREEK SMALL LETTER KAPPA
{ '\u03BB', '\u039B' }, // GREEK SMALL LETTER LAMDA
{ '\u03BC', '\u004D' }, // GREEK SMALL LETTER MU
{ '\u03BD', '\u004E' }, // GREEK SMALL LETTER NU
{ '\u03BE', '\u039E' }, // GREEK SMALL LETTER XI
{ '\u03BF', '\u004F' }, // GREEK SMALL LETTER OMICRON
{ '\u03C0', '\u03A0' }, // GREEK SMALL LETTER PI
{ '\u03C1', '\u0050' }, // GREEK SMALL LETTER RHO
{ '\u03C2', '\u03A3' }, // GREEK SMALL LETTER FINAL SIGMA
{ '\u03C3', '\u03A3' }, // GREEK SMALL LETTER SIGMA
{ '\u03C4', '\u0054' }, // GREEK SMALL LETTER TAU
{ '\u03C5', '\u0059' }, // GREEK SMALL LETTER UPSILON
{ '\u03C6', '\u03A6' }, // GREEK SMALL LETTER PHI
{ '\u03C7', '\u0058' }, // GREEK SMALL LETTER CHI
{ '\u03C8', '\u03A8' }, // GREEK SMALL LETTER PSI
{ '\u03C9', '\u03A9' }, // GREEK SMALL LETTER OMEGA
{ '\u03CA', '\u0049' }, // GREEK SMALL LETTER IOTA WITH DIALYTIKA
{ '\u03CB', '\u0059' }, // GREEK SMALL LETTER UPSILON WITH DIALYTIKA
{ '\u03CC', '\u004F' }, // GREEK SMALL LETTER OMICRON WITH TONOS
{ '\u03CD', '\u0059' }, // GREEK SMALL LETTER UPSILON WITH TONOS
{ '\u03CE', '\u03A9' } // GREEK SMALL LETTER OMEGA WITH TONOS
};
private static final char[] extAlphabet = { '\u000c', // FORM FEED
'\u005e', // CIRCUMFLEX ACCENT
'\u007b', // LEFT CURLY BRACKET
'\u007d', // RIGHT CURLY BRACKET
'\\', // REVERSE SOLIDUS
'\u005b', // LEFT SQUARE BRACKET
'\u007e', // TILDE
'\u005d', // RIGHT SQUARE BRACKET
'\u007c', // VERTICAL LINES
'\u20ac', // EURO SIGN
};
private static final String[] extBytes = { "1b0a", // FORM FEED
"1b14", // CIRCUMFLEX ACCENT
"1b28", // LEFT CURLY BRACKET
"1b29", // RIGHT CURLY BRACKET
"1b2f", // REVERSE SOLIDUS
"1b3c", // LEFT SQUARE BRACKET
"1b3d", // TILDE
"1b3e", // RIGHT SQUARE BRACKET
"1b40", // VERTICAL LINES
"1b65", // EURO SIGN
};
// NOTE: this is an adjustment required to compensate for
// multi-byte characters split across the end of a pdu part
// if the previous part is noted to be ending in a '1b'
// call this method on the first char of the next part
// to adjust it for the missing '1b'
public static String getMultiCharFor(char c) {
switch (c) {
// GSM 0x0A (line feed) ==> form feed
case '\n':
return "'\u000c'";
// GSM 0x14 (greek capital lamda) ==> circumflex
case '\u039B':
return "^";
// GSM 0x28 (left parenthesis) ==> left curly brace
case '(':
return "{";
// GSM 0x29 (right parenthesis) ==> right curly brace
case ')':
return "}";
// GSM 0x2f (solidus or slash) ==> reverse solidus or backslash
case '/':
return "\\";
// GSM 0x3c (less than sign) ==> left square bracket
case '<':
return "[";
// GSM 0x3d (equals sign) ==> tilde
case '=':
return "~";
// GSM 0x3e (greater than sign) ==> right square bracket
case '>':
return "]";
// GSM 0x40 (inverted exclamation point) ==> pipe
case '\u00A1':
return "|";
// GSM 0x65 (latin small e) ==> euro
case 'e':
return "\u20ac";
}
return "";
}
private static final char[] stdAlphabet = { '\u0040', // COMMERCIAL AT
'\u00A3', // POUND SIGN
'\u0024', // DOLLAR SIGN
'\u00A5', // YEN SIGN
'\u00E8', // LATIN SMALL LETTER E WITH GRAVE
'\u00E9', // LATIN SMALL LETTER E WITH ACUTE
'\u00F9', // LATIN SMALL LETTER U WITH GRAVE
'\u00EC', // LATIN SMALL LETTER I WITH GRAVE
'\u00F2', // LATIN SMALL LETTER O WITH GRAVE
'\u00E7', // LATIN SMALL LETTER C WITH CEDILLA
'\n', // LINE FEED
'\u00D8', // LATIN CAPITAL LETTER O WITH STROKE
'\u00F8', // LATIN SMALL LETTER O WITH STROKE
'\r', // CARRIAGE RETURN
'\u00C5', // LATIN CAPITAL LETTER A WITH RING ABOVE
'\u00E5', // LATIN SMALL LETTER A WITH RING ABOVE
'\u0394', // GREEK CAPITAL LETTER DELTA
'\u005F', // LOW LINE
'\u03A6', // GREEK CAPITAL LETTER PHI
'\u0393', // GREEK CAPITAL LETTER GAMMA
'\u039B', // GREEK CAPITAL LETTER LAMDA
'\u03A9', // GREEK CAPITAL LETTER OMEGA
'\u03A0', // GREEK CAPITAL LETTER PI
'\u03A8', // GREEK CAPITAL LETTER PSI
'\u03A3', // GREEK CAPITAL LETTER SIGMA
'\u0398', // GREEK CAPITAL LETTER THETA
'\u039E', // GREEK CAPITAL LETTER XI
'\u00A0', // ESCAPE TO EXTENSION TABLE (or displayed as NBSP, see
// note
// above)
'\u00C6', // LATIN CAPITAL LETTER AE
'\u00E6', // LATIN SMALL LETTER AE
'\u00DF', // LATIN SMALL LETTER SHARP S (German)
'\u00C9', // LATIN CAPITAL LETTER E WITH ACUTE
'\u0020', // SPACE
'\u0021', // EXCLAMATION MARK
'\u0022', // QUOTATION MARK
'\u0023', // NUMBER SIGN
'\u00A4', // CURRENCY SIGN
'\u0025', // PERCENT SIGN
'\u0026', // AMPERSAND
'\'', // APOSTROPHE
'\u0028', // LEFT PARENTHESIS
'\u0029', // RIGHT PARENTHESIS
'\u002A', // ASTERISK
'\u002B', // PLUS SIGN
'\u002C', // COMMA
'\u002D', // HYPHEN-MINUS
'\u002E', // FULL STOP
'\u002F', // SOLIDUS
'\u0030', // DIGIT ZERO
'\u0031', // DIGIT ONE
'\u0032', // DIGIT TWO
'\u0033', // DIGIT THREE
'\u0034', // DIGIT FOUR
'\u0035', // DIGIT FIVE
'\u0036', // DIGIT SIX
'\u0037', // DIGIT SEVEN
'\u0038', // DIGIT EIGHT
'\u0039', // DIGIT NINE
'\u003A', // COLON
'\u003B', // SEMICOLON
'\u003C', // LESS-THAN SIGN
'\u003D', // EQUALS SIGN
'\u003E', // GREATER-THAN SIGN
'\u003F', // QUESTION MARK
'\u00A1', // INVERTED EXCLAMATION MARK
'\u0041', // LATIN CAPITAL LETTER A
'\u0042', // LATIN CAPITAL LETTER B
'\u0043', // LATIN CAPITAL LETTER C
'\u0044', // LATIN CAPITAL LETTER D
'\u0045', // LATIN CAPITAL LETTER E
'\u0046', // LATIN CAPITAL LETTER F
'\u0047', // LATIN CAPITAL LETTER G
'\u0048', // LATIN CAPITAL LETTER H
'\u0049', // LATIN CAPITAL LETTER I
'\u004A', // LATIN CAPITAL LETTER J
'\u004B', // LATIN CAPITAL LETTER K
'\u004C', // LATIN CAPITAL LETTER L
'\u004D', // LATIN CAPITAL LETTER M
'\u004E', // LATIN CAPITAL LETTER N
'\u004F', // LATIN CAPITAL LETTER O
'\u0050', // LATIN CAPITAL LETTER P
'\u0051', // LATIN CAPITAL LETTER Q
'\u0052', // LATIN CAPITAL LETTER R
'\u0053', // LATIN CAPITAL LETTER S
'\u0054', // LATIN CAPITAL LETTER T
'\u0055', // LATIN CAPITAL LETTER U
'\u0056', // LATIN CAPITAL LETTER V
'\u0057', // LATIN CAPITAL LETTER W
'\u0058', // LATIN CAPITAL LETTER X
'\u0059', // LATIN CAPITAL LETTER Y
'\u005A', // LATIN CAPITAL LETTER Z
'\u00C4', // LATIN CAPITAL LETTER A WITH DIAERESIS
'\u00D6', // LATIN CAPITAL LETTER O WITH DIAERESIS
'\u00D1', // LATIN CAPITAL LETTER N WITH TILDE
'\u00DC', // LATIN CAPITAL LETTER U WITH DIAERESIS
'\u00A7', // SECTION SIGN
'\u00BF', // INVERTED QUESTION MARK
'\u0061', // LATIN SMALL LETTER A
'\u0062', // LATIN SMALL LETTER B
'\u0063', // LATIN SMALL LETTER C
'\u0064', // LATIN SMALL LETTER D
'\u0065', // LATIN SMALL LETTER E
'\u0066', // LATIN SMALL LETTER F
'\u0067', // LATIN SMALL LETTER G
'\u0068', // LATIN SMALL LETTER H
'\u0069', // LATIN SMALL LETTER I
'\u006A', // LATIN SMALL LETTER J
'\u006B', // LATIN SMALL LETTER K
'\u006C', // LATIN SMALL LETTER L
'\u006D', // LATIN SMALL LETTER M
'\u006E', // LATIN SMALL LETTER N
'\u006F', // LATIN SMALL LETTER O
'\u0070', // LATIN SMALL LETTER P
'\u0071', // LATIN SMALL LETTER Q
'\u0072', // LATIN SMALL LETTER R
'\u0073', // LATIN SMALL LETTER S
'\u0074', // LATIN SMALL LETTER T
'\u0075', // LATIN SMALL LETTER U
'\u0076', // LATIN SMALL LETTER V
'\u0077', // LATIN SMALL LETTER W
'\u0078', // LATIN SMALL LETTER X
'\u0079', // LATIN SMALL LETTER Y
'\u007A', // LATIN SMALL LETTER Z
'\u00E4', // LATIN SMALL LETTER A WITH DIAERESIS
'\u00F6', // LATIN SMALL LETTER O WITH DIAERESIS
'\u00F1', // LATIN SMALL LETTER N WITH TILDE
'\u00FC', // LATIN SMALL LETTER U WITH DIAERESIS
'\u00E0', // LATIN SMALL LETTER A WITH GRAVE
};
// ==================================================
// FIRST OCTET CONSTANTS
// ==================================================
// to add, use the & with MASK to clear bits on original value
// and | this cleared value with constant specified
// TP-MTI xxxxxx00 = SMS-DELIVER
// xxxxxx10 = SMS-STATUS-REPORT
// xxxxxx01 = SMS-SUBMIT
public static final int TP_MTI_MASK = 0xFC;
public static final int TP_MTI_SMS_DELIVER = 0x00;
public static final int TP_MTI_SMS_SUBMIT = 0x01;
public static final int TP_MTI_SMS_STATUS_REPORT = 0x02;
// TP-RD xxxxx0xx = accept duplicate messages
// xxxxx1xx = reject duplicate messages
// for SMS-SUBMIT only
public static final int TP_RD_ACCEPT_DUPLICATES = 0x00;
// TP-VPF xxx00xxx = no validity period
// xxx10xxx = validity period integer-representation
// xxx11xxx = validity period timestamp-representation
public static final int TP_VPF_MASK = 0xE7;
public static final int TP_VPF_NONE = 0x00;
public static final int TP_VPF_INTEGER = 0x10;
public static final int TP_VPF_TIMESTAMP = 0x18;
// TP-SRI xx0xxxxx = no status report to SME (for SMS-DELIVER only)
// xx1xxxxx = status report to SME
public static final int TP_SRI_MASK = 0xDF;
// TP-SRR xx0xxxxx = no status report (for SMS-SUBMIT only)
// xx1xxxxx = status report
public static final int TP_SRR_NO_REPORT = 0x00;
public static final int TP_SRR_REPORT = 0x20;
// TP-UDHI x0xxxxxx = no UDH
// x1xxxxxx = UDH present
public static final int TP_UDHI_MASK = 0xBF;
public static final int TP_UDHI_NO_UDH = 0x00;
public static final int TP_UDHI_WITH_UDH = 0x40;
// ==================================================
// ADDRESS-TYPE CONSTANTS
// ==================================================
// some typical ones used for sending, though receiving may get other types
// usually 1 001 0001 (0x91) international format
// 1 000 0001 (0x81) (unknown) short number (e.g. access codes)
// 1 101 0000 (0xD0) alphanumeric (e.g. access code names like PasaLoad)
public static final int ADDRESS_NUMBER_PLAN_ID_TELEPHONE = 0x01;
public static final int ADDRESS_TYPE_MASK = 0x70;
public static final int ADDRESS_TYPE_UNKNOWN = 0x00;
public static final int ADDRESS_TYPE_INTERNATIONAL = 0x10;
public static final int ADDRESS_TYPE_NATIONAL = 0x20;
public static final int ADDRESS_TYPE_ALPHANUMERIC = 0x50;
public static int getAddressTypeFor(MsIsdn number) {
switch (number.getType()) {
case International:
return createAddressType(ADDRESS_TYPE_INTERNATIONAL | ADDRESS_NUMBER_PLAN_ID_TELEPHONE);
case National:
return createAddressType(ADDRESS_TYPE_NATIONAL | ADDRESS_NUMBER_PLAN_ID_TELEPHONE);
default:
return createAddressType(ADDRESS_TYPE_UNKNOWN | ADDRESS_NUMBER_PLAN_ID_TELEPHONE);
}
}
public static int extractAddressType(int addressType) {
return addressType & ADDRESS_TYPE_MASK;
}
public static int createAddressType(int addressType) {
// last bit is always set
return 0x80 | addressType;
}
// ==================================================
// DCS ENCODING CONSTANTS
// ==================================================
public static final int DCS_CODING_GROUP_MASK = 0x0F;
public static final int DCS_CODING_GROUP_DATA = 0xF0;
public static final int DCS_CODING_GROUP_GENERAL = 0xC0;
public static final int DCS_ENCODING_MASK = 0xF3;
public static final int DCS_ENCODING_7BIT = 0x00;
public static final int DCS_ENCODING_8BIT = 0x04;
public static final int DCS_ENCODING_UCS2 = 0x08;
public static final int DCS_MESSAGE_CLASS_MASK = 0xEC;
public static final int DCS_MESSAGE_CLASS_FLASH = 0x10;
public static final int DCS_MESSAGE_CLASS_ME = 0x11;
public static final int DCS_MESSAGE_CLASS_SIM = 0x12;
public static final int DCS_MESSAGE_CLASS_TE = 0x13;
public static int extractDcsEncoding(int dataCodingScheme) {
return dataCodingScheme & ~PduUtils.DCS_ENCODING_MASK;
}
public static int extractDcsClass(int dataCodingScheme) {
return dataCodingScheme & ~DCS_MESSAGE_CLASS_MASK;
}
public static int extractDcsFlash(int dataCodingScheme) {
// this is only useful if DCS != 0
return dataCodingScheme & ~DCS_MESSAGE_CLASS_MASK;
}
public static String decodeDataCodingScheme(Pdu pdu) {
StringBuffer sb = new StringBuffer();
switch (PduUtils.extractDcsEncoding(pdu.getDataCodingScheme())) {
case PduUtils.DCS_ENCODING_7BIT:
sb.append("7-bit GSM Alphabet");
break;
case PduUtils.DCS_ENCODING_8BIT:
sb.append("8-bit encoding");
break;
case PduUtils.DCS_ENCODING_UCS2:
sb.append("UCS2 encoding");
break;
}
// are flash messages are only applicable to general coding group?
if ((pdu.getDataCodingScheme() & ~PduUtils.DCS_CODING_GROUP_GENERAL) == 0) {
switch (PduUtils.extractDcsClass(pdu.getDataCodingScheme())) {
case PduUtils.DCS_MESSAGE_CLASS_FLASH:
sb.append(", (Flash Message)");
break;
case PduUtils.DCS_MESSAGE_CLASS_ME:
sb.append(", (Class1 ME Message)");
break;
case PduUtils.DCS_MESSAGE_CLASS_SIM:
sb.append(", (Class2 SIM Message)");
break;
case PduUtils.DCS_MESSAGE_CLASS_TE:
sb.append(", (Class3 TE Message)");
break;
}
}
return sb.toString();
}
public static byte[] encode8bitUserData(String text) {
try {
return text.getBytes("ISO8859_1");
} catch (UnsupportedEncodingException e) {
throw new UnrecoverableSmslibException("Cannot encode user data", e);
}
}
public static byte[] encodeUcs2UserData(String text) {
try {
// UTF-16 Big-Endian, no Byte Order Marker at start
return text.getBytes("UTF-16BE");
} catch (UnsupportedEncodingException e) {
throw new UnrecoverableSmslibException("Cannot encode user data", e);
}
}
public static byte[] encode7bitUserData(byte @Nullable [] udhOctets, byte[] textSeptets) {
// UDH octets and text have to be encoded together in a single pass
// UDH octets will need to be converted to unencoded septets in order
// to properly pad the data
if (udhOctets == null) {
// convert string to uncompressed septets
return unencodedSeptetsToEncodedSeptets(textSeptets);
}
// convert UDH octets as if they were encoded septets
// NOTE: DO NOT DISCARD THE LAST SEPTET IF IT IS ZERO
byte[] udhSeptets = PduUtils.encodedSeptetsToUnencodedSeptets(udhOctets, false);
// combine the two arrays and encode them as a whole
byte[] combined = new byte[udhSeptets.length + textSeptets.length];
System.arraycopy(udhSeptets, 0, combined, 0, udhSeptets.length);
System.arraycopy(textSeptets, 0, combined, udhSeptets.length, textSeptets.length);
// convert encoded byte[] to a PDU string
return unencodedSeptetsToEncodedSeptets(combined);
}
public static String decode8bitEncoding(byte @Nullable [] udhData, byte[] pduData) {
// standard 8-bit characters
try {
int udhLength = ((udhData == null) ? 0 : udhData.length);
return new String(pduData, udhLength, pduData.length - udhLength, "ISO8859_1");
} catch (UnsupportedEncodingException e) {
throw new UnrecoverableSmslibException("Cannot decode user data", e);
}
}
public static String decodeUcs2Encoding(byte @Nullable [] udhData, byte[] pduData) {
try {
int udhLength = ((udhData == null) ? 0 : udhData.length);
// standard unicode
return new String(pduData, udhLength, pduData.length - udhLength, "UTF-16");
} catch (UnsupportedEncodingException e) {
throw new UnrecoverableSmslibException("Cannot decode user data", e);
}
}
public static byte swapNibbles(int b) {
return (byte) (((b << 4) & 0xF0) | ((b >>> 4) & 0x0F));
}
public static String readBCDNumbers(int numDigits, byte[] addressData) {
// reads length BCD numbers from the current position
StringBuffer sb = new StringBuffer();
for (int i = 0; i < addressData.length; i++) {
int b = addressData[i];
int num1 = b & 0x0F;
sb.append(num1);
int num2 = (b >>> 4) & 0x0F;
if (num2 != 0x0F) {
// check if fillbits
sb.append(num2);
}
}
return sb.toString();
}
public static int createSwappedBCD(int decimal) {
// creates a swapped BCD representation of a 2-digit decimal
int tens = (decimal & 0xFF) / 10;
int ones = (decimal & 0xFF) - (tens * 10);
return (ones << 4) | tens;
}
// from Java String to uncompressed septets (GSM characters)
public static byte[] stringToUnencodedSeptets(String s) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i, j, index;
char ch;
String myS = s;
myS = myS.replace('\u00C7', // LATIN CAPITAL LETTER C WITH CEDILLA
'\u00E7' // LATIN SMALL LETTER C WITH CEDILLA
);
for (i = 0; i < myS.length(); i++) {
ch = myS.charAt(i);
index = -1;
for (j = 0; j < extAlphabet.length; j++) {
if (extAlphabet[j] == ch) {
index = j;
break;
}
}
if (index != -1) // An extended char...
{
baos.write((byte) Integer.parseInt(extBytes[index].substring(0, 2), 16));
baos.write((byte) Integer.parseInt(extBytes[index].substring(2, 4), 16));
} else
// Maybe a standard char...
{
index = -1;
for (j = 0; j < stdAlphabet.length; j++) {
if (stdAlphabet[j] == ch) {
index = j;
baos.write((byte) j);
break;
}
}
if (index == -1) // Maybe a Greek Char...
{
for (j = 0; j < grcAlphabetRemapping.length; j++) {
if (grcAlphabetRemapping[j][0] == ch) {
index = j;
ch = grcAlphabetRemapping[j][1];
break;
}
}
if (index != -1) {
for (j = 0; j < stdAlphabet.length; j++) {
if (stdAlphabet[j] == ch) {
index = j;
baos.write((byte) j);
break;
}
}
} else
// Unknown char replacement...
{
baos.write((byte) ' ');
}
}
}
}
return baos.toByteArray();
}
// from compress unencoded septets
public static byte[] unencodedSeptetsToEncodedSeptets(byte[] septetBytes) {
byte[] txtBytes;
byte[] txtSeptets;
int txtBytesLen;
BitSet bits;
int i, j;
txtBytes = septetBytes;
txtBytesLen = txtBytes.length;
bits = new BitSet();
for (i = 0; i < txtBytesLen; i++) {
for (j = 0; j < 7; j++) {
if ((txtBytes[i] & (1 << j)) != 0) {
bits.set((i * 7) + j);
}
}
}
// big diff here
int encodedSeptetByteArrayLength = txtBytesLen * 7 / 8 + ((txtBytesLen * 7 % 8 != 0) ? 1 : 0);
txtSeptets = new byte[encodedSeptetByteArrayLength];
for (i = 0; i < encodedSeptetByteArrayLength; i++) {
for (j = 0; j < 8; j++) {
txtSeptets[i] |= (byte) ((bits.get((i * 8) + j) ? 1 : 0) << j);
}
}
return txtSeptets;
}
// from GSM characters to java string
public static String unencodedSeptetsToString(byte[] bytes) {
StringBuffer text;
String extChar;
int i, j;
text = new StringBuffer();
for (i = 0; i < bytes.length; i++) {
if (bytes[i] == 0x1b) {
// NOTE: - ++i can be a problem if the '1b'
// is right at the end of a PDU
// - this will be an issue for displaying
// partial PDUs e.g. via toString()
if (i < bytes.length - 1) {
extChar = "1b" + Integer.toHexString(bytes[++i]);
for (j = 0; j < extBytes.length; j++) {
if (extBytes[j].equalsIgnoreCase(extChar)) {
text.append(extAlphabet[j]);
}
}
}
} else {
text.append(stdAlphabet[bytes[i]]);
}
}
return text.toString();
}
public static int getNumSeptetsForOctets(int numOctets) {
return numOctets * 8 / 7 + ((numOctets * 8 % 7 != 0) ? 1 : 0);
// return numOctets + (numOctets/7);
}
// decompress encoded septets to unencoded form
public static byte[] encodedSeptetsToUnencodedSeptets(byte[] octetBytes) {
return encodedSeptetsToUnencodedSeptets(octetBytes, true);
}
public static byte[] encodedSeptetsToUnencodedSeptets(byte[] octetBytes, boolean discardLast) {
byte newBytes[];
BitSet bitSet;
int i, j, value1, value2;
bitSet = new BitSet(octetBytes.length * 8);
value1 = 0;
for (i = 0; i < octetBytes.length; i++) {
for (j = 0; j < 8; j++) {
value1 = (i * 8) + j;
if ((octetBytes[i] & (1 << j)) != 0) {
bitSet.set(value1);
}
}
}
value1++;
// this is a bit count NOT a byte count
value2 = value1 / 7 + ((value1 % 7 != 0) ? 1 : 0); // big diff here
// System.out.println(octetBytes.length);
// System.out.println(value1+" --> "+value2);
if (value2 == 0) {
value2++;
}
newBytes = new byte[value2];
for (i = 0; i < value2; i++) {
for (j = 0; j < 7; j++) {
if ((value1 + 1) > (i * 7 + j)) {
if (bitSet.get(i * 7 + j)) {
newBytes[i] |= (byte) (1 << j);
}
}
}
}
if (discardLast && octetBytes.length * 8 % 7 > 0) {
// when decoding a 7bit encoded string
// the last septet may become 0, this should be discarded
// since this is an artifact of the encoding not part of the
// original string
// this is only done for decoding 7bit encoded text NOT for
// reversing octets to septets (e.g. for the encoding the UDH)
if (newBytes[newBytes.length - 1] == 0) {
byte[] retVal = new byte[newBytes.length - 1];
System.arraycopy(newBytes, 0, retVal, 0, retVal.length);
return retVal;
}
}
return newBytes;
}
// converts a PDU style string to a byte array
public static byte[] pduToBytes(String s) {
byte[] bytes = new byte[s.length() / 2];
for (int i = 0; i < s.length(); i += 2) {
bytes[i / 2] = (byte) (Integer.parseInt(s.substring(i, i + 2), 16));
}
return bytes;
}
// converts a byte array to PDU style string
public static String bytesToPdu(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
sb.append(byteToPdu(bytes[i] & 0xFF));
}
return sb.toString();
}
public static String byteToBits(byte b) {
String bits = Integer.toBinaryString(b & 0xFF);
while (bits.length() < 8) {
bits = "0" + bits;
}
return bits;
}
public static String byteToPdu(int b) {
StringBuffer sb = new StringBuffer();
String s = Integer.toHexString(b & 0xFF);
if (s.length() == 1) {
sb.append("0");
}
sb.append(s);
return sb.toString().toUpperCase();
}
}

View File

@@ -0,0 +1,48 @@
package org.smslib.pduUtils.gsm3040;
import java.util.Calendar;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class SmsDeliveryPdu extends Pdu {
// can only create via the factory
SmsDeliveryPdu() {
}
// ==================================================
// TIMESTAMP
// ==================================================
private @Nullable Calendar timestamp;
public void setTimestamp(Calendar timestamp) {
this.timestamp = timestamp;
}
public @Nullable Date getTimestamp() {
Calendar timestampFinal = this.timestamp;
return timestampFinal == null ? null : timestampFinal.getTime();
}
}

View File

@@ -0,0 +1,89 @@
package org.smslib.pduUtils.gsm3040;
import java.util.Calendar;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class SmsStatusReportPdu extends Pdu {
// can only create via the factory
SmsStatusReportPdu() {
}
// ==================================================
// MESSAGE REFERENCE
// ==================================================
// usually just 0x00 to let MC supply
private int messageReference = 0x00;
public void setMessageReference(int reference) {
this.messageReference = reference;
}
public int getMessageReference() {
return this.messageReference;
}
// ==================================================
// STATUS
// ==================================================
private int status = 0x00;
public void setStatus(int status) {
this.status = status;
}
public int getStatus() {
return this.status;
}
// ==================================================
// TIMESTAMP
// ==================================================
private @Nullable Calendar timestamp;
public void setTimestamp(Calendar timestamp) {
this.timestamp = timestamp;
}
public @Nullable Date getTimestamp() {
Calendar timestampFinal = this.timestamp;
return timestampFinal == null ? null : timestampFinal.getTime();
}
// ==================================================
// DISCHARGE TIME
// ==================================================
private @Nullable Calendar dischargeTime;
public void setDischargeTime(Calendar myDischargeTime) {
this.dischargeTime = myDischargeTime;
}
public @Nullable Date getDischargeTime() {
Calendar dischargeTimeFinal = this.dischargeTime;
return dischargeTimeFinal == null ? null : dischargeTimeFinal.getTime();
}
}

View File

@@ -0,0 +1,78 @@
package org.smslib.pduUtils.gsm3040;
import java.util.Calendar;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class SmsSubmitPdu extends Pdu {
// ==================================================
// FIRST OCTET UTILITIES
// ==================================================
public int getTpVpf() {
return getFirstOctetField(PduUtils.TP_VPF_MASK);
}
// ==================================================
// MESSAGE REFERENCE
// ==================================================
// usually just 0x00 to let MC supply
private int messageReference = 0x00;
public void setMessageReference(int reference) {
this.messageReference = reference;
}
public int getMessageReference() {
return this.messageReference;
}
// ==================================================
// VALIDITY PERIOD
// ==================================================
// which one is used depends of validity period format (TP-VPF)
private int validityPeriod = -1;
@Nullable
private Calendar validityPeriodTimeStamp;
public int getValidityPeriod() {
return this.validityPeriod;
}
public void setValidityPeriod(int validityPeriod) {
this.validityPeriod = validityPeriod;
}
public void setValidityTimestamp(Calendar date) {
this.validityPeriodTimeStamp = date;
}
public @Nullable Date getValidityDate() {
Calendar validityPeriodTimeStampFinal = this.validityPeriodTimeStamp;
return validityPeriodTimeStampFinal == null ? null : validityPeriodTimeStampFinal.getTime();
}
}

View File

@@ -0,0 +1,188 @@
package org.smslib.pduUtils.gsm3040.ie;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.smslib.UnrecoverableSmslibException;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class ConcatInformationElement extends InformationElement {
private static final int CONCAT_IE_LENGTH_8BIT = 5;
public static final int CONCAT_8BIT_REF = 0x00;
public static final int CONCAT_16BIT_REF = 0x08;
private static int defaultConcatType = CONCAT_8BIT_REF;
private static int defaultConcatLength = CONCAT_IE_LENGTH_8BIT;
public static int getDefaultConcatLength() {
return defaultConcatLength;
}
public static int getDefaultConcatType() {
return defaultConcatType;
}
ConcatInformationElement(byte identifier, byte[] data) {
super(identifier, data);
if (getIdentifier() == CONCAT_8BIT_REF) {
// iei
// iel
// ref
// max
// seq
if (data.length != 3) {
throw new IllegalArgumentException("Invalid data length in: " + getClass().getSimpleName());
}
} else if (getIdentifier() == CONCAT_16BIT_REF) {
// iei
// iel
// ref(2 bytes)
// max
// seq
if (data.length != 4) {
throw new IllegalArgumentException("Invalid data length in: " + getClass().getSimpleName());
}
} else {
throw new IllegalArgumentException("Invalid identifier in data in: " + getClass().getSimpleName());
}
validate();
}
ConcatInformationElement(int identifier, int mpRefNo, int mpMaxNo, int mpSeqNo) {
super((byte) (identifier & 0xFF), getData(identifier, mpRefNo, mpMaxNo, mpSeqNo));
validate();
}
private static byte[] getData(int identifier, int mpRefNo, int mpMaxNo, int mpSeqNo) {
byte[] data = null;
switch (identifier) {
case CONCAT_8BIT_REF:
data = new byte[3];
data[0] = (byte) (mpRefNo & 0xFF);
data[1] = (byte) (mpMaxNo & 0xFF);
data[2] = (byte) (mpSeqNo & 0xFF);
break;
case CONCAT_16BIT_REF:
data = new byte[4];
data[0] = (byte) ((mpRefNo & 0xFF00) >>> 8);
data[1] = (byte) (mpRefNo & 0xFF);
data[2] = (byte) (mpMaxNo & 0xFF);
data[3] = (byte) (mpSeqNo & 0xFF);
break;
default:
throw new IllegalArgumentException("Invalid identifier for ConcatInformationElement");
}
return data;
}
public int getMpRefNo() {
// this is 8-bit in 0x00 and 16-bit in 0x08
byte[] data = getData();
if (getIdentifier() == CONCAT_8BIT_REF) {
return (data[0] & (0xFF));
} else if (getIdentifier() == CONCAT_16BIT_REF) {
return ((data[0] << 8) | data[1]) & (0xFFFF);
}
throw new UnrecoverableSmslibException("Invalid identifier");
}
public void setMpRefNo(int mpRefNo) {
// this is 8-bit in 0x00 and 16-bit in 0x08
byte[] data = getData();
if (getIdentifier() == CONCAT_8BIT_REF) {
data[0] = (byte) (mpRefNo & (0xFF));
} else if (getIdentifier() == CONCAT_16BIT_REF) {
data[0] = (byte) ((mpRefNo >>> 8) & (0xFF));
data[1] = (byte) ((mpRefNo) & (0xFF));
} else {
throw new UnrecoverableSmslibException("Invalid identifier");
}
}
public int getMpMaxNo() {
byte[] data = getData();
if (getIdentifier() == CONCAT_8BIT_REF) {
return (data[1] & (0xFF));
} else if (getIdentifier() == CONCAT_16BIT_REF) {
return (data[2] & (0xFF));
}
throw new UnrecoverableSmslibException("Invalid identifier");
}
public void setMpMaxNo(int mpMaxNo) {
byte[] data = getData();
if (getIdentifier() == CONCAT_8BIT_REF) {
data[1] = (byte) (mpMaxNo & 0xFF);
} else if (getIdentifier() == CONCAT_16BIT_REF) {
data[2] = (byte) (mpMaxNo & 0xFF);
} else {
throw new UnrecoverableSmslibException("Invalid identifier");
}
}
public int getMpSeqNo() {
byte[] data = getData();
if (getIdentifier() == CONCAT_8BIT_REF) {
return (data[2] & (0xFF));
} else if (getIdentifier() == CONCAT_16BIT_REF) {
return (data[3] & (0xFF));
}
throw new UnrecoverableSmslibException("Invalid identifier");
}
public void setMpSeqNo(int mpSeqNo) {
byte[] data = getData();
if (getIdentifier() == CONCAT_8BIT_REF) {
data[2] = (byte) (mpSeqNo & (0xFF));
} else if (getIdentifier() == CONCAT_16BIT_REF) {
data[3] = (byte) (mpSeqNo & (0xFF));
} else {
throw new UnrecoverableSmslibException("Invalid identifier");
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString());
sb.append("[MpRefNo: ");
sb.append(getMpRefNo());
sb.append(", MpMaxNo: ");
sb.append(getMpMaxNo());
sb.append(", MpSeqNo: ");
sb.append(getMpSeqNo());
sb.append("]");
return sb.toString();
}
private void validate() {
if (getMpMaxNo() == 0) {
throw new IllegalArgumentException("mpMaxNo must be > 0");
}
if (getMpSeqNo() == 0) {
throw new IllegalArgumentException("mpSeqNo must be > 0");
}
}
}

View File

@@ -0,0 +1,70 @@
package org.smslib.pduUtils.gsm3040.ie;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.smslib.pduUtils.gsm3040.PduUtils;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class InformationElement {
private byte identifier;
private byte[] data;
// iei
// iel (implicit length of data)
// ied (raw ie data)
InformationElement(byte id, byte[] ieData) {
this.identifier = id;
this.data = ieData;
}
// for outgoing messages
void initialize(byte id, byte[] ieData) {
this.identifier = id;
this.data = ieData;
}
public int getIdentifier() {
return (this.identifier & 0xFF);
}
public int getLength() {
return this.data.length;
}
public byte[] getData() {
return this.data;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName() + "[");
sb.append(PduUtils.byteToPdu(this.identifier));
sb.append(", ");
sb.append(PduUtils.byteToPdu(this.data.length));
sb.append(", ");
sb.append(PduUtils.bytesToPdu(this.data));
sb.append("]");
return sb.toString();
}
}

View File

@@ -0,0 +1,53 @@
package org.smslib.pduUtils.gsm3040.ie;
import org.eclipse.jdt.annotation.NonNullByDefault;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class InformationElementFactory {
// used to determine what InformationElement to use based on bytes from a UDH
// assumes the supplied bytes are correct
public static InformationElement createInformationElement(int id, byte[] data) {
byte iei = (byte) (id & 0xFF);
switch (iei) {
case ConcatInformationElement.CONCAT_8BIT_REF:
case ConcatInformationElement.CONCAT_16BIT_REF:
return new ConcatInformationElement(iei, data);
case PortInformationElement.PORT_16BIT:
return new PortInformationElement(iei, data);
default:
return new InformationElement(iei, data);
}
}
public static ConcatInformationElement generateConcatInfo(int mpRefNo, int partNo) {
ConcatInformationElement concatInfo = new ConcatInformationElement(
ConcatInformationElement.getDefaultConcatType(), mpRefNo, 1, partNo);
return concatInfo;
}
public static PortInformationElement generatePortInfo(int destPort, int srcPort) {
PortInformationElement portInfo = new PortInformationElement(PortInformationElement.PORT_16BIT, destPort,
srcPort);
return portInfo;
}
}

View File

@@ -0,0 +1,87 @@
package org.smslib.pduUtils.gsm3040.ie;
import org.eclipse.jdt.annotation.NonNullByDefault;
//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
//
//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
//PduUtils is distributed under the terms of the Apache License version 2.0
//
//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.
/**
* Extracted from SMSLib
*/
@NonNullByDefault
public class PortInformationElement extends InformationElement {
public static final int PORT_16BIT = 0x05;
PortInformationElement(byte id, byte[] data) {
super(id, data);
if (getIdentifier() != PORT_16BIT) {
throw new IllegalArgumentException(
"Invalid identifier " + getIdentifier() + " in data in: " + getClass().getSimpleName());
}
// iei
// iel
// dest(2 bytes)
// src (2 bytes)
if (data.length != 4) {
throw new IllegalArgumentException("Invalid data length in: " + getClass().getSimpleName());
}
}
PortInformationElement(int identifier, int destPort, int srcPort) {
super((byte) (identifier & 0xFF), getData(identifier, destPort, srcPort));
}
private static byte[] getData(int identifier, int destPort, int srcPort) {
byte[] data = null;
switch (identifier) {
case PORT_16BIT:
data = new byte[4];
data[0] = (byte) ((destPort & 0xFF00) >>> 8);
data[1] = (byte) (destPort & 0xFF);
data[2] = (byte) ((srcPort & 0xFF00) >>> 8);
data[3] = (byte) (srcPort & 0xFF);
break;
default:
throw new IllegalArgumentException("Invalid identifier for PortInformationElement");
}
return data;
}
public int getDestPort() {
// first 2 bytes of data
byte[] data = getData();
return (((data[0] & 0xFF) << 8) | (data[1] & 0xFF));
}
public int getSrcPort() {
// next 2 bytes of data
byte[] data = getData();
return (((data[2] & 0xFF) << 8) | (data[3] & 0xFF));
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString());
sb.append("[Dst Port: ");
sb.append(getDestPort());
sb.append(", Src Port: ");
sb.append(getSrcPort());
sb.append("]");
return sb.toString();
}
}