1 package org.smslib.pduUtils.gsm3040;
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.Calendar;
8 import java.util.Iterator;
10 import java.util.TimeZone;
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;
18 //PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
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
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
27 //http://www.apache.org/licenses/LICENSE-2.0
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.
36 * Extracted from SMSLib
39 public class PduGenerator {
40 private ByteArrayOutputStream baos = new ByteArrayOutputStream();
42 private int firstOctetPosition = -1;
44 private boolean updateFirstOctet = false;
46 protected void writeSmscInfo(Pdu pdu) {
47 String smscAddress = pdu.getSmscAddress();
48 if (smscAddress != null) {
49 writeBCDAddress(smscAddress, pdu.getSmscAddressType(), pdu.getSmscInfoLength());
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());
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);
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;
74 validityInt = (validityPeriod / 168) + 192;
76 this.baos.write(validityInt);
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
95 tzValue = 128 - tzValue;
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));
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);
116 this.baos.write(addressType);
118 this.baos.write(alphaNumBytes);
122 writeBCDAddress(address, addressType, addressLength);
126 protected void writeBCDAddress(String address, int addressType, int addressLength) {
128 // ADDRESS LENGTH - either an octet count or semi-octet count
129 this.baos.write(addressLength);
131 this.baos.write(addressType);
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";
139 for (int i = 0; i < myaddress.length(); i++) {
140 char c = myaddress.charAt(i);
142 digit |= ((Integer.parseInt(Character.toString(c), 16)) << 4);
143 this.baos.write(digit);
147 digit |= (Integer.parseInt(Character.toString(c), 16) & 0x0F);
152 protected void writeUDData(Pdu pdu, int mpRefNo, int partNo) {
153 int dcs = pdu.getDataCodingScheme();
155 switch (PduUtils.extractDcsEncoding(dcs)) {
156 case PduUtils.DCS_ENCODING_7BIT:
157 writeUDData7bit(pdu, mpRefNo, partNo);
159 case PduUtils.DCS_ENCODING_8BIT:
160 writeUDData8bit(pdu, mpRefNo, partNo);
162 case PduUtils.DCS_ENCODING_UCS2:
163 writeUDDataUCS2(pdu, mpRefNo, partNo);
166 throw new IllegalArgumentException("Invalid DCS encoding: " + PduUtils.extractDcsEncoding(dcs));
168 } catch (IOException e) {
169 throw new UnrecoverableSmslibException("Cannot write uddata", e);
173 protected void writeUDH(Pdu pdu) throws IOException {
174 // stream directly into the internal baos
175 writeUDH(pdu, this.baos);
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());
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
197 if (!pdu.isBinary()) {
198 maxParts = pdu.getDecodedText().length() / maxMessageLength + 1;
200 byte[] pduDataBytes = pdu.getDataBytes();
201 if (pduDataBytes == null) {
202 throw new UnrecoverableSmslibException("Cannot compute offset for empty data bytes");
204 maxParts = pduDataBytes.length / maxMessageLength + 1;
206 if (pdu.hasTpUdhi()) {
207 ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
208 if (concatInfoFinal != null) {
210 concatInfoFinal.setMpMaxNo(maxParts);
214 if ((maxParts > 1) && (partNo > 0)) {
215 // - if partNo > maxParts
217 if (partNo > maxParts) {
218 throw new IllegalArgumentException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
220 offset = ((partNo - 1) * maxMessageLength);
222 // just get from the start
228 protected void checkForConcat(Pdu pdu, int lengthOfText, int maxLength, int maxLengthWithUdh, int mpRefNo,
230 if ((lengthOfText <= maxLengthWithUdh) || ((lengthOfText > maxLengthWithUdh) && (lengthOfText <= maxLength))) {
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);
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
243 ConcatInformationElement concatInfo = InformationElementFactory.generateConcatInfo(mpRefNo, partNo);
244 pdu.addInformationElement(concatInfo);
245 this.updateFirstOctet = true;
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;
256 // this already has the UDH Length field, no need to add 1
257 return currentUdhLength + ConcatInformationElement.getDefaultConcatLength();
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)
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();
290 // encode both as one unit
291 byte[] udBytes = PduUtils.encode7bitUserData(udhBytes, textSeptets);
292 // write combined encoded array
293 this.baos.write(udBytes);
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
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) {
309 concatInfoFinal.setMpMaxNo(maxParts);
313 if ((maxParts > 1) && (partNo > 0)) {
314 // - if partNo > maxParts
316 if (partNo > maxParts) {
317 throw new UnrecoverableSmslibException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
319 offset = ((partNo - 1) * maxMessageLength);
321 // just get from the start
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;
330 protected void writeUDData8bit(Pdu pdu, int mpRefNo, int partNo) throws IOException {
331 // NOTE: binary messages are also handled here
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");
339 data = dataBytesFinal;
342 data = PduUtils.encode8bitUserData(pdu.getDecodedText());
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);
361 // udLength is an octet count for 8bit/ucs2
362 int udLength = totalUDHLength + dataToWrite.length;
364 this.baos.write(udLength);
365 // write UDH to the stream directly
366 if (pdu.hasTpUdhi()) {
367 writeUDH(pdu, this.baos);
370 this.baos.write(dataToWrite);
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()));
391 // udLength is an octet count for 8bit/ucs2
392 int udLength = totalUDHLength + (textToEncode.length() * 2);
394 this.baos.write(udLength);
395 // write UDH to the stream directly
396 if (pdu.hasTpUdhi()) {
397 writeUDH(pdu, this.baos);
399 // write encoded text
400 this.baos.write(PduUtils.encodeUcs2UserData(textToEncode));
403 protected void writeByte(int i) {
407 protected void writeBytes(byte[] b) throws IOException {
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);
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) {
427 this.baos = new ByteArrayOutputStream();
428 this.firstOctetPosition = -1;
429 this.updateFirstOctet = false;
431 switch (pdu.getTpMti()) {
432 case PduUtils.TP_MTI_SMS_DELIVER:
433 generateSmsDeliverPduString((SmsDeliveryPdu) pdu, mpRefNo, partNo);
435 case PduUtils.TP_MTI_SMS_SUBMIT:
436 generateSmsSubmitPduString((SmsSubmitPdu) pdu, mpRefNo, partNo);
438 case PduUtils.TP_MTI_SMS_STATUS_REPORT:
439 generateSmsStatusReportPduString((SmsStatusReportPdu) pdu);
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);
449 return PduUtils.bytesToPdu(pduBytes);
450 } catch (IOException e) {
451 throw new UnrecoverableSmslibException("Cannot generate pdu", e);
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");
463 writeFirstOctet(pdu);
465 writeByte(pdu.getMessageReference());
466 // destination address info
467 writeAddress(address, pdu.getAddressType(), address.length());
469 writeByte(pdu.getProtocolIdentifier());
470 // data coding scheme
471 writeByte(pdu.getDataCodingScheme());
473 switch (pdu.getTpVpf()) {
474 case PduUtils.TP_VPF_INTEGER:
475 writeValidityPeriodInteger(pdu.getValidityPeriod());
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");
482 writeTimeStampStringForDate(validityDate);
487 writeUDData(pdu, mpRefNo, partNo);
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 {
496 writeFirstOctet(pdu);
497 // originator address info
498 String address = pdu.getAddress();
499 if (address == null) {
500 throw new IllegalArgumentException("Address cannot be null");
502 writeAddress(address, pdu.getAddressType(), address.length());
504 writeByte(pdu.getProtocolIdentifier());
505 // data coding scheme
506 writeByte(pdu.getDataCodingScheme());
508 Date timestamp = pdu.getTimestamp();
509 if (timestamp != null) {
510 writeTimeStampStringForDate(timestamp);
514 writeUDData(pdu, mpRefNo, partNo);
517 protected void generateSmsStatusReportPduString(SmsStatusReportPdu pdu) throws IOException {
521 writeFirstOctet(pdu);
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");
529 writeAddress(address, pdu.getAddressType(), address.length());
531 Date timestamp = pdu.getTimestamp();
532 if (timestamp == null) {
533 throw new IllegalArgumentException("cannot write null timestamp");
535 writeTimeStampStringForDate(timestamp);
536 // discharge time(timestamp)
537 Date dischargeTime = pdu.getDischargeTime();
538 if (dischargeTime == null) {
539 throw new IllegalArgumentException("cannot write null dischargeTime");
541 writeTimeStampStringForDate(dischargeTime);
543 writeByte(pdu.getStatus());