/* * 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 javax.persistence.*; import net.frontlinesms.FrontlineSMSConstants; import net.frontlinesms.FrontlineUtils; import net.frontlinesms.csv.CsvUtils; import net.frontlinesms.data.EntityField; import net.frontlinesms.messaging.MessageFormatter; /** * @author Alex Anderson alex(at)masabi(dot)com */ @Entity public class KeywordAction { /** Table name */ public static final String TABLE_NAME = "KeywordAction"; //> ENTITY FIELDS /** Details of the fields that this class has. */ public enum Field implements EntityField<KeywordAction> { /** Field name for {@link KeywordAction#type} */ TYPE("type"), /** File name for {@link KeywordAction#keyword} */ KEYWORD("keyword"), /** Counter */ COUNTER("counter"); /** 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; } } //> CONSTANTS public enum Type { /** No action. This type should only occur in test code. */ NO_ACTION, /** Action: forward the received message to a group */ FORWARD, /** Action: add the sender's msisdn to a group */ JOIN, /** Action: remove the sender's msisdn from a group */ LEAVE, /** Reply: send a specified reply to the sender's msisdn */ REPLY, /** Action: executes an external command */ EXTERNAL_CMD, /** Action: send an e-mail */ EMAIL; } public enum ExternalCommandType { HTTP_REQUEST, COMMAND_LINE; } /** * The action type of the external command. TODO these should probably be represented with a pair of booleans. * These are only used when {@link #externalCommandResponseType} is * {@link ExternalCommandResponseType#PLAIN_TEXT} */ public enum ExternalCommandResponseActionType { REPLY, FORWARD, REPLY_AND_FORWARD, DO_NOTHING; } public enum ExternalCommandResponseType { PLAIN_TEXT, LIST_COMMANDS, DONT_WAIT; } //> CONSTRUCTORS /** * Default constructor, to be used by hibernate. * This constructor should <b>not</b> be used in factory methods. */ KeywordAction() {} /** * Constructor for <b>unit tests only</b>. */ KeywordAction(Type type) { assert(type != null) : "Type cannot be NULL."; this.type = type; } /** * Creates a new keyword action and sets the keyword for it. * @param type The type of this keyword. * @param keyword value for {@link #keyword}. */ KeywordAction(Type type, Keyword keyword) { assert(keyword!=null): "You must supply a " + Keyword.class.getSimpleName() + " for your new " + getClass().getSimpleName(); this.type = type; this.keyword = keyword; } //> 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; /** Keyword which this action is attached to */ @ManyToOne(targetEntity=Keyword.class, optional=false) private Keyword keyword; @ManyToOne(optional=true) private Group group; @ManyToOne(optional=true) private EmailAccount emailAccount; @Column(length=FrontlineMessage.SMS_MAX_CHARACTERS) private String commandString; private int commandInteger; private int counter; private long startDate; private long endDate; private String emailRecipients; private String emailSubject; private ExternalCommandType externalCommandType; @Column(length=FrontlineSMSConstants.EXTERNAL_COMMAND_MAX_LENGTH) private String externalCommand; private ExternalCommandResponseType externalCommandResponseType; private ExternalCommandResponseActionType externalCommandResponseActionType; //> ACCESSOR METHODS /** @return {@link #type} */ public Type getType() { return this.type; } /** @return the external command type */ public ExternalCommandType getExternalCommandType() { assert(this.type==Type.EXTERNAL_CMD) : "This method cannot be called on an action of type " + type; return this.externalCommandType; } /** @return the external command response type */ public ExternalCommandResponseType getExternalCommandResponseType() { assert(this.type==Type.EXTERNAL_CMD) : "Cannot get command response from type: " + type; return externalCommandResponseType; } /** * Sets the external command response type of this instance of KeywordAction. * @param type new value for {@link #externalCommandResponseType} */ public void setExternalCommandResponseType(ExternalCommandResponseType type) { assert(this.type==Type.EXTERNAL_CMD) : "Cannot set command response from type: " + type; this.externalCommandResponseType = type; } /** * Gets the external command response action type of this instance of KeywordAction. * @return */ public ExternalCommandResponseActionType getCommandResponseActionType() { assert(this.type==Type.EXTERNAL_CMD) : "Cannot get command response action from type: " + type; return externalCommandResponseActionType; } /** * Sets the external command response action type of this instance of KeywordAction. * @param type */ public void setCommandResponseActionType(ExternalCommandResponseActionType type) { assert(this.type==Type.EXTERNAL_CMD) : "Cannot get command response action from type: " + type; this.externalCommandResponseActionType = type; } /** @param type new value for {@link #externalCommandType} */ public void setExternalCommandType(ExternalCommandType type) { this.externalCommandType = type; } /** * @param time The time that this action was triggered * @return <code>true</code> if the current date is within {@link #startDate} and {@link #endDate}; <code>false</code> otherwise. */ public boolean isAlive(long time) { return time >= this.startDate && time <= this.endDate; } /** * Gets this action start date. * @return {@link #startDate} */ public long getStartDate() { return this.startDate; } /** * Gets this action email recipients. * @return {@link #emailRecipients} */ public String getEmailRecipients() { assert(this.type==Type.EMAIL) : "Cannot get email recipients from action of type: " + type; return this.emailRecipients; } /** * Gets this action email subject. * @return {@link #emailSubject} */ public String getEmailSubject() { assert(this.type==Type.EMAIL) : "Cannot get email subject from action of type: " + type; return this.emailSubject; } /** @return {@link #endDate} */ public long getEndDate() { return this.endDate; } /** @param date new value for {@link #startDate} */ public void setStartDate(long date) { this.startDate = date; } /** @param date new value for {@link #endDate} */ public void setEndDate(long date) { this.endDate = date; } /** @param group new value for {@link #group} */ public void setGroup(Group group) { assert(hasGroup()) : "Cannot set group from action of type: " + type; this.group = group; } /** * Check if this action can have a group attached to it. * @return <code>true</code> if a group may be attached to an action of this type; <code>false</code> otherwise. */ private boolean hasGroup() { return this.type==Type.JOIN || this.type==Type.LEAVE || this.type==Type.FORWARD || this.type==Type.EXTERNAL_CMD; } /** * Sets the email recipients of this instance of KeywordAction. * @param recipients new value for {@link #emailRecipients} */ public void setEmailRecipients(String recipients) { assert(this.type==Type.EMAIL) : "Cannot set email recipients from action of type: " + type; this.emailRecipients = recipients; } /** @param subject new value for {@link #emailSubject} of {@link #EMAIL} */ public void setEmailSubject(String subject) { assert(this.type==Type.EMAIL) : "Cannot set email subject from action of type: " + type; this.emailSubject = subject; } /** @param emailAccount new value for {@link #emailAccount} of {@link #EMAIL} */ public void setEmailAccount(EmailAccount emailAccount) { assert(this.type==Type.EMAIL) : "Cannot get group from action of type: " + type; this.emailAccount = emailAccount; } /** @param text new value for {@link #commandString} of a {@link #FORWARD} */ public void setForwardText(String text) { assert(this.type==Type.FORWARD) : "Cannot get forward text from action of type: " + type; this.commandString = text; } /** @param commandText new value for {@link #commandString} */ public void setCommandText(String commandText) { this.commandString = commandText; } /** @param commandLine new value for {@link #externalCommand} */ public void setCommandLine(String commandLine) { this.externalCommand = commandLine; } /** @return how many times this action was executed */ public int getCounter() { return this.counter; } /** * Increments how many times this action was executed. * This method should ONLY be called from the {@link KeywordActionDao}, due to consistency * issues. Avoid calling {@link KeywordActionDao#update(KeywordAction)} after incrementing * for related reasons. */ public void incrementCounter() { ++counter; } /** @return the group related to this keyword action */ public Group getGroup() { assert(hasGroup()) : "Cannot get group from action of type: " + type; return this.group; } /** @return the email account related to this keyword action */ public EmailAccount getEmailAccount() { assert(this.type==Type.EMAIL) : "Cannot get group from action of type: " + type; return this.emailAccount; } /** @return {@link #unformattedReplyText} the reply text for this action (if it is of TYPE_REPLY or TYPE_EMAIL) */ public String getUnformattedReplyText() { assert(this.type==Type.REPLY || this.type==Type.EMAIL) : "Cannot get reply text from action of type: " + type; return this.commandString; } /** * If this action is of TYPE_REPLY or TYPE_EMAIL, sets the reply text. * @param replyText new value for {@link #replyText} */ public void setReplyText(String replyText) { assert(this.type==Type.REPLY || this.type==Type.EMAIL) : "Cannot set reply text from action of type: " + type; this.commandString = replyText; } /** @return the forward text for this action (if it is of TYPE_FORWARD). */ public String getUnformattedForwardText() { assert(this.type==Type.FORWARD) : "Cannot get forward text from action of type: " + type; return this.commandString; } /** @return the command text for this action (if it is of TYPE_EXTERNAL_CMD). */ public String getUnformattedCommandText() { assert(this.type==Type.EXTERNAL_CMD) : "Cannot get command text from type: " + type; return this.commandString; } /** @return the command line for this action (if it is of TYPE_EXTERNAL_CMD). */ public String getUnformattedCommand() { assert(this.type==Type.EXTERNAL_CMD) : "Cannot get command from type: " + type; return this.externalCommand; } /** * Gets the keyword that this action is associated with. * @return {@link #keyword} */ public Keyword getKeyword() { return this.keyword; } //> STATIC HELPER METHODS public static class KeywordUtils { public static final String personaliseMessage(Contact contact, String messageText) { // Replace any user-defined variables they might have been included return messageText.replace(CsvUtils.MARKER_CONTACT_NAME, contact.getName()); } /** * Creates the formatted reply text for this action from an incoming message. * * If this action is not of TYPE_REPLY, throws an IllegalStateException. * @param senderMsisdn * @param incomingMessageText * @return * TODO remove incomingKeyword parameter */ public static final String getReplyText(KeywordAction action, Contact sender, String senderMsisdn, String incomingMessageText, String incomingKeyword) { String senderDisplayName; if(sender != null) senderDisplayName = sender.getDisplayName(); else senderDisplayName = senderMsisdn; return formatText(action.getUnformattedReplyText(), false, action, senderMsisdn, senderDisplayName, incomingMessageText); } /** * Creates the formatted reply text for this action from an incoming message. * * If this action is not of TYPE_REPLY, throws an IllegalStateException. * @param senderMsisdn * @param incomingMessageText * @return */ public static final String getEmailSubject(KeywordAction action, Contact sender, String senderMsisdn, String incomingMessageText, String incomingKeyword) throws IllegalStateException { String senderDisplayName; if(sender != null) senderDisplayName = sender.getDisplayName(); else senderDisplayName = senderMsisdn; return formatText(action.getEmailSubject(), false, action, senderMsisdn, senderDisplayName, incomingMessageText); } /** * Creates the formatted external command or email for this action from an incoming message. * * If this action is not of TYPE_EXTERNAL_CMD, throws an IllegalStateException. * @param action * @param sender * @param senderMsisdn * @param incomingMessageText * @return */ public static final String getExternalCommand(KeywordAction action, Contact sender, String senderMsisdn, String incomingMessageText) { String senderDisplayName; if (sender != null) senderDisplayName = sender.getDisplayName(); else senderDisplayName = senderMsisdn; return formatText(action.getUnformattedCommand(), true, action, senderMsisdn, senderDisplayName, incomingMessageText); } /** * Creates the formatted external command reply for this action from an incoming message. * * If this action is not of TYPE_EXTERNAL_CMD, throws an IllegalStateException. * @param action * @param response * @return * @throws IllegalStateException */ public static final String getExternalCommandReplyMessage(KeywordAction action, String response) { return KeywordUtils.getFormattedCommandReply(action, response); } /** * Creates the formatted forward text for this action from an incoming message. * * If this action is not of TYPE_FORWARD an IllegalStateException should be thrown. * @param sender The Contact object representing the sender of this message, or NULL if this msisdn is not associated with a Contact. * @param senderMsisdn The msisdn from which the message * @param incomingMessageText The text of the received message. * @return */ public static final String getForwardText(KeywordAction action, Contact sender, String senderMsisdn, String incomingMessageText) { String senderDisplayName; if(sender != null) senderDisplayName = sender.getDisplayName(); else senderDisplayName = senderMsisdn; return formatText(action.getUnformattedForwardText(), false, action, senderMsisdn, senderDisplayName, incomingMessageText); } /** * Formats a message, inserting particular variables where their presence has been requested by placeholders. */ protected static final String getFormattedCommandReply(KeywordAction action, String response) { String command = action.getUnformattedCommandText(); command = command.replace(CsvUtils.MARKER_COMMAND_RESPONSE, response); return command; } /** * Remove the keyword from the start of a received message. If called on text that does not start with the * keyword, the text will be returned unchanged. * @param messageText * @param keywordString * @return */ static final String removeKeyword(String messageText, String keywordString) { String keywordInMessage = extractKeyword(messageText, keywordString); if(keywordInMessage == null) { return messageText; } else { if(messageText.length() == keywordString.length()) return ""; else return messageText.substring(keywordString.length() + 1); } } /** * Extracts the keyword from the start of a message, and returns the keyword as it appeared in * the message. * @param messageText * @param keywordString * @return the keyword string <em>as it appears at the start of messageText</em> or <code>null</code> if messageText does not start with keyword. */ static final String extractKeyword(String messageText, String keywordString) { int keywordLength = keywordString.length(); if(messageText.length() == keywordLength) { if(messageText.equalsIgnoreCase(keywordString)) { return messageText; } } else if(messageText.length() > keywordLength) { String keywordInMessage = messageText.substring(0, keywordLength); if(keywordInMessage.equalsIgnoreCase(keywordString)) { char charAfterKeyword = messageText.charAt(keywordLength); if(charAfterKeyword == ' ' ||charAfterKeyword == '\n' ||charAfterKeyword == '\r') { return keywordInMessage; } } } return null; } static String formatText(String unformattedText, boolean urlEncode, KeywordAction action, String senderMsisdn, String senderDisplayName, String incomingMessageText) { String keywordString = action.getKeyword().getKeyword(); String keywordInMessage = extractKeyword(incomingMessageText, keywordString); String messageWithoutKeyword = removeKeyword(incomingMessageText, keywordString); if(urlEncode) { senderMsisdn = FrontlineUtils.urlEncode(senderMsisdn); keywordInMessage = FrontlineUtils.urlEncode(keywordInMessage); senderDisplayName = FrontlineUtils.urlEncode(senderDisplayName); messageWithoutKeyword = FrontlineUtils.urlEncode(messageWithoutKeyword); } // TODO perhaps all variables should be subbed? return MessageFormatter.formatMessage(unformattedText, MessageFormatter.MARKER_SENDER_NUMBER, /*->*/ senderMsisdn, MessageFormatter.MARKER_KEYWORD_KEY, /*->*/ keywordInMessage, MessageFormatter.MARKER_SENDER_NAME, /*->*/ senderDisplayName, // N.B. message content should always be substituted last to prevent injection attacks MessageFormatter.MARKER_MESSAGE_CONTENT, /*->*/ messageWithoutKeyword ); } } //> STATIC FACTORY METHODS /** * Creates a keyword action to automatically REPLY to messages. * @param keyword The keyword that triggers this action * @param replyText The text to reply with when this action is triggered * @param start * @param end * @return a new instance of KeywordAction */ public static KeywordAction createReplyAction(Keyword keyword, String replyText, long start, long end) { KeywordAction action = new KeywordAction(Type.REPLY, keyword); action.setReplyText(replyText); action.setStartDate(start); action.setEndDate(end); return action; } /** * Creates a keyword action to automatically send Email. * @param keyword The keyword that triggers this action * @param replyText The text to reply with when this action is triggered * @param account * @param to * @param subject * @param start * @param end * @return a new instance of KeywordAction */ public static KeywordAction createEmailAction(Keyword keyword, String replyText, EmailAccount account, String to, String subject,long start, long end) { KeywordAction action = new KeywordAction(Type.EMAIL, keyword); action.setReplyText(replyText); action.setEmailAccount(account); action.setEmailRecipients(to); action.setEmailSubject(subject); action.setStartDate(start); action.setEndDate(end); return action; } /** * Creates a keyword action to automatically execute a external command. * @param keyword The keyword that triggers this action * @param commandLine The command to be executed * @param commandType * <li> HTTP request * <li> Command line execution. * @param responseType * <li> Plain Text * <li> Command list * <li> No response * @param responseActionType * <li> Forward to Group Only * <li> Auto Reply Only * <li> Both * <li> Neither * @param commandMsg The message to be sent with response. * @param toFwd The group to be forwarded. * @param start * @param end * @return a new instance of KeywordAction */ public static KeywordAction createExternalCommandAction(Keyword keyword, String commandLine, ExternalCommandType commandType, ExternalCommandResponseType responseType, ExternalCommandResponseActionType responseActionType, String commandMsg, Group toFwd, long start, long end) { KeywordAction action = new KeywordAction(Type.EXTERNAL_CMD, keyword); action.setCommandLine(commandLine); action.setExternalCommandType(commandType); action.setExternalCommandResponseType(responseType); action.setCommandResponseActionType(responseActionType); action.setCommandText(commandMsg); action.setGroup(toFwd); action.setStartDate(start); action.setEndDate(end); return action; } /** * Creates a keyword action to automatically add a contact to a group. * @param keyword The keyword that triggers this action * @param group The group to add the sender to when this action is triggered * @return a new instance of KeywordAction */ public static KeywordAction createGroupJoinAction(Keyword keyword, Group group, long start, long end) { KeywordAction action = new KeywordAction(Type.JOIN, keyword); action.setGroup(group); action.setStartDate(start); action.setEndDate(end); return action; } /** * Creates a keyword action to automatically remove a contact from a group. * @param keyword The keyword that triggers this action * @param group The group to remove the sender from when this action is triggered * @return a new instance of KeywordAction */ public static KeywordAction createGroupLeaveAction(Keyword keyword, Group group, long start, long end) { KeywordAction action = new KeywordAction(Type.LEAVE, keyword); action.setGroup(group); action.setStartDate(start); action.setEndDate(end); return action; } /** * Creates a keyword action to automatically forward a message to a group * @param keyword The keyword that triggers the action * @param group The group to forward the message onto * @param forwardText The message text to forward on to the group * @return a new instance of KeywordAction */ public static KeywordAction createForwardAction(Keyword keyword, Group group, String forwardText, long start, long end) { KeywordAction action = new KeywordAction(Type.FORWARD, keyword); action.setGroup(group); action.setForwardText(forwardText); action.setStartDate(start); action.setEndDate(end); return action; } //> GENERATED CODE /** @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + commandInteger; result = prime * result + ((commandString == null) ? 0 : commandString.hashCode()); result = prime * result + ((emailRecipients == null) ? 0 : emailRecipients.hashCode()); result = prime * result + ((emailSubject == null) ? 0 : emailSubject.hashCode()); result = prime * result + (int) (endDate ^ (endDate >>> 32)); result = prime * result + ((externalCommand == null) ? 0 : externalCommand.hashCode()); result = prime * result + ((externalCommandResponseActionType == null) ? 0 : externalCommandResponseActionType.hashCode()); result = prime * result + ((externalCommandResponseType == null) ? 0 : externalCommandResponseType.hashCode()); result = prime * result + ((externalCommandType == null) ? 0 : externalCommandType.hashCode()); result = prime * result + ((keyword == null) ? 0 : keyword.hashCode()); result = prime * result + (int) (startDate ^ (startDate >>> 32)); result = prime * result + type.hashCode(); return result; } /** @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; KeywordAction other = (KeywordAction) obj; if (commandInteger != other.commandInteger) return false; if (commandString == null) { if (other.commandString != null) return false; } else if (!commandString.equals(other.commandString)) return false; if (emailRecipients == null) { if (other.emailRecipients != null) return false; } else if (!emailRecipients.equals(other.emailRecipients)) return false; if (emailSubject == null) { if (other.emailSubject != null) return false; } else if (!emailSubject.equals(other.emailSubject)) return false; if (endDate != other.endDate) return false; if (externalCommand == null) { if (other.externalCommand != null) return false; } else if (!externalCommand.equals(other.externalCommand)) return false; if (externalCommandResponseActionType != other.externalCommandResponseActionType) return false; if (externalCommandResponseType != other.externalCommandResponseType) return false; if (externalCommandType != other.externalCommandType) return false; if (keyword == null) { if (other.keyword != null) return false; } else if (!keyword.equals(other.keyword)) return false; if (startDate != other.startDate) return false; if (type != other.type) return false; return true; } }