package esmska.data; import esmska.utils.MiscUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; /** Class for preparing attributes of sms (single or multiple) * * @author ripper */ public class Envelope { private static final Config config = Config.getInstance(); private static final Logger logger = Logger.getLogger(Envelope.class.getName()); private static final Gateways gateways = Gateways.getInstance(); private String text = ""; private Set<Contact> contacts = new HashSet<Contact>(); private Gateway gateway; //current reference gateway private String senderName; //current reference signature user name // how much (in percents) you can cut down the message in order to keep word boundaries private static final double wordCutSpread = 0.1; // <editor-fold defaultstate="collapsed" desc="PropertyChange support"> private PropertyChangeSupport changeSupport = new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } // </editor-fold> /** get text of sms */ public String getText() { return text; } /** set text of sms */ public void setText(String text) { text = StringUtils.defaultString(text, ""); String oldText = this.text; if (config.isRemoveAccents()) { text = MiscUtils.removeAccents(text); } this.text = text; changeSupport.firePropertyChange("text", oldText, text); } /** get all recipients */ public Set<Contact> getContacts() { return Collections.unmodifiableSet(contacts); } /** set all recipients */ public void setContacts(Set<Contact> contacts) { Set<Contact> oldContacts = this.contacts; this.contacts = contacts; this.gateway = computeWorstGateway(); this.senderName = extractSenderName(); changeSupport.firePropertyChange("contacts", oldContacts, contacts); } /* Get current reference sender name used for display and computational purposes */ public String getSenderName() { return senderName; } /* Get the gateway allowing shortest messages from all contacts */ private Gateway computeWorstGateway() { Gateway worstGateway = null; int worstLength = Integer.MAX_VALUE; for (Contact c : contacts) { Gateway gw = gateways.get(c.getGateway()); if (gw == null) { continue; } if (gw.getSMSLength() < worstLength) { worstLength = gw.getSMSLength(); worstGateway = gw; } } return worstGateway; } /* Extract sender name from current gateway */ private String extractSenderName() { if (gateway == null) { return ""; } else { return gateway.getSenderName(); } } /** get maximum length of sendable message * @param customText a message text to measure. Because of cutting message * by word boundaries, the maximum length varies depending on how the input * text is structured. */ public int getMaxTextLength(String customText) { if (gateway == null) { return Gateway.maxMessageLength; } int maxLength = gateway.getMaxChars() * gateway.getMaxParts(); //some characters were wasted by splitting text by word boundaries int lostChars = computeLostCharsByWordCutting(customText, gateway.getMaxChars()); maxLength -= lostChars; return maxLength; } /** get maximum length of sendable message. Uses envelope text to measure it. */ public int getMaxTextLength() { return getMaxTextLength(text); } /** get length of one sms */ public int getSMSLength() { if (gateway != null) { return gateway.getSMSLength(); } else { return Gateway.maxMessageLength; } } /** * Get number of sms pieces cutting from msgText depending on max SMS length * limit. The pieces will be split by word boundaries, unless it would take * away more than 10% of the text - in that case it will be split * by characters (splitting the word). * * @param msgText full message text * @param limit max SMS length limit * @return resulting number of sms cutting from msgText; the number is 0 * if @param limit is zero or negative */ public int getSMSCount(String msgText, int limit) { return getIndicesOfCuts(msgText, limit).size(); } /** generate list of sms's to send */ public ArrayList<SMS> generate() { ArrayList<SMS> list = new ArrayList<SMS>(); for (Contact c : contacts) { Gateway gateway = gateways.get(c.getGateway()); int limit = (gateway != null ? gateway.getMaxChars() : Gateway.maxMessageLength); String msgText = text; // fix user signature in multisend mode if (contacts.size() > 1 && gateway != null) { // in this mode the user can't control the sender signature // per recipient, so we use the default one for each recipient msgText = StringUtils.removeStart(text, senderName); if (!StringUtils.startsWith(msgText, gateway.getSenderName())) { msgText = gateway.getSenderName() + msgText; } } String messageId = SMS.generateID(); // cut out the messages ArrayList<String> messages = cutOutMessages(msgText, limit); for (String cutText : messages) { SMS sms = new SMS(c.getNumber(), cutText, c.getGateway(), c.getName(), messageId); list.add(sms); } } logger.log(Level.FINE, "Envelope specified for {0} contact(s) generated {1} SMS(s)", new Object[]{contacts.size(), list.size()}); return list; } /** Take a full message text and cut it out into pieces depending on max SMS * length limit, while trying to keep word boundaries. * * @param msgText full message text * @param limit max message/piece length * @return a list of cut out pieces of msgText */ private ArrayList<String> cutOutMessages(String msgText, int limit) { ArrayList<String> messages = new ArrayList<String>(); int from = 0; //start index of cut, inclusive //to - ending index, exclusive for (Integer to : getIndicesOfCuts(msgText, limit)) { String cutText = msgText.substring(from, to); messages.add(cutText); from = to; } return messages; } /** Cut text into pieces while trying to keep word boundaries. * * @param msgText full message text * @param limit max single piece length * @return list containing indices of cuts (in ascending order). The list * is empty if @param limit is zero or negative. */ public ArrayList<Integer> getIndicesOfCuts(String msgText, int limit) { ArrayList<Integer> listOfCuts = new ArrayList<Integer>(); if (limit <= 0) { return listOfCuts; } int cutLength; for (int from = 0; from < msgText.length(); from += cutLength) { int indexOfCut = findIndexOfCut(msgText, from, limit); cutLength = indexOfCut - from; //length of currently cut out text if (cutLength <= 0) { // we would be stuck in a loop throw new IllegalStateException("Error while message cutting"); } listOfCuts.add(indexOfCut); //add index into list } return listOfCuts; } /** Find the first index of cut in a text, which would try to keep word * boundaries. * If the word boundaries can't be kept (controlled by wordCutSpread), * standard per-character cutting will be done instead. * * @param msgText full message text * @param start the beginning index, inclusive * @param limit max message/piece length * @return the ending index of the cut, exclusive */ private int findIndexOfCut(String msgText, int start, int limit) { //initial index of cut, not aware of word boundaries int indexOfCut = start + limit; if (indexOfCut >= msgText.length()) { //we've already exceeding text length, no word searching needed return msgText.length(); } if (Character.isWhitespace(msgText.charAt(indexOfCut)) || Character.isWhitespace(msgText.charAt(indexOfCut - 1))) { //we've hit the ideal spot between words, no more work needed return indexOfCut; } //compute the index we can't cross when searching for word boundaries int minIndexOfCut = start + ((int)(limit * (1 - wordCutSpread))); while (indexOfCut > 0 && !Character.isWhitespace(msgText.charAt(indexOfCut - 1))) { //traverse left until we find a whitespace indexOfCut--; if (indexOfCut < minIndexOfCut) { //whitespace not found and we've already crossed the allowed line //return the original cut return (start + limit); } } return indexOfCut; } /** * If exists penultimate index of cut msgText to SMS pieces, find it, * other return 0; * * @param msgText full message text * @param limit max length of SMS * @return penultimate index of cut or 0 */ public int getPenultimateIndexOfCut(String msgText, int limit) { ArrayList<Integer> indicesOfCuts = getIndicesOfCuts(msgText, limit); if (indicesOfCuts.size() <= 1) { return 0; } return (indicesOfCuts.get(indicesOfCuts.size() - 2)); } /** * This function calculates the number of white spaces resulting by splitting * of msgText to the pieces by word boundaries. * * @param msgText full message text * @param limit max length of SMS * @return the number of white spaces */ private int computeLostCharsByWordCutting(String msgText, int limit) { int lostChars = ((getSMSCount(msgText, limit) - 1) * limit) - getPenultimateIndexOfCut(msgText, limit); if (lostChars <= 0) { return 0; } return lostChars; } }