]> git.basschouten.com Git - openhab-addons.git/blob
77453acf2e8feb8a2311ab79ca618750a885115f
[openhab-addons.git] /
1 package org.smslib.pduUtils.gsm3040;
2
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.Calendar;
7 import java.util.Date;
8 import java.util.Iterator;
9 import java.util.List;
10 import java.util.TimeZone;
11
12 import org.eclipse.jdt.annotation.NonNullByDefault;
13 import org.smslib.UnrecoverableSmslibException;
14 import org.smslib.pduUtils.gsm3040.ie.ConcatInformationElement;
15 import org.smslib.pduUtils.gsm3040.ie.InformationElement;
16 import org.smslib.pduUtils.gsm3040.ie.InformationElementFactory;
17
18 //PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
19 //
20 //Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
21 //PduUtils is distributed under the terms of the Apache License version 2.0
22 //
23 //Licensed under the Apache License, Version 2.0 (the "License");
24 //you may not use this file except in compliance with the License.
25 //You may obtain a copy of the License at
26 //
27 //http://www.apache.org/licenses/LICENSE-2.0
28 //
29 //Unless required by applicable law or agreed to in writing, software
30 //distributed under the License is distributed on an "AS IS" BASIS,
31 //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32 //See the License for the specific language governing permissions and
33 //limitations under the License.
34
35 /**
36  * Extracted from SMSLib
37  */
38 @NonNullByDefault
39 public class PduGenerator {
40     private ByteArrayOutputStream baos = new ByteArrayOutputStream();
41
42     private int firstOctetPosition = -1;
43
44     private boolean updateFirstOctet = false;
45
46     protected void writeSmscInfo(Pdu pdu) {
47         String smscAddress = pdu.getSmscAddress();
48         if (smscAddress != null) {
49             writeBCDAddress(smscAddress, pdu.getSmscAddressType(), pdu.getSmscInfoLength());
50         } else {
51             writeByte(0);
52         }
53     }
54
55     protected void writeFirstOctet(Pdu pdu) {
56         // store the position in case it will need to be updated later
57         this.firstOctetPosition = pdu.getSmscInfoLength() + 1;
58         writeByte(pdu.getFirstOctet());
59     }
60
61     // validity period conversion from hours to the proper integer
62     protected void writeValidityPeriodInteger(int validityPeriod) {
63         if (validityPeriod == -1) {
64             this.baos.write(0xFF);
65         } else {
66             int validityInt;
67             if (validityPeriod <= 12) {
68                 validityInt = (validityPeriod * 12) - 1;
69             } else if (validityPeriod <= 24) {
70                 validityInt = (((validityPeriod - 12) * 2) + 143);
71             } else if (validityPeriod <= 720) {
72                 validityInt = (validityPeriod / 24) + 166;
73             } else {
74                 validityInt = (validityPeriod / 168) + 192;
75             }
76             this.baos.write(validityInt);
77         }
78     }
79
80     protected void writeTimeStampStringForDate(Date timestamp) {
81         Calendar cal = Calendar.getInstance();
82         cal.setTime(timestamp);
83         int year = cal.get(Calendar.YEAR) - 2000;
84         int month = cal.get(Calendar.MONTH) + 1;
85         int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
86         int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
87         int minute = cal.get(Calendar.MINUTE);
88         int sec = cal.get(Calendar.SECOND);
89         TimeZone tz = cal.getTimeZone();
90         int offset = tz.getOffset(timestamp.getTime());
91         int minOffset = offset / 60000;
92         int tzValue = minOffset / 15;
93         // for negative offsets, add 128 to the absolute value
94         if (tzValue < 0) {
95             tzValue = 128 - tzValue;
96         }
97         // note: the nibbles are written as BCD style
98         this.baos.write(PduUtils.createSwappedBCD(year));
99         this.baos.write(PduUtils.createSwappedBCD(month));
100         this.baos.write(PduUtils.createSwappedBCD(dayOfMonth));
101         this.baos.write(PduUtils.createSwappedBCD(hourOfDay));
102         this.baos.write(PduUtils.createSwappedBCD(minute));
103         this.baos.write(PduUtils.createSwappedBCD(sec));
104         this.baos.write(PduUtils.createSwappedBCD(tzValue));
105     }
106
107     protected void writeAddress(String address, int addressType, int addressLength) throws IOException {
108         switch (PduUtils.extractAddressType(addressType)) {
109             case PduUtils.ADDRESS_TYPE_ALPHANUMERIC:
110                 byte[] textSeptets = PduUtils.stringToUnencodedSeptets(address);
111                 byte[] alphaNumBytes = PduUtils.encode7bitUserData(null, textSeptets);
112                 // ADDRESS LENGTH - should be the semi-octet count
113                 // - this type is not used for SMSCInfo
114                 this.baos.write(alphaNumBytes.length * 2);
115                 // ADDRESS TYPE
116                 this.baos.write(addressType);
117                 // ADDRESS TEXT
118                 this.baos.write(alphaNumBytes);
119                 break;
120             default:
121                 // BCD-style
122                 writeBCDAddress(address, addressType, addressLength);
123         }
124     }
125
126     protected void writeBCDAddress(String address, int addressType, int addressLength) {
127         // BCD-style
128         // ADDRESS LENGTH - either an octet count or semi-octet count
129         this.baos.write(addressLength);
130         // ADDRESS TYPE
131         this.baos.write(addressType);
132         // ADDRESS NUMBERS
133         // if address.length is not even, pad the string an with F at the end
134         String myaddress = address;
135         if (myaddress.length() % 2 == 1) {
136             myaddress = myaddress + "F";
137         }
138         int digit = 0;
139         for (int i = 0; i < myaddress.length(); i++) {
140             char c = myaddress.charAt(i);
141             if (i % 2 == 1) {
142                 digit |= ((Integer.parseInt(Character.toString(c), 16)) << 4);
143                 this.baos.write(digit);
144                 // clear it
145                 digit = 0;
146             } else {
147                 digit |= (Integer.parseInt(Character.toString(c), 16) & 0x0F);
148             }
149         }
150     }
151
152     protected void writeUDData(Pdu pdu, int mpRefNo, int partNo) {
153         int dcs = pdu.getDataCodingScheme();
154         try {
155             switch (PduUtils.extractDcsEncoding(dcs)) {
156                 case PduUtils.DCS_ENCODING_7BIT:
157                     writeUDData7bit(pdu, mpRefNo, partNo);
158                     break;
159                 case PduUtils.DCS_ENCODING_8BIT:
160                     writeUDData8bit(pdu, mpRefNo, partNo);
161                     break;
162                 case PduUtils.DCS_ENCODING_UCS2:
163                     writeUDDataUCS2(pdu, mpRefNo, partNo);
164                     break;
165                 default:
166                     throw new IllegalArgumentException("Invalid DCS encoding: " + PduUtils.extractDcsEncoding(dcs));
167             }
168         } catch (IOException e) {
169             throw new UnrecoverableSmslibException("Cannot write uddata", e);
170         }
171     }
172
173     protected void writeUDH(Pdu pdu) throws IOException {
174         // stream directly into the internal baos
175         writeUDH(pdu, this.baos);
176     }
177
178     protected void writeUDH(Pdu pdu, ByteArrayOutputStream udhBaos) throws IOException {
179         // need to insure that proper concat info is inserted
180         // before writing if needed
181         // i.e. the reference number, maxseq and seq have to be set from
182         // outside (OutboundMessage)
183         udhBaos.write(pdu.getUDHLength());
184         for (Iterator<InformationElement> ieIterator = pdu.getInformationElements(); ieIterator.hasNext();) {
185             InformationElement ie = ieIterator.next();
186             udhBaos.write(ie.getIdentifier());
187             udhBaos.write(ie.getLength());
188             udhBaos.write(ie.getData());
189         }
190     }
191
192     protected int computeOffset(Pdu pdu, int maxMessageLength, int partNo) {
193         // computes offset to which part of the string is to be encoded into the PDU
194         // also sets the MpMaxNo field of the concatInfo if message is multi-part
195         int offset;
196         int maxParts = 1;
197         if (!pdu.isBinary()) {
198             maxParts = pdu.getDecodedText().length() / maxMessageLength + 1;
199         } else {
200             byte[] pduDataBytes = pdu.getDataBytes();
201             if (pduDataBytes == null) {
202                 throw new UnrecoverableSmslibException("Cannot compute offset for empty data bytes");
203             }
204             maxParts = pduDataBytes.length / maxMessageLength + 1;
205         }
206         if (pdu.hasTpUdhi()) {
207             ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
208             if (concatInfoFinal != null) {
209                 if (partNo > 0) {
210                     concatInfoFinal.setMpMaxNo(maxParts);
211                 }
212             }
213         }
214         if ((maxParts > 1) && (partNo > 0)) {
215             // - if partNo > maxParts
216             // - error
217             if (partNo > maxParts) {
218                 throw new IllegalArgumentException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
219             }
220             offset = ((partNo - 1) * maxMessageLength);
221         } else {
222             // just get from the start
223             offset = 0;
224         }
225         return offset;
226     }
227
228     protected void checkForConcat(Pdu pdu, int lengthOfText, int maxLength, int maxLengthWithUdh, int mpRefNo,
229             int partNo) {
230         if ((lengthOfText <= maxLengthWithUdh) || ((lengthOfText > maxLengthWithUdh) && (lengthOfText <= maxLength))) {
231         } else {
232             // need concat
233             ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
234             if (concatInfoFinal != null) {
235                 // if concatInfo is already present then just replace the values with the supplied
236                 concatInfoFinal.setMpRefNo(mpRefNo);
237                 concatInfoFinal.setMpSeqNo(partNo);
238             } else {
239                 // add concat info with the specified mpRefNo, bogus maxSeqNo, and partNo
240                 // bogus maxSeqNo will be replaced once it is known in the later steps
241                 // this just needs to be added since its presence is needed to compute
242                 // the UDH length
243                 ConcatInformationElement concatInfo = InformationElementFactory.generateConcatInfo(mpRefNo, partNo);
244                 pdu.addInformationElement(concatInfo);
245                 this.updateFirstOctet = true;
246             }
247         }
248     }
249
250     protected int computePotentialUdhLength(Pdu pdu) {
251         int currentUdhLength = pdu.getTotalUDHLength();
252         if (currentUdhLength == 0) {
253             // add 1 for the UDH Length field
254             return ConcatInformationElement.getDefaultConcatLength() + 1;
255         }
256         // this already has the UDH Length field, no need to add 1
257         return currentUdhLength + ConcatInformationElement.getDefaultConcatLength();
258     }
259
260     protected void writeUDData7bit(Pdu pdu, int mpRefNo, int partNo) throws IOException {
261         String decodedText = pdu.getDecodedText();
262         // partNo states what part of the unencoded text will be used
263         // - max length is based on the size of the UDH
264         // for 7bit => maxLength = 160 - total UDH septets
265         // check if this message needs a concat
266         byte[] textSeptetsForDecodedText = PduUtils.stringToUnencodedSeptets(decodedText);
267         int potentialUdhLength = PduUtils.getNumSeptetsForOctets(computePotentialUdhLength(pdu));
268         checkForConcat(pdu, textSeptetsForDecodedText.length,
269                 160 - PduUtils.getNumSeptetsForOctets(pdu.getTotalUDHLength()), // CHANGED
270                 160 - potentialUdhLength, mpRefNo, partNo);
271         // given the IEs in the pdu derive the max message body length
272         // this length will include the potential concat added in the previous step
273         int totalUDHLength = pdu.getTotalUDHLength();
274         int maxMessageLength = 160 - PduUtils.getNumSeptetsForOctets(totalUDHLength);
275         // get septets for part
276         byte[] textSeptets = getUnencodedSeptetsForPart(pdu, maxMessageLength, partNo);
277         // udlength is the sum of udh septet length and the text septet length
278         int udLength = PduUtils.getNumSeptetsForOctets(totalUDHLength) + textSeptets.length;
279         this.baos.write(udLength);
280         // generate UDH byte[]
281         // UDHL (sum of all IE lengths)
282         // IE list
283         byte[] udhBytes = null;
284         if (pdu.hasTpUdhi()) {
285             ByteArrayOutputStream udhBaos = new ByteArrayOutputStream();
286             writeUDH(pdu, udhBaos);
287             // buffer the udh since this needs to be 7-bit encoded with the text
288             udhBytes = udhBaos.toByteArray();
289         }
290         // encode both as one unit
291         byte[] udBytes = PduUtils.encode7bitUserData(udhBytes, textSeptets);
292         // write combined encoded array
293         this.baos.write(udBytes);
294     }
295
296     private byte[] getUnencodedSeptetsForPart(Pdu pdu, int maxMessageLength, int partNo) {
297         // computes offset to which part of the string is to be encoded into the PDU
298         // also sets the MpMaxNo field of the concatInfo if message is multi-part
299         int offset;
300         int maxParts = 1;
301         // must use the unencoded septets not the actual string since
302         // it is possible that some special characters in string are multi-septet
303         byte[] unencodedSeptets = PduUtils.stringToUnencodedSeptets(pdu.getDecodedText());
304         maxParts = (unencodedSeptets.length / maxMessageLength) + 1;
305         if (pdu.hasTpUdhi()) {
306             ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
307             if (concatInfoFinal != null) {
308                 if (partNo > 0) {
309                     concatInfoFinal.setMpMaxNo(maxParts);
310                 }
311             }
312         }
313         if ((maxParts > 1) && (partNo > 0)) {
314             // - if partNo > maxParts
315             // - error
316             if (partNo > maxParts) {
317                 throw new UnrecoverableSmslibException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
318             }
319             offset = ((partNo - 1) * maxMessageLength);
320         } else {
321             // just get from the start
322             offset = 0;
323         }
324         // copy the portion of the full unencoded septet array for this part
325         byte[] septetsForPart = new byte[Math.min(maxMessageLength, unencodedSeptets.length - offset)];
326         System.arraycopy(unencodedSeptets, offset, septetsForPart, 0, septetsForPart.length);
327         return septetsForPart;
328     }
329
330     protected void writeUDData8bit(Pdu pdu, int mpRefNo, int partNo) throws IOException {
331         // NOTE: binary messages are also handled here
332         byte[] data;
333         if (pdu.isBinary()) {
334             // use the supplied bytes
335             byte[] dataBytesFinal = pdu.getDataBytes();
336             if (dataBytesFinal == null) {
337                 throw new UnrecoverableSmslibException("Data cannot be null");
338             }
339             data = dataBytesFinal;
340         } else {
341             // encode the text
342             data = PduUtils.encode8bitUserData(pdu.getDecodedText());
343         }
344         // partNo states what part of the unencoded text will be used
345         // - max length is based on the size of the UDH
346         // for 8bit => maxLength = 140 - the total UDH bytes
347         // check if this message needs a concat
348         int potentialUdhLength = computePotentialUdhLength(pdu);
349         checkForConcat(pdu, data.length, 140 - pdu.getTotalUDHLength(), // CHANGED
350                 140 - potentialUdhLength, mpRefNo, partNo);
351         // given the IEs in the pdu derive the max message body length
352         // this length will include the potential concat added in the previous step
353         int totalUDHLength = pdu.getTotalUDHLength();
354         int maxMessageLength = 140 - totalUDHLength;
355         // compute which portion of the message will be part of the message
356         int offset = computeOffset(pdu, maxMessageLength, partNo);
357         byte[] dataToWrite = new byte[Math.min(maxMessageLength, data.length - offset)];
358         System.arraycopy(data, offset, dataToWrite, 0, dataToWrite.length);
359         // generate udlength
360         // based on partNo
361         // udLength is an octet count for 8bit/ucs2
362         int udLength = totalUDHLength + dataToWrite.length;
363         // write udlength
364         this.baos.write(udLength);
365         // write UDH to the stream directly
366         if (pdu.hasTpUdhi()) {
367             writeUDH(pdu, this.baos);
368         }
369         // write data
370         this.baos.write(dataToWrite);
371     }
372
373     protected void writeUDDataUCS2(Pdu pdu, int mpRefNo, int partNo) throws IOException {
374         String decodedText = pdu.getDecodedText();
375         // partNo states what part of the unencoded text will be used
376         // - max length is based on the size of the UDH
377         // for ucs2 => maxLength = (140 - the total UDH bytes)/2
378         // check if this message needs a concat
379         int potentialUdhLength = computePotentialUdhLength(pdu);
380         checkForConcat(pdu, decodedText.length(), (140 - pdu.getTotalUDHLength()) / 2, // CHANGED
381                 (140 - potentialUdhLength) / 2, mpRefNo, partNo);
382         // given the IEs in the pdu derive the max message body length
383         // this length will include the potential concat added in the previous step
384         int totalUDHLength = pdu.getTotalUDHLength();
385         int maxMessageLength = (140 - totalUDHLength) / 2;
386         // compute which portion of the message will be part of the message
387         int offset = computeOffset(pdu, maxMessageLength, partNo);
388         String textToEncode = decodedText.substring(offset, Math.min(offset + maxMessageLength, decodedText.length()));
389         // generate udlength
390         // based on partNo
391         // udLength is an octet count for 8bit/ucs2
392         int udLength = totalUDHLength + (textToEncode.length() * 2);
393         // write udlength
394         this.baos.write(udLength);
395         // write UDH to the stream directly
396         if (pdu.hasTpUdhi()) {
397             writeUDH(pdu, this.baos);
398         }
399         // write encoded text
400         this.baos.write(PduUtils.encodeUcs2UserData(textToEncode));
401     }
402
403     protected void writeByte(int i) {
404         this.baos.write(i);
405     }
406
407     protected void writeBytes(byte[] b) throws IOException {
408         this.baos.write(b);
409     }
410
411     public List<String> generatePduList(Pdu pdu, int mpRefNo) {
412         // generate all required PDUs for a given message
413         // mpRefNo comes from the ModemGateway
414         ArrayList<String> pduList = new ArrayList<>();
415         for (int i = 1; i <= pdu.getMpMaxNo(); i++) {
416             String pduString = generatePduString(pdu, mpRefNo, i);
417             pduList.add(pduString);
418         }
419         return pduList;
420     }
421
422     // NOTE: partNo indicates which part of a multipart message to generate
423     // assuming that the message is multipart, this will be ignored if the
424     // message is not a concat message
425     public String generatePduString(Pdu pdu, int mpRefNo, int partNo) {
426         try {
427             this.baos = new ByteArrayOutputStream();
428             this.firstOctetPosition = -1;
429             this.updateFirstOctet = false;
430             // process the PDU
431             switch (pdu.getTpMti()) {
432                 case PduUtils.TP_MTI_SMS_DELIVER:
433                     generateSmsDeliverPduString((SmsDeliveryPdu) pdu, mpRefNo, partNo);
434                     break;
435                 case PduUtils.TP_MTI_SMS_SUBMIT:
436                     generateSmsSubmitPduString((SmsSubmitPdu) pdu, mpRefNo, partNo);
437                     break;
438                 case PduUtils.TP_MTI_SMS_STATUS_REPORT:
439                     generateSmsStatusReportPduString((SmsStatusReportPdu) pdu);
440                     break;
441             }
442             // in case concat is detected in the writeUD() method
443             // and there was no UDHI at the time of detection
444             // the old firstOctet must be overwritten with the new value
445             byte[] pduBytes = this.baos.toByteArray();
446             if (this.updateFirstOctet) {
447                 pduBytes[this.firstOctetPosition] = (byte) (pdu.getFirstOctet() & 0xFF);
448             }
449             return PduUtils.bytesToPdu(pduBytes);
450         } catch (IOException e) {
451             throw new UnrecoverableSmslibException("Cannot generate pdu", e);
452         }
453     }
454
455     protected void generateSmsSubmitPduString(SmsSubmitPdu pdu, int mpRefNo, int partNo) throws IOException {
456         String address = pdu.getAddress();
457         if (address == null) {
458             throw new IllegalArgumentException("adress cannot be null");
459         }
460         // SMSC address info
461         writeSmscInfo(pdu);
462         // first octet
463         writeFirstOctet(pdu);
464         // message reference
465         writeByte(pdu.getMessageReference());
466         // destination address info
467         writeAddress(address, pdu.getAddressType(), address.length());
468         // protocol id
469         writeByte(pdu.getProtocolIdentifier());
470         // data coding scheme
471         writeByte(pdu.getDataCodingScheme());
472         // validity period
473         switch (pdu.getTpVpf()) {
474             case PduUtils.TP_VPF_INTEGER:
475                 writeValidityPeriodInteger(pdu.getValidityPeriod());
476                 break;
477             case PduUtils.TP_VPF_TIMESTAMP:
478                 Date validityDate = pdu.getValidityDate();
479                 if (validityDate == null) {
480                     throw new IllegalArgumentException("Cannot get validity date for pdu");
481                 }
482                 writeTimeStampStringForDate(validityDate);
483                 break;
484         }
485         // user data
486         // headers
487         writeUDData(pdu, mpRefNo, partNo);
488     }
489
490     // NOTE: the following are just for validation of the PduParser
491     // - there is no normal scenario where these are used
492     protected void generateSmsDeliverPduString(SmsDeliveryPdu pdu, int mpRefNo, int partNo) throws IOException {
493         // SMSC address info
494         writeSmscInfo(pdu);
495         // first octet
496         writeFirstOctet(pdu);
497         // originator address info
498         String address = pdu.getAddress();
499         if (address == null) {
500             throw new IllegalArgumentException("Address cannot be null");
501         }
502         writeAddress(address, pdu.getAddressType(), address.length());
503         // protocol id
504         writeByte(pdu.getProtocolIdentifier());
505         // data coding scheme
506         writeByte(pdu.getDataCodingScheme());
507         // timestamp
508         Date timestamp = pdu.getTimestamp();
509         if (timestamp != null) {
510             writeTimeStampStringForDate(timestamp);
511         }
512         // user data
513         // headers
514         writeUDData(pdu, mpRefNo, partNo);
515     }
516
517     protected void generateSmsStatusReportPduString(SmsStatusReportPdu pdu) throws IOException {
518         // SMSC address info
519         writeSmscInfo(pdu);
520         // first octet
521         writeFirstOctet(pdu);
522         // message reference
523         writeByte(pdu.getMessageReference());
524         // destination address info
525         String address = pdu.getAddress();
526         if (address == null) {
527             throw new IllegalArgumentException("Address cannot be null");
528         }
529         writeAddress(address, pdu.getAddressType(), address.length());
530         // timestamp
531         Date timestamp = pdu.getTimestamp();
532         if (timestamp == null) {
533             throw new IllegalArgumentException("cannot write null timestamp");
534         }
535         writeTimeStampStringForDate(timestamp);
536         // discharge time(timestamp)
537         Date dischargeTime = pdu.getDischargeTime();
538         if (dischargeTime == null) {
539             throw new IllegalArgumentException("cannot write null dischargeTime");
540         }
541         writeTimeStampStringForDate(dischargeTime);
542         // status
543         writeByte(pdu.getStatus());
544     }
545 }