From ffea4e7a9e3bea787ec7d8faa29714989e8e7d44 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Fri, 11 Oct 2013 06:21:35 -0600 Subject: [PATCH 1/4] addition of raw error code --- .gitignore | 4 + .../smpp/util/DeliveryReceipt.java | 962 ++++++++++-------- .../smpp/util/DeliveryReceiptTest.java | 28 + 3 files changed, 572 insertions(+), 422 deletions(-) diff --git a/.gitignore b/.gitignore index bcc52fed..7f275add 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *~ target/ +.classpath +.project +.settings/ + diff --git a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java index f75f8828..18225df2 100644 --- a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java +++ b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java @@ -32,428 +32,546 @@ import org.slf4j.LoggerFactory; /** - * Utility class to represent a Delivery Receipt that may be contained within - * a DataSm or DeliverSm PDU. A delivery receipt has a specific message text - * and a few specific optional parameters. - * - * @author joelauer (twitter: @jjlauer or http://twitter.com/jjlauer) + * Utility class to represent a Delivery Receipt that may be contained within a + * DataSm or DeliverSm PDU. A delivery receipt has a specific message text and a + * few specific optional parameters. + * + * @author joelauer (twitter: @jjlauer or http://twitter.com/jjlauer) */ public class DeliveryReceipt { - private static final Logger logger = LoggerFactory.getLogger(DeliveryReceipt.class); - - // template format of the dates included with delivery receipts - private static final DateTimeFormatter dateFormatTemplate = DateTimeFormat.forPattern("yyMMddHHmm"); - private static final DateTimeFormatter dateFormatTemplateWithSeconds = DateTimeFormat.forPattern("yyMMddHHmmss"); - // an example of a 3rd format 20110303100008 (yyyyMMddHHmmss) - private static final DateTimeFormatter dateFormatTemplateWithFullYearAndSeconds = DateTimeFormat.forPattern("yyyyMMddHHmmss"); - - public static final String FIELD_ID = "id:"; - public static final String FIELD_SUB = "sub:"; - public static final String FIELD_DLVRD = "dlvrd:"; - public static final String FIELD_SUBMIT_DATE = "submit date:"; - public static final String FIELD_DONE_DATE = "done date:"; - public static final String FIELD_STAT = "stat:"; - public static final String FIELD_ERR = "err:"; - public static final String FIELD_TEXT = "text:"; - - // field "id": id of message originally submitted - private String messageId; - // field "sub": number of messages originally submitted - private int submitCount; - // field "dlvrd": number of messages delivered - private int deliveredCount; - // field "submit date": date message was originally submitted at - private DateTime submitDate; - // field "done date": date message reached a final "done" state - private DateTime doneDate; - // field "stat": final state of message - private byte state; - // field "err": network/smsc specific error code - private int errorCode; - // field "text": first 20 characters of original message - private String text; - - public DeliveryReceipt() { - // do nothing - } - - public DeliveryReceipt(String messageId, int submitCount, int deliveredCount, DateTime submitDate, DateTime doneDate, byte state, int errorCode, String text) { - this.messageId = messageId; - this.submitCount = submitCount; - this.deliveredCount = deliveredCount; - this.submitDate = submitDate; - this.doneDate = doneDate; - this.state = state; - this.errorCode = errorCode; - this.text = text; - } - - public int getDeliveredCount() { - return deliveredCount; - } - - public void setDeliveredCount(int deliveredCount) { - this.deliveredCount = deliveredCount; - } - - public int getErrorCode() { - return errorCode; - } - - public void setErrorCode(int errorCode) { - this.errorCode = errorCode; - } - - public DateTime getDoneDate() { - return doneDate; - } - - public void setDoneDate(DateTime finalDate) { - this.doneDate = finalDate; - } - - public String getMessageId() { - return messageId; - } - - public long getMessageIdAsLong() throws NumberFormatException { - return Long.parseLong(this.messageId); - } - - /** - * Sets the "id" field to the exact String we'll use. Utility method provided - * for setting this value as a long. - * @param messageId - */ - public void setMessageId(String messageId) { - this.messageId = messageId; - } - - /** - * Sets the "id" field parameter as a long value that is zero padded to - * 10 digits. - * @param messageId - */ - public void setMessageId(long messageId) { - this.messageId = String.format("%010d", messageId); - } - - public byte getState() { - return state; - } - - public void setState(byte state) { - this.state = state; - } - - public int getSubmitCount() { - return submitCount; - } - - public void setSubmitCount(int submitCount) { - this.submitCount = submitCount; - } - - public DateTime getSubmitDate() { - return submitDate; - } - - public void setSubmitDate(DateTime submitDate) { - this.submitDate = submitDate; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public String toShortMessage() { - StringBuilder buf = new StringBuilder(200); - buf.append(FIELD_ID); - buf.append(this.messageId); - buf.append(" "); - buf.append(FIELD_SUB); - buf.append(String.format("%03d", this.submitCount)); - buf.append(" "); - buf.append(FIELD_DLVRD); - buf.append(String.format("%03d", this.deliveredCount)); - buf.append(" "); - buf.append(FIELD_SUBMIT_DATE); - if (this.submitDate == null) { - buf.append("0000000000"); - } else { - buf.append(dateFormatTemplate.print(this.submitDate)); - } - buf.append(" "); - buf.append(FIELD_DONE_DATE); - if (this.submitDate == null) { - buf.append("0000000000"); - } else { - buf.append(dateFormatTemplate.print(this.doneDate)); - } - buf.append(" "); - buf.append(FIELD_STAT); - buf.append(toStateText(this.state)); - buf.append(" "); - buf.append(FIELD_ERR); - buf.append(String.format("%03d", this.errorCode)); - buf.append(" "); - buf.append(FIELD_TEXT); - if (this.text != null) { - if (this.text.length() > 20) { - buf.append(this.text.substring(0, 20)); - } else { - buf.append(this.text); - } - } - return buf.toString(); - } - - @Override - public String toString() { - StringBuilder buf = new StringBuilder(160); - buf.append("(id="); - buf.append(this.messageId); - buf.append(" sub="); - buf.append(this.submitCount); - buf.append(" dlvrd="); - buf.append(this.deliveredCount); - buf.append(" submitDate="); - buf.append(this.submitDate); - buf.append(" doneDate="); - buf.append(this.doneDate); - buf.append(" state="); - buf.append(toStateText(this.state)); - buf.append("["); - buf.append(this.state); - buf.append("] err="); - buf.append(this.errorCode); - buf.append(" text=["); - buf.append(this.text); - buf.append("])"); - return buf.toString(); - } - - static private DateTime parseDateTimeHelper(String value, DateTimeZone zone) { - if (value == null) { - return null; - } - // pick the correct template based on length - if (value.length() == 14) { - return dateFormatTemplateWithFullYearAndSeconds.withZone(zone).parseDateTime(value); - } else if(value.length() == 12) { - return dateFormatTemplateWithSeconds.withZone(zone).parseDateTime(value); - } else { - return dateFormatTemplate.withZone(zone).parseDateTime(value); - } - } - - static public void findFieldAndAddToTreeMap(String normalizedText, String field, TreeMap fieldsByStartPos) { - int startPos = normalizedText.indexOf(field); - //logger.debug("Found field " + field + " at startPos " + startPos); - if (startPos >= 0) { - fieldsByStartPos.put(startPos, field); - } - } - - static public DeliveryReceipt parseShortMessage(String shortMessage, DateTimeZone zone) throws DeliveryReceiptException { - return parseShortMessage(shortMessage, zone, true); - } - - /** - * Parses the text of the short message and creates a DeliveryReceipt from - * the fields. This method is lenient as possible. The order of the fields - * does not matter, as well as permitting some fields to be optional. - * @param shortMessage - * @param zone - * @return - * @throws DeliveryReceiptException - */ - static public DeliveryReceipt parseShortMessage(String shortMessage, DateTimeZone zone, boolean checkMissingFields) throws DeliveryReceiptException { - // for case insensitivity, convert to lowercase (normalized text) - String normalizedText = shortMessage.toLowerCase(); - - // create a new DLR with fields set to "uninitialized" values - DeliveryReceipt dlr = new DeliveryReceipt(null, -1, -1, null, null, (byte)-1, -1, null); - TreeMap fieldsByStartPos = new TreeMap(); - - // find location of all possible fields in text of message and add to - // treemap by their startPos so that we'll end up with an ordered list of their occurrence - // a field "value" only technically ends wit the start of the next field "label" - // since a field value could technically contain ":" or spaces - // SMPP really has HORRIBLE specs for delivery receipts... - findFieldAndAddToTreeMap(normalizedText, FIELD_ID, fieldsByStartPos); - findFieldAndAddToTreeMap(normalizedText, FIELD_SUB, fieldsByStartPos); - findFieldAndAddToTreeMap(normalizedText, FIELD_DLVRD, fieldsByStartPos); - findFieldAndAddToTreeMap(normalizedText, FIELD_SUBMIT_DATE, fieldsByStartPos); - findFieldAndAddToTreeMap(normalizedText, FIELD_DONE_DATE, fieldsByStartPos); - findFieldAndAddToTreeMap(normalizedText, FIELD_STAT, fieldsByStartPos); - findFieldAndAddToTreeMap(normalizedText, FIELD_ERR, fieldsByStartPos); - findFieldAndAddToTreeMap(normalizedText, FIELD_TEXT, fieldsByStartPos); - - // process all fields in the order they appear - Map.Entry curFieldEntry = fieldsByStartPos.firstEntry(); - while (curFieldEntry != null) { - Map.Entry nextFieldEntry = fieldsByStartPos.higherEntry(curFieldEntry.getKey()); - - // calculate the positions for the substring to extract the field value - int fieldLabelStartPos = curFieldEntry.getKey().intValue(); - int startPos = fieldLabelStartPos + curFieldEntry.getValue().length(); - int endPos = (nextFieldEntry != null ? nextFieldEntry.getKey().intValue() : normalizedText.length()); - - String fieldLabel = curFieldEntry.getValue(); - String fieldValue = shortMessage.substring(startPos, endPos).trim(); - - //logger.debug("startPos [" + curFieldEntry.getKey() + "] label [" + curFieldEntry.getValue() + "] value [" + fieldValue + "]"); - - if (!StringUtil.isEmpty(fieldValue)) { - if (fieldLabel.equalsIgnoreCase(FIELD_ID)) { - dlr.messageId = fieldValue; - } else if (fieldLabel.equalsIgnoreCase(FIELD_SUB)) { - try { - dlr.submitCount = Integer.parseInt(fieldValue); - } catch (NumberFormatException e) { - throw new DeliveryReceiptException("Unable to convert [sub] field with value [" + fieldValue + "] into an integer"); - } - } else if (fieldLabel.equalsIgnoreCase(FIELD_DLVRD)) { - try { - dlr.deliveredCount = Integer.parseInt(fieldValue); - } catch (NumberFormatException e) { - throw new DeliveryReceiptException("Unable to convert [dlvrd] field with value [" + fieldValue + "] into an integer"); - } - } else if (fieldLabel.equalsIgnoreCase(FIELD_SUBMIT_DATE)) { - try { - dlr.submitDate = parseDateTimeHelper(fieldValue, zone); - } catch (IllegalArgumentException e) { - throw new DeliveryReceiptException("Unable to convert [submit date] field with value [" + fieldValue + "] into a datetime object"); - } - } else if (fieldLabel.equalsIgnoreCase(FIELD_DONE_DATE)) { - try { - dlr.doneDate = parseDateTimeHelper(fieldValue, zone); - } catch (IllegalArgumentException e) { - throw new DeliveryReceiptException("Unable to convert [done date] field with value [" + fieldValue + "] into a datetime object"); - } - } else if (fieldLabel.equalsIgnoreCase(FIELD_STAT)) { - dlr.state = DeliveryReceipt.toState(fieldValue); - if (dlr.state < 0) { - throw new DeliveryReceiptException("Unable to convert [stat] field with value [" + fieldValue + "] into a valid state"); - } - } else if (fieldLabel.equalsIgnoreCase(FIELD_ERR)) { - try { - dlr.errorCode = Integer.parseInt(fieldValue); - } catch (NumberFormatException e) { - throw new DeliveryReceiptException("Unable to convert [err] field with value [" + fieldValue + "] into an integer"); - } - } else if (fieldLabel.equalsIgnoreCase(FIELD_TEXT)) { - dlr.text = fieldValue; - } else { - throw new DeliveryReceiptException("Unsupported field [" + fieldValue + "] found"); - } - } - - curFieldEntry = nextFieldEntry; - } - - if (checkMissingFields) { - if (StringUtil.isEmpty(dlr.messageId)) { - throw new DeliveryReceiptException("Unable to find [id] field or empty value in delivery receipt message"); - } - - if (dlr.submitCount < 0) { - throw new DeliveryReceiptException("Unable to find [sub] field or empty value in delivery receipt message"); - } - - if (dlr.deliveredCount < 0) { - throw new DeliveryReceiptException("Unable to find [dlvrd] field or empty value in delivery receipt message"); - } - - if (dlr.submitDate == null) { - throw new DeliveryReceiptException("Unable to find [submit date] field or empty value in delivery receipt message"); - } - - if (dlr.doneDate == null) { - throw new DeliveryReceiptException("Unable to find [done date] field or empty value in delivery receipt message"); - } - - if (dlr.state < 0) { - throw new DeliveryReceiptException("Unable to find [stat] field or empty value in delivery receipt message"); - } - - if (dlr.errorCode < 0) { - throw new DeliveryReceiptException("Unable to find [err] field or empty value in delivery receipt message"); - } - } - - return dlr; - } - - static public byte toState(String stateText) { - if (stateText == null) { - return -1; - } - - if (stateText.equalsIgnoreCase("DELIVRD")) { - return SmppConstants.STATE_DELIVERED; - } else if (stateText.equalsIgnoreCase("EXPIRED")) { - return SmppConstants.STATE_EXPIRED; - } else if (stateText.equalsIgnoreCase("DELETED")) { - return SmppConstants.STATE_DELETED; - } else if (stateText.equalsIgnoreCase("UNDELIV")) { - return SmppConstants.STATE_UNDELIVERABLE; - } else if (stateText.equalsIgnoreCase("ACCEPTD")) { - return SmppConstants.STATE_ACCEPTED; - } else if (stateText.equalsIgnoreCase("UNKNOWN")) { - return SmppConstants.STATE_UNKNOWN; - } else if (stateText.equalsIgnoreCase("REJECTD")) { - return SmppConstants.STATE_REJECTED; - } else if (stateText.equalsIgnoreCase("ENROUTE")) { - return SmppConstants.STATE_ENROUTE; - } else { - return -1; - } - } - - static public String toStateText(byte state) { - switch (state) { - case SmppConstants.STATE_DELIVERED: - return "DELIVRD"; - case SmppConstants.STATE_EXPIRED: - return "EXPIRED"; - case SmppConstants.STATE_DELETED: - return "DELETED"; - case SmppConstants.STATE_UNDELIVERABLE: - return "UNDELIV"; - case SmppConstants.STATE_ACCEPTED: - return "ACCEPTD"; - case SmppConstants.STATE_UNKNOWN: - return "UNKNOWN"; - case SmppConstants.STATE_REJECTED: - return "REJECTD"; - case SmppConstants.STATE_ENROUTE: - return "ENROUTE"; - default: - return "BADSTAT"; - } - } - - /** - * Converts a long value to a hex string. E.g. 98765432101L to "16fee0e525" - * @param value - * @return - */ - static public String toMessageIdAsHexString(long value) { - return String.format("%x", value); - } - - /** - * Converts a hex string to a long value. E.g. "16fee0e525" to 98765432101L - * @param value - * @return - */ - static public long toMessageIdAsLong(String value) throws NumberFormatException { - return Long.parseLong(value, 16); - } + private static final Logger logger = LoggerFactory + .getLogger(DeliveryReceipt.class); + + // template format of the dates included with delivery receipts + private static final DateTimeFormatter dateFormatTemplate = DateTimeFormat + .forPattern("yyMMddHHmm"); + private static final DateTimeFormatter dateFormatTemplateWithSeconds = DateTimeFormat + .forPattern("yyMMddHHmmss"); + // an example of a 3rd format 20110303100008 (yyyyMMddHHmmss) + private static final DateTimeFormatter dateFormatTemplateWithFullYearAndSeconds = DateTimeFormat + .forPattern("yyyyMMddHHmmss"); + + // the "err" field cannot be longer than 3 chars + public static final int FIELD_ERR_MAX_LEN = 3; + + public static final String FIELD_ID = "id:"; + public static final String FIELD_SUB = "sub:"; + public static final String FIELD_DLVRD = "dlvrd:"; + public static final String FIELD_SUBMIT_DATE = "submit date:"; + public static final String FIELD_DONE_DATE = "done date:"; + public static final String FIELD_STAT = "stat:"; + public static final String FIELD_ERR = "err:"; + public static final String FIELD_TEXT = "text:"; + + // field "id": id of message originally submitted + private String messageId; + // field "sub": number of messages originally submitted + private int submitCount; + // field "dlvrd": number of messages delivered + private int deliveredCount; + // field "submit date": date message was originally submitted at + private DateTime submitDate; + // field "done date": date message reached a final "done" state + private DateTime doneDate; + // field "stat": final state of message + private byte state; + // field "err": network/smsc specific error code + private int errorCode; + // smpp 3.4 spec states that the "err" field is a length 3 c-octet string + // in order to allow for reverse compatibility we will store both values + private String rawErrorCode; + // field "text": first 20 characters of original message + private String text; + + public DeliveryReceipt() { + // do nothing + } + + public DeliveryReceipt(String messageId, int submitCount, + int deliveredCount, DateTime submitDate, DateTime doneDate, + byte state, int errorCode, String text) { + setMessageId(messageId); + setSubmitCount(submitCount); + setDeliveredCount(deliveredCount); + setSubmitDate(submitDate); + setDoneDate(doneDate); + setState(state); + setErrorCode(errorCode); + setText(text); + } + + public DeliveryReceipt(String messageId, int submitCount, + int deliveredCount, DateTime submitDate, DateTime doneDate, + byte state, String errorCode, String text) { + setMessageId(messageId); + setSubmitCount(submitCount); + setDeliveredCount(deliveredCount); + setSubmitDate(submitDate); + setDoneDate(doneDate); + setState(state); + setRawErrorCode(errorCode); + setText(text); + } + + public int getDeliveredCount() { + return deliveredCount; + } + + public void setDeliveredCount(int deliveredCount) { + this.deliveredCount = deliveredCount; + } + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + this.rawErrorCode = String.format("%03d", errorCode); + } + + public String getRawErrorCode() { + return rawErrorCode; + } + + /** + * Smpp 3.4 spec states that the "err" field is a <= 3 c-octet string, this + * field takes that into account and will be chained with the + * {@link #setErrorCode(int)} field if the "err" field is valid + * + * @param rawErrorCode + */ + public void setRawErrorCode(String rawErrorCode) { + this.rawErrorCode = rawErrorCode; + + try { + setErrorCode(Integer.parseInt(rawErrorCode)); + } catch (Exception e) { + setErrorCode(0); + } + } + + public DateTime getDoneDate() { + return doneDate; + } + + public void setDoneDate(DateTime finalDate) { + this.doneDate = finalDate; + } + + public String getMessageId() { + return messageId; + } + + public long getMessageIdAsLong() throws NumberFormatException { + return Long.parseLong(this.messageId); + } + + /** + * Sets the "id" field to the exact String we'll use. Utility method + * provided for setting this value as a long. + * + * @param messageId + */ + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + /** + * Sets the "id" field parameter as a long value that is zero padded to 10 + * digits. + * + * @param messageId + */ + public void setMessageId(long messageId) { + this.messageId = String.format("%010d", messageId); + } + + public byte getState() { + return state; + } + + public void setState(byte state) { + this.state = state; + } + + public int getSubmitCount() { + return submitCount; + } + + public void setSubmitCount(int submitCount) { + this.submitCount = submitCount; + } + + public DateTime getSubmitDate() { + return submitDate; + } + + public void setSubmitDate(DateTime submitDate) { + this.submitDate = submitDate; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String toShortMessage() { + StringBuilder buf = new StringBuilder(200); + buf.append(FIELD_ID); + buf.append(this.messageId); + buf.append(" "); + buf.append(FIELD_SUB); + buf.append(String.format("%03d", this.submitCount)); + buf.append(" "); + buf.append(FIELD_DLVRD); + buf.append(String.format("%03d", this.deliveredCount)); + buf.append(" "); + buf.append(FIELD_SUBMIT_DATE); + if (this.submitDate == null) { + buf.append("0000000000"); + } else { + buf.append(dateFormatTemplate.print(this.submitDate)); + } + buf.append(" "); + buf.append(FIELD_DONE_DATE); + if (this.submitDate == null) { + buf.append("0000000000"); + } else { + buf.append(dateFormatTemplate.print(this.doneDate)); + } + buf.append(" "); + buf.append(FIELD_STAT); + buf.append(toStateText(this.state)); + buf.append(" "); + buf.append(FIELD_ERR); + buf.append(this.rawErrorCode); + buf.append(" "); + buf.append(FIELD_TEXT); + if (this.text != null) { + if (this.text.length() > 20) { + buf.append(this.text.substring(0, 20)); + } else { + buf.append(this.text); + } + } + return buf.toString(); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(160); + buf.append("(id="); + buf.append(this.messageId); + buf.append(" sub="); + buf.append(this.submitCount); + buf.append(" dlvrd="); + buf.append(this.deliveredCount); + buf.append(" submitDate="); + buf.append(this.submitDate); + buf.append(" doneDate="); + buf.append(this.doneDate); + buf.append(" state="); + buf.append(toStateText(this.state)); + buf.append("["); + buf.append(this.state); + buf.append("] err="); + buf.append(this.rawErrorCode); + buf.append(" text=["); + buf.append(this.text); + buf.append("])"); + return buf.toString(); + } + + /** + * Validation method to guarantee that an err value passed in is valid by + * smpp 3.4 spec + * + * @param errorCode + * @return + */ + private static boolean isValidErrorCode(int errorCode) { + return isValidErrorCode(String.format("%03d", errorCode)); + } + + /** + * Validation method to guarantee that an err value passed in is valid by + * smpp 3.4 spec + * + * @param errorCode + * @return + */ + private static boolean isValidErrorCode(String errorCode) { + if (StringUtil.isEmpty(errorCode) + || (!StringUtil.isEmpty(errorCode) && errorCode.length() <= FIELD_ERR_MAX_LEN)) + return true; + else + return false; + } + + static private DateTime parseDateTimeHelper(String value, DateTimeZone zone) { + if (value == null) { + return null; + } + // pick the correct template based on length + if (value.length() == 14) { + return dateFormatTemplateWithFullYearAndSeconds.withZone(zone) + .parseDateTime(value); + } else if (value.length() == 12) { + return dateFormatTemplateWithSeconds.withZone(zone).parseDateTime( + value); + } else { + return dateFormatTemplate.withZone(zone).parseDateTime(value); + } + } + + static public void findFieldAndAddToTreeMap(String normalizedText, + String field, TreeMap fieldsByStartPos) { + int startPos = normalizedText.indexOf(field); + // logger.debug("Found field " + field + " at startPos " + startPos); + if (startPos >= 0) { + fieldsByStartPos.put(startPos, field); + } + } + + static public DeliveryReceipt parseShortMessage(String shortMessage, + DateTimeZone zone) throws DeliveryReceiptException { + return parseShortMessage(shortMessage, zone, true); + } + + /** + * Parses the text of the short message and creates a DeliveryReceipt from + * the fields. This method is lenient as possible. The order of the fields + * does not matter, as well as permitting some fields to be optional. + * + * @param shortMessage + * @param zone + * @return + * @throws DeliveryReceiptException + */ + static public DeliveryReceipt parseShortMessage(String shortMessage, + DateTimeZone zone, boolean checkMissingFields) + throws DeliveryReceiptException { + // for case insensitivity, convert to lowercase (normalized text) + String normalizedText = shortMessage.toLowerCase(); + + // create a new DLR with fields set to "uninitialized" values + DeliveryReceipt dlr = new DeliveryReceipt(null, -1, -1, null, null, + (byte) -1, -1, null); + TreeMap fieldsByStartPos = new TreeMap(); + + // find location of all possible fields in text of message and add to + // treemap by their startPos so that we'll end up with an ordered list + // of their occurrence + // a field "value" only technically ends wit the start of the next field + // "label" + // since a field value could technically contain ":" or spaces + // SMPP really has HORRIBLE specs for delivery receipts... + findFieldAndAddToTreeMap(normalizedText, FIELD_ID, fieldsByStartPos); + findFieldAndAddToTreeMap(normalizedText, FIELD_SUB, fieldsByStartPos); + findFieldAndAddToTreeMap(normalizedText, FIELD_DLVRD, fieldsByStartPos); + findFieldAndAddToTreeMap(normalizedText, FIELD_SUBMIT_DATE, + fieldsByStartPos); + findFieldAndAddToTreeMap(normalizedText, FIELD_DONE_DATE, + fieldsByStartPos); + findFieldAndAddToTreeMap(normalizedText, FIELD_STAT, fieldsByStartPos); + findFieldAndAddToTreeMap(normalizedText, FIELD_ERR, fieldsByStartPos); + findFieldAndAddToTreeMap(normalizedText, FIELD_TEXT, fieldsByStartPos); + + // process all fields in the order they appear + Map.Entry curFieldEntry = fieldsByStartPos + .firstEntry(); + while (curFieldEntry != null) { + Map.Entry nextFieldEntry = fieldsByStartPos + .higherEntry(curFieldEntry.getKey()); + + // calculate the positions for the substring to extract the field + // value + int fieldLabelStartPos = curFieldEntry.getKey().intValue(); + int startPos = fieldLabelStartPos + + curFieldEntry.getValue().length(); + int endPos = (nextFieldEntry != null ? nextFieldEntry.getKey() + .intValue() : normalizedText.length()); + + String fieldLabel = curFieldEntry.getValue(); + String fieldValue = shortMessage.substring(startPos, endPos).trim(); + + // logger.debug("startPos [" + curFieldEntry.getKey() + "] label [" + // + curFieldEntry.getValue() + "] value [" + fieldValue + "]"); + + if (!StringUtil.isEmpty(fieldValue)) { + if (fieldLabel.equalsIgnoreCase(FIELD_ID)) { + dlr.messageId = fieldValue; + } else if (fieldLabel.equalsIgnoreCase(FIELD_SUB)) { + try { + dlr.submitCount = Integer.parseInt(fieldValue); + } catch (NumberFormatException e) { + throw new DeliveryReceiptException( + "Unable to convert [sub] field with value [" + + fieldValue + "] into an integer"); + } + } else if (fieldLabel.equalsIgnoreCase(FIELD_DLVRD)) { + try { + dlr.deliveredCount = Integer.parseInt(fieldValue); + } catch (NumberFormatException e) { + throw new DeliveryReceiptException( + "Unable to convert [dlvrd] field with value [" + + fieldValue + "] into an integer"); + } + } else if (fieldLabel.equalsIgnoreCase(FIELD_SUBMIT_DATE)) { + try { + dlr.submitDate = parseDateTimeHelper(fieldValue, zone); + } catch (IllegalArgumentException e) { + throw new DeliveryReceiptException( + "Unable to convert [submit date] field with value [" + + fieldValue + + "] into a datetime object"); + } + } else if (fieldLabel.equalsIgnoreCase(FIELD_DONE_DATE)) { + try { + dlr.doneDate = parseDateTimeHelper(fieldValue, zone); + } catch (IllegalArgumentException e) { + throw new DeliveryReceiptException( + "Unable to convert [done date] field with value [" + + fieldValue + + "] into a datetime object"); + } + } else if (fieldLabel.equalsIgnoreCase(FIELD_STAT)) { + dlr.state = DeliveryReceipt.toState(fieldValue); + if (dlr.state < 0) { + throw new DeliveryReceiptException( + "Unable to convert [stat] field with value [" + + fieldValue + "] into a valid state"); + } + } else if (fieldLabel.equalsIgnoreCase(FIELD_ERR)) { + if (isValidErrorCode(fieldValue)) + dlr.setRawErrorCode(fieldValue); + else + throw new DeliveryReceiptException( + "The [err] field was not of a valid lengh of <= " + + FIELD_ERR_MAX_LEN); + } else if (fieldLabel.equalsIgnoreCase(FIELD_TEXT)) { + dlr.text = fieldValue; + } else { + throw new DeliveryReceiptException("Unsupported field [" + + fieldValue + "] found"); + } + } + + curFieldEntry = nextFieldEntry; + } + + if (checkMissingFields) { + if (StringUtil.isEmpty(dlr.messageId)) { + throw new DeliveryReceiptException( + "Unable to find [id] field or empty value in delivery receipt message"); + } + + if (dlr.submitCount < 0) { + throw new DeliveryReceiptException( + "Unable to find [sub] field or empty value in delivery receipt message"); + } + + if (dlr.deliveredCount < 0) { + throw new DeliveryReceiptException( + "Unable to find [dlvrd] field or empty value in delivery receipt message"); + } + + if (dlr.submitDate == null) { + throw new DeliveryReceiptException( + "Unable to find [submit date] field or empty value in delivery receipt message"); + } + + if (dlr.doneDate == null) { + throw new DeliveryReceiptException( + "Unable to find [done date] field or empty value in delivery receipt message"); + } + + if (dlr.state < 0) { + throw new DeliveryReceiptException( + "Unable to find [stat] field or empty value in delivery receipt message"); + } + + if (StringUtil.isEmpty(dlr.rawErrorCode) && dlr.errorCode < 0) { + throw new DeliveryReceiptException( + "Unable to find [err] field or empty value in delivery receipt message"); + } + } + + return dlr; + } + + static public byte toState(String stateText) { + if (stateText == null) { + return -1; + } + + if (stateText.equalsIgnoreCase("DELIVRD")) { + return SmppConstants.STATE_DELIVERED; + } else if (stateText.equalsIgnoreCase("EXPIRED")) { + return SmppConstants.STATE_EXPIRED; + } else if (stateText.equalsIgnoreCase("DELETED")) { + return SmppConstants.STATE_DELETED; + } else if (stateText.equalsIgnoreCase("UNDELIV")) { + return SmppConstants.STATE_UNDELIVERABLE; + } else if (stateText.equalsIgnoreCase("ACCEPTD")) { + return SmppConstants.STATE_ACCEPTED; + } else if (stateText.equalsIgnoreCase("UNKNOWN")) { + return SmppConstants.STATE_UNKNOWN; + } else if (stateText.equalsIgnoreCase("REJECTD")) { + return SmppConstants.STATE_REJECTED; + } else if (stateText.equalsIgnoreCase("ENROUTE")) { + return SmppConstants.STATE_ENROUTE; + } else { + return -1; + } + } + + static public String toStateText(byte state) { + switch (state) { + case SmppConstants.STATE_DELIVERED: + return "DELIVRD"; + case SmppConstants.STATE_EXPIRED: + return "EXPIRED"; + case SmppConstants.STATE_DELETED: + return "DELETED"; + case SmppConstants.STATE_UNDELIVERABLE: + return "UNDELIV"; + case SmppConstants.STATE_ACCEPTED: + return "ACCEPTD"; + case SmppConstants.STATE_UNKNOWN: + return "UNKNOWN"; + case SmppConstants.STATE_REJECTED: + return "REJECTD"; + case SmppConstants.STATE_ENROUTE: + return "ENROUTE"; + default: + return "BADSTAT"; + } + } + + /** + * Converts a long value to a hex string. E.g. 98765432101L to "16fee0e525" + * + * @param value + * @return + */ + static public String toMessageIdAsHexString(long value) { + return String.format("%x", value); + } + + /** + * Converts a hex string to a long value. E.g. "16fee0e525" to 98765432101L + * + * @param value + * @return + */ + static public long toMessageIdAsLong(String value) + throws NumberFormatException { + return Long.parseLong(value, 16); + } } \ No newline at end of file diff --git a/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java b/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java index c249b6ee..32f6ccbc 100644 --- a/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java +++ b/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java @@ -325,4 +325,32 @@ public void parseReceiptWithMissingSubAndDlvrdFields() throws Exception { Assert.assertEquals(0, dlr.getErrorCode()); Assert.assertNull(dlr.getText()); } + + @Test + public void parseShortMessageWithSmpp3_4SpecCompliantErrAsStringValue() throws Exception { + String receipt0 = "id:0123456789 sub:002 dlvrd:001 submit date:1005232039 done date:1005242339 stat:DELIVRD err:21b text:This is a sample mes"; + + DeliveryReceipt dlr = DeliveryReceipt.parseShortMessage(receipt0, DateTimeZone.UTC); + + Assert.assertEquals("21b", dlr.getRawErrorCode()); + + // being set if we cannot parse value + Assert.assertEquals(0, dlr.getErrorCode()); + + Assert.assertEquals(receipt0, dlr.toShortMessage()); + } + + @Test + public void parseShortMessageWithSmpp3_4SpecCompliantErrAsIntValue() throws Exception { + String receipt0 = "id:0123456789 sub:002 dlvrd:001 submit date:1005232039 done date:1005242339 stat:DELIVRD err:010 text:This is a sample mes"; + + DeliveryReceipt dlr = DeliveryReceipt.parseShortMessage(receipt0, DateTimeZone.UTC); + + Assert.assertEquals("010", dlr.getRawErrorCode()); + + // being set if we cannot parse value + Assert.assertEquals(10, dlr.getErrorCode()); + + Assert.assertEquals(receipt0, dlr.toShortMessage()); + } } From 3ab9119bcd51267a5ab730b63f57cb334b65a4a5 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Fri, 11 Oct 2013 06:50:35 -0600 Subject: [PATCH 2/4] validation that all unit tests are passing --- .../java/com/cloudhopper/smpp/util/DeliveryReceipt.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java index 18225df2..9105f867 100644 --- a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java +++ b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java @@ -85,7 +85,7 @@ public class DeliveryReceipt { private String text; public DeliveryReceipt() { - // do nothing + setErrorCode(0); } public DeliveryReceipt(String messageId, int submitCount, @@ -128,7 +128,9 @@ public int getErrorCode() { public void setErrorCode(int errorCode) { this.errorCode = errorCode; - this.rawErrorCode = String.format("%03d", errorCode); + + if (StringUtil.isEmpty(this.rawErrorCode)) + this.rawErrorCode = String.format("%03d", errorCode); } public String getRawErrorCode() { From fecba099f592f74da0bd4c6270e1d79a1c4cd186 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Sun, 13 Oct 2013 20:50:06 -0600 Subject: [PATCH 3/4] introduced the #rawErrorCode to allow for the String representation as well as maintaining backward compatibility of the int value --- .../smpp/util/DeliveryReceipt.java | 36 +++++++++---------- .../smpp/util/DeliveryReceiptTest.java | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java index 9105f867..fe2cef4b 100644 --- a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java +++ b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java @@ -91,27 +91,27 @@ public DeliveryReceipt() { public DeliveryReceipt(String messageId, int submitCount, int deliveredCount, DateTime submitDate, DateTime doneDate, byte state, int errorCode, String text) { - setMessageId(messageId); - setSubmitCount(submitCount); - setDeliveredCount(deliveredCount); - setSubmitDate(submitDate); - setDoneDate(doneDate); - setState(state); - setErrorCode(errorCode); - setText(text); + this.messageId = messageId; + this.submitCount = submitCount; + this.deliveredCount = deliveredCount; + this.submitDate = submitDate; + this.doneDate = doneDate; + this.state = state; + this.errorCode = errorCode; + this.text = text; } public DeliveryReceipt(String messageId, int submitCount, int deliveredCount, DateTime submitDate, DateTime doneDate, byte state, String errorCode, String text) { - setMessageId(messageId); - setSubmitCount(submitCount); - setDeliveredCount(deliveredCount); - setSubmitDate(submitDate); - setDoneDate(doneDate); - setState(state); + this.messageId = messageId; + this.submitCount = submitCount; + this.deliveredCount = deliveredCount; + this.submitDate = submitDate; + this.doneDate = doneDate; + this.state = state; setRawErrorCode(errorCode); - setText(text); + this.text = text; } public int getDeliveredCount() { @@ -129,8 +129,7 @@ public int getErrorCode() { public void setErrorCode(int errorCode) { this.errorCode = errorCode; - if (StringUtil.isEmpty(this.rawErrorCode)) - this.rawErrorCode = String.format("%03d", errorCode); + this.rawErrorCode = String.format("%03d", errorCode); } public String getRawErrorCode() { @@ -148,9 +147,8 @@ public void setRawErrorCode(String rawErrorCode) { this.rawErrorCode = rawErrorCode; try { - setErrorCode(Integer.parseInt(rawErrorCode)); + this.errorCode = Integer.parseInt(rawErrorCode); } catch (Exception e) { - setErrorCode(0); } } diff --git a/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java b/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java index 32f6ccbc..f9a4f46b 100644 --- a/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java +++ b/src/test/java/com/cloudhopper/smpp/util/DeliveryReceiptTest.java @@ -335,7 +335,7 @@ public void parseShortMessageWithSmpp3_4SpecCompliantErrAsStringValue() throws E Assert.assertEquals("21b", dlr.getRawErrorCode()); // being set if we cannot parse value - Assert.assertEquals(0, dlr.getErrorCode()); + Assert.assertEquals(-1, dlr.getErrorCode()); Assert.assertEquals(receipt0, dlr.toShortMessage()); } From cdd48d794be6dedb1c55883a8540503eed3dcc9c Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Sun, 13 Oct 2013 20:53:54 -0600 Subject: [PATCH 4/4] addition of some header comments --- .../java/com/cloudhopper/smpp/util/DeliveryReceipt.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java index fe2cef4b..bc1e4221 100644 --- a/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java +++ b/src/main/java/com/cloudhopper/smpp/util/DeliveryReceipt.java @@ -35,6 +35,12 @@ * Utility class to represent a Delivery Receipt that may be contained within a * DataSm or DeliverSm PDU. A delivery receipt has a specific message text and a * few specific optional parameters. + *

+ * If the {@link #setRawErrorCode(String)} method takes in a String that is not + * parseable to an int via {@link Integer#parseInt(String)} then the + * {@link #errorCode} property will remain what it was originally set as, + * default(int) or in the case of + * {@link #parseShortMessage(String, DateTimeZone)} -1. * * @author joelauer (twitter: @jjlauer or http://twitter.com/jjlauer)