/* * FrontlineSMS <http://www.frontlinesms.com> * Copyright 2007, 2008 kiwanja * * This file is part of FrontlineSMS. * * FrontlineSMS is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * FrontlineSMS is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with FrontlineSMS. If not, see <http://www.gnu.org/licenses/>. */ package net.frontlinesms.data.domain; import java.util.Arrays; import javax.persistence.*; import org.hibernate.annotations.DiscriminatorFormula; import org.smslib.util.GsmAlphabet; import org.smslib.util.HexUtils; import org.smslib.util.TpduUtils; import net.frontlinesms.FrontlineSMSConstants; import net.frontlinesms.data.EntityField; import net.frontlinesms.ui.i18n.Internationalised; /** * Object representing an SMS message in our data structure. * @author Alex */ @Entity // This class is mapped to the database table called "message", as this class used to be called "Message" @Table(name="message") @DiscriminatorFormula("(CASE WHEN dtype IS NULL THEN 'FrontlineMessage' ELSE dtype END)") public class FrontlineMessage { /** Discriminator column for this class. This was only implemented when {@link FrontlineMultimediaMessage} was * added. Setting it to null will result in a plain {@link FrontlineMessage} being instantiated, as per the * {@link DiscriminatorFormula} annotation on this class. */ @SuppressWarnings("unused") private String dtype = this.getClass().getSimpleName(); //> DATABASE COLUMN NAMES /** Database column name for field {@link #textMessageContent} */ private static final String COLUMN_TEXT_CONTENT = "textContent"; //> CONSTANTS public enum Type { /** This is a pseudo-message type, used as a blanket for all types. */ ALL, /** Message type: unknown */ UNKNOWN, /** Message type: received */ RECEIVED, /** Message type: outbound */ OUTBOUND, /** Message type: delivery report */ DELIVERY_REPORT; } public enum Status implements Internationalised { /** Message status: DRAFT - nothing has been done with this message yet */ DRAFT(FrontlineSMSConstants.COMMON_DRAFT), /** messages of TYPE_RECEIVED should always be STATUS_RECEIVED */ RECEIVED(FrontlineSMSConstants.COMMON_RECEIVED), /** outgoing message that is created, and will be sent to a phone as soon as one is available */ OUTBOX(FrontlineSMSConstants.COMMON_OUTBOX), /** outgoing message given to a phone, which the phone is trying to send */ PENDING(FrontlineSMSConstants.COMMON_PENDING), /** outgoing message successfully delivered to the GSM network*/ SENT(FrontlineSMSConstants.COMMON_SENT), /** outgoing message that has had delivery confirmed by the GSM network */ DELIVERED(FrontlineSMSConstants.COMMON_DELIVERED), /** Outgoing message that had status KEEP TRYING returned by the GSM network */ KEEP_TRYING(FrontlineSMSConstants.COMMON_RETRYING), @Deprecated ABORTED(null), @Deprecated UNKNOWN(null), /** Outgoing message that had status FAILED returned by the GSM network */ FAILED(FrontlineSMSConstants.COMMON_FAILED); private final String i18nKey; private Status(String i18nKey) { this.i18nKey = i18nKey; } public String getI18nKey() { return i18nKey; } } /** Number of times a failed message send is retried before status is set to STATUS_FAILED */ public static final int MAX_RETRIES = 2; /** The maximum number of parts in an SMS message. TODO rename this SMS_PART_LIMIT */ public static final int SMS_LIMIT = 255; /** Maximum number of characters that can be fit into a single 7-bit GSM SMS message. TODO this value should probably be fetched from {@link TpduUtils}. */ public static final int SMS_LENGTH_LIMIT = 160; /** Maximum number of characters that can be fit in one part of a multipart 7-bit GSM SMS message. TODO this number is incorrect, I suspect. The value should probably be fetched from {@link TpduUtils}. */ public static final int SMS_MULTIPART_LENGTH_LIMIT = 135; /** Maximum number of characters that can be fit into a single UCS-2 SMS message. TODO this value should probably be fetched from {@link TpduUtils}. */ public static final int SMS_LENGTH_LIMIT_UCS2 = 70; /** Maximum number of characters that can be fit in one part of a multipart UCS-2 SMS message. TODO this number is incorrect, I suspect. The value should probably be fetched from {@link TpduUtils}. */ public static final int SMS_MULTIPART_LENGTH_LIMIT_UCS2 = 60; /** Maximum number of characters that can be fit into a single binary SMS message. TODO this value should probably be fetched from {@link TpduUtils}. */ public static final int SMS_LENGTH_LIMIT_BINARY = 140; /** Maximum number of characters that can be fit in one part of a binary SMS message. TODO this number is incorrect, I suspect. The value should probably be fetched from {@link TpduUtils}. */ public static final int SMS_MULTIPART_LENGTH_LIMIT_BINARY = 120; /** Maximum number of characters that can be fit into a 255-part GSM 7bit message */ public static final int SMS_MAX_CHARACTERS = 255 * SMS_MULTIPART_LENGTH_LIMIT; //> ENTITY FIELDS /** Details of the fields that this class has. */ public enum Field implements EntityField<FrontlineMessage> { ID("id"), TYPE("type"), DATE("date"), STATUS("status"), SENDER_MSISDN("senderMsisdn"), RECIPIENT_MSISDN("recipientMsisdn"), MESSAGE_CONTENT("textMessageContent"), SMSC_REFERENCE("smscReference"); /** name of a field */ private final String fieldName; /** * Creates a new {@link Field} * @param fieldName name of the field */ Field(String fieldName) { this.fieldName = fieldName; } /** @see EntityField#getFieldName() */ public String getFieldName() { return this.fieldName; } } //> INSTANCE PROPERTIES /** Unique id for this entity. This is for hibernate usage. */ @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(unique=true,nullable=false,updatable=false) @SuppressWarnings("unused") private long id; private Type type; private int retriesRemaining; private Status status; private String recipientMsisdn; private int recipientSmsPort; private int smsPartsCount; private long date; private Integer smscReference; private String senderMsisdn; /** Text content of this message. */ @Column(name=COLUMN_TEXT_CONTENT, length=SMS_MAX_CHARACTERS) private String textMessageContent; /** Binary content of this message. */ private byte[] binaryMessageContent; //> CONSTRUCTOR /** Default constructor empty for hibernate */ FrontlineMessage() {} protected FrontlineMessage(Type type, String textContent) { this.type = type; this.setTextMessageContent(textContent); this.setSmsPartsCount(getExpectedSmsCount()); } //> ACCESSOR METHODS /** * Gets the type of this Message. Should be one of the Message.TYPE_ constants. * @return */ public Type getType() { return this.type; } /** * Gets the status of this Message. Should be one of the Message.STATUS_ constants. * @return */ public Status getStatus() { return this.status; } /** * sets the type of this Message. Should be one of the Message.STATUS_ constants. * only allows you to change the status of an outgoing message * @param messageStatus */ public void setStatus(Status messageStatus) { this.status = messageStatus; } /** * Gets the MSISDN (phone number) of the sender of this message. * @return */ public String getSenderMsisdn() { return this.senderMsisdn; } /** * sets the sender number of an outgoing message, * usually done once it is assigned to an outgoing device, * if the MSISDN is known, or manually assigned to the device. * @param senderPhoneNumber new value for {@link #senderMsisdn} */ public void setSenderMsisdn(String senderPhoneNumber) { this.senderMsisdn = senderPhoneNumber; } /** * Sets {@link #recipientMsisdn} * @param recipientPhoneNumber new value for {@link #recipientMsisdn} */ public void setRecipientMsisdn(String recipientPhoneNumber) { this.recipientMsisdn = recipientPhoneNumber; } /** * Sets {@link #recipientSmsPort} * @param recipientSmsPort new value for {@link #recipientSmsPort} */ public void setRecipientSmsPort(int recipientSmsPort) { this.recipientSmsPort = recipientSmsPort; } /** * Gets the MSISDN (phone number) of the recipient of this message. * @return */ public String getRecipientMsisdn() { return this.recipientMsisdn; } /** * Gets the sms port of the recipient of this message, or -1 * if none is specified. * @return {@link #recipientSmsPort} */ public int getRecipientSmsPort() { return this.recipientSmsPort; } /** * Gets the text content of this message. * @return {@link #textMessageContent} */ public String getTextContent() { return this.getTextMessageContent(); } /** * Gets the binary content of this message. * @return {@link #binaryMessageContent} */ public byte[] getBinaryContent() { return this.binaryMessageContent; } /** * Gets the number of SMS sent. * @return the number of parts this message was sent as */ public int getNumberOfSMS() { return this.getSmsPartsCount() == 0 ? this.getExpectedSmsCount() : this.getSmsPartsCount(); } /** * Gets the date at which this message was sent (messages of TYPE_SENT) * or received (messages of TYPE_RECEIVED). * @return */ public long getDate() { return this.date; } /** * @return the SMSC reference number of this Message. this appears after a message is sent, so that * delivery reciepts can be matched up to previous messages. */ public Integer getSmscReference() { return this.smscReference; } /** * sets the SMSC reference number of this Message. this appears after a message is sent, so that * delivery reciepts can be matched up to previous messages. * Don't set this for incoming messages * @param smscReference */ public void setSmscReference(int smscReference) { this.smscReference = smscReference; } /** @return the retries left for this message */ public int getRetriesRemaining() { return this.retriesRemaining; } /** sets the retries left for this message */ public void setRetriesRemaining(int retries) { this.retriesRemaining = retries; } /** * Check whether the content of this message is binary or text * @return <code>true</code> if the content of this message is binary; <code>false</code> otherwise. */ public boolean isBinaryMessage() { return this.binaryMessageContent != null; } /** @return the number of SMS parts that we'd expect this message to take */ private int getExpectedSmsCount() { if(this.isBinaryMessage()) { int octetCount = this.getBinaryContent().length; if(octetCount <= SMS_LENGTH_LIMIT_BINARY) { return 1; } else { return (int) Math.ceil(octetCount / (double)SMS_MULTIPART_LENGTH_LIMIT_BINARY); } } else { int expectedNumberOfSmsParts = FrontlineMessage.getExpectedNumberOfSmsParts(this.getTextContent()); if(expectedNumberOfSmsParts == 0) { // the method used above can return 0 in some cases. An empty message will still cost money. expectedNumberOfSmsParts = 1; } return expectedNumberOfSmsParts; } } //> STATIC FACTORY METHODS /** * Creates an binary incoming message in the internal data structure. * @param dateReceived The date this message was received. * @param senderMsisdn The MSISDN (phone number) of the sender of this message. * @param recipientMsisdn The MSISDN (phone number) of the recipient of this message. * @param recipientPort * @param content * @return Message object representing the sent message. */ public static FrontlineMessage createBinaryIncomingMessage(long dateReceived, String senderMsisdn, String recipientMsisdn, int recipientPort, byte[] content) { FrontlineMessage m = new FrontlineMessage(); m.type = Type.RECEIVED; m.status = Status.RECEIVED; m.setDate(dateReceived); m.senderMsisdn = senderMsisdn; m.recipientMsisdn = recipientMsisdn; m.recipientSmsPort = recipientPort; m.binaryMessageContent = content; m.setTextMessageContent(HexUtils.encode(content)); return m; } /** * Creates an binary outgoing message in the internal data structure. * @param dateSent The date at which this message was sent. * @param senderMsisdn The MSISDN (phone number) of the sender of this message * @param recipientMsisdn The MSISDN (phone number) of the recipient of this message * @param recipientPort * @param content * @return a Message object representing the received message. * * FIXME rename this to createOutgoingFormMessage as that is what it is. */ public static FrontlineMessage createBinaryOutgoingMessage(long dateSent, String senderMsisdn, String recipientMsisdn, int recipientPort, byte[] content) { FrontlineMessage m = new FrontlineMessage(); m.type = Type.OUTBOUND; m.status = Status.DRAFT; m.setDate(dateSent); m.senderMsisdn = senderMsisdn; m.recipientMsisdn = recipientMsisdn; m.recipientSmsPort = recipientPort; m.binaryMessageContent = content; m.setTextMessageContent(HexUtils.encode(content)); return m; } /** * Creates an outgoing message in the internal data structure. * @param dateSent The date at which this message was sent. * @param senderMsisdn The MSISDN (phone number) of the sender of this message * @param recipientMsisdn The MSISDN (phone number) of the recipient of this message * @param messageContent The text content of this message. * @return a Message object representing the received message. */ public static FrontlineMessage createOutgoingMessage(long dateSent, String senderMsisdn, String recipientMsisdn, String messageContent) { FrontlineMessage m = new FrontlineMessage(); m.type = Type.OUTBOUND; m.status = Status.DRAFT; m.setDate(dateSent); m.senderMsisdn = senderMsisdn; m.recipientMsisdn = recipientMsisdn; m.setTextMessageContent(messageContent); return m; } /** * Creates an incoming message in the internal data structure. * @param dateReceived The date this message was received. * @param senderMsisdn The MSISDN (phone number) of the sender of this message. * @param recipientMsisdn The MSISDN (phone number) of the recipient of this message. * @param messageContent The text content of this message. * @returna Message object representing the sent message. */ public static FrontlineMessage createIncomingMessage(long dateReceived, String senderMsisdn, String recipientMsisdn, String messageContent) { FrontlineMessage m = new FrontlineMessage(); m.type = Type.RECEIVED; m.status = Status.RECEIVED; m.setDate(dateReceived); m.senderMsisdn = senderMsisdn; m.recipientMsisdn = recipientMsisdn; m.setTextMessageContent(messageContent); return m; } //> GENERATED METHODS /** * {@link #status} and {@link #smscReference} are not included in {@link #equals(Object)} or {@link #hashCode()} * as they are liable to change throughout a message's lifetime. Likewise, {@link #senderMsisdn} is ignored for * {@link Type#OUTBOUND} and {@link #recipientMsisdn} is ignored for {@link Type#RECEIVED} and {@link Type#DELIVERY_REPORT}. * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (getDate() ^ (getDate() >>> 32)); result = prime * result + Arrays.hashCode(binaryMessageContent); result = prime * result + ((getTextMessageContent() == null) ? 0 : getTextMessageContent().hashCode()); if(!(type == Type.RECEIVED || type == Type.DELIVERY_REPORT)) { result = prime * result + ((recipientMsisdn == null) ? 0 : recipientMsisdn.hashCode()); } result = prime * result + recipientSmsPort; result = prime * result + retriesRemaining; if(type != Type.OUTBOUND) { result = prime * result + ((senderMsisdn == null) ? 0 : senderMsisdn.hashCode()); } result = prime * result + getSmsPartsCount(); result = prime * result + (type==null ? 0 : type.hashCode()); return result; } /** * {@link #status} and {@link #smscReference} are not included in {@link #equals(Object)} or {@link #hashCode()} * as they are liable to change throughout a message's lifetime. * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; FrontlineMessage other = (FrontlineMessage) obj; if (getDate() != other.getDate()) return false; if (!Arrays.equals(binaryMessageContent, other.binaryMessageContent)) return false; if (getTextMessageContent() == null) { if (other.getTextMessageContent() != null) return false; } else if (!getTextMessageContent().equals(other.getTextMessageContent())) return false; if(!(type == Type.RECEIVED || type == Type.DELIVERY_REPORT)) { if (recipientMsisdn == null) { if (other.recipientMsisdn != null) return false; } else if (!recipientMsisdn.equals(other.recipientMsisdn)) return false; } if (recipientSmsPort != other.recipientSmsPort) return false; if (retriesRemaining != other.retriesRemaining) return false; if(type != Type.OUTBOUND) { if (senderMsisdn == null) { if (other.senderMsisdn != null) return false; } else if (!senderMsisdn.equals(other.senderMsisdn)) return false; } if (getSmsPartsCount() != other.getSmsPartsCount()) return false; if (type != other.type) return false; return true; } /** * Calculate the expected number of SMS parts required to send a text message. * This method <strong>will not work</strong> for <em>binary</em> messages. * @param message the text content of the message * @return the number of SMS parts that we'd expect the supplied message to use, or <code>0</code> if no supplied message has zero length. */ public static int getExpectedNumberOfSmsParts(String message) { int messageLength = message.length(); boolean areAllCharactersValidGSM = GsmAlphabet.areAllCharactersValidGSM(message); int singleMessageCharacterLimit, multipartMessageCharacterLimit; if(areAllCharactersValidGSM) { singleMessageCharacterLimit = FrontlineMessage.SMS_LENGTH_LIMIT; multipartMessageCharacterLimit = FrontlineMessage.SMS_MULTIPART_LENGTH_LIMIT; } else { // It appears there are some unicode-only characters here. We should therefore // treat this message as if it will be sent as unicode. singleMessageCharacterLimit = FrontlineMessage.SMS_LENGTH_LIMIT_UCS2; multipartMessageCharacterLimit = FrontlineMessage.SMS_MULTIPART_LENGTH_LIMIT_UCS2; } if (messageLength > getTotalLengthAllowed(message)) { return (int)Math.ceil((double)messageLength / (double)multipartMessageCharacterLimit); } else { if (messageLength <= singleMessageCharacterLimit) { return messageLength == 0 ? 0 : 1; } else { return (int)Math.ceil(messageLength / (double)multipartMessageCharacterLimit); } } } public void setDate(long date) { this.date = date; } public static int getTotalLengthAllowed(String message) { boolean areAllCharactersValidGSM = GsmAlphabet.areAllCharactersValidGSM(message); if (areAllCharactersValidGSM) { return FrontlineMessage.SMS_LENGTH_LIMIT + FrontlineMessage.SMS_MULTIPART_LENGTH_LIMIT * (FrontlineMessage.SMS_LIMIT - 1); } else { return FrontlineMessage.SMS_LENGTH_LIMIT_UCS2 + FrontlineMessage.SMS_MULTIPART_LENGTH_LIMIT_UCS2 * (FrontlineMessage.SMS_LIMIT - 1); } } public void setTextMessageContent(String textMessageContent) { this.textMessageContent = textMessageContent; } // FIXME what does this method provide which getTextContent() does not? N.B. obviously don't rename the field unless appropriate hibernate mapping is applied private String getTextMessageContent() { return textMessageContent; } public void setSmsPartsCount(int smsPartsCount) { this.smsPartsCount = smsPartsCount; } public int getSmsPartsCount() { return smsPartsCount; } }