All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cloudhopper.smpp.util.DeliveryReceipt Maven / Gradle / Ivy

Go to download

Efficient, scalable, and flexible Java implementation of the Short Messaging Peer to Peer Protocol (SMPP)

There is a newer version: 5.1.0-113
Show newest version
package com.cloudhopper.smpp.util;

/*
 * #%L
 * ch-smpp
 * %%
 * Copyright (C) 2009 - 2015 Cloudhopper by Twitter
 * %%
 * 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.
 * #L%
 */

import com.cloudhopper.commons.util.StringUtil;
import com.cloudhopper.smpp.SmppConstants;
import java.util.Map;
import java.util.TreeMap;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
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.
 * 

* 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) */ 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"); // 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() { setErrorCode(0); } 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; setErrorCode(errorCode); this.text = text; } public DeliveryReceipt(String messageId, int submitCount, int deliveredCount, DateTime submitDate, DateTime doneDate, byte state, String errorCode, String text) { this.messageId = messageId; this.submitCount = submitCount; this.deliveredCount = deliveredCount; this.submitDate = submitDate; this.doneDate = doneDate; this.state = state; setRawErrorCode(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; 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 { this.errorCode = Integer.parseInt(rawErrorCode); } catch (Exception e) { } } 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.doneDate == 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 { return parseShortMessage(shortMessage, zone, checkMissingFields, 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. * This method allows to completely switch off field validation, in which * case the returned DeliveryReceipt will contain default values for * missing fields, or fields that parsing failed * * @param shortMessage the body of the delivery receipt * @param zone the time zone to use for interpreting time stamps * @param checkMissingFields whether to throw exception if fields were missing * @param validateFields whether to throw exception if fields cannot be parsed * @return a DeliveryReceipt object from the parsed information in the short message * @throws DeliveryReceiptException if checkMissingFields is true and there are fields missing, * or if validateFields is true and fields cannot be parsed */ static public DeliveryReceipt parseShortMessage(String shortMessage, DateTimeZone zone, boolean checkMissingFields, boolean validateFields) 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) { if (validateFields) { 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) { if (validateFields) { 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) { if (validateFields) { 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) { if (validateFields) { 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 && validateFields) { throw new DeliveryReceiptException( "Unable to convert [stat] field with value [" + fieldValue + "] into a valid state"); } } else if (fieldLabel.equalsIgnoreCase(FIELD_ERR)) { if (!validateFields || 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 { if (validateFields) { 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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy