package esmska.data; import esmska.data.event.ValuedEventSupport; import esmska.data.event.ValuedListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Timer; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; /** Class representing queue of SMS * * @author ripper */ public class Queue { public static enum Events { /** New sms added. * Event value: added sms. */ SMS_ADDED, /** Existing sms removed. * Event value: removed sms. */ SMS_REMOVED, /** All sms's removed. * Event value: null. */ QUEUE_CLEARED, /** The postition of sms in the queue changed. * Event value: moved sms. */ SMS_POSITION_CHANGED, /** Existing sms is now ready for sending. * Event value: ready sms. */ NEW_SMS_READY, /** Existing sms is now being sent. * Event value: sms being sent. */ SENDING_SMS, /** Existing sms has been sent. * Event value: sent sms. */ SMS_SENT, /** Existing sms failed to be sent. * Event value: failed sms. */ SMS_SENDING_FAILED, /** Queue has been paused. * Event value: null. */ QUEUE_PAUSED, /** Queue has been resumed. * Event value: null. */ QUEUE_RESUMED } /** Internal tick interval of the queue in milliseconds. After each tick the current delay of all messages is recomputed. */ public static final int TIMER_TICK = 250; /** shared instance */ private static final Queue instance = new Queue(); private static final Logger logger = Logger.getLogger(Queue.class.getName()); private static final History history = History.getInstance(); /** map of [gateway name;SMS[*]] */ private final SortedMap<String,List<SMS>> queue = Collections.synchronizedSortedMap(new TreeMap<String,List<SMS>>()); private final AtomicBoolean paused = new AtomicBoolean(); //map of <gateway name, current delay in seconds> private final Map<String, Long> gatewayDelay = Collections.synchronizedMap(new HashMap<String, Long>()); //every second check the queue private final Timer timer = new Timer(TIMER_TICK, new TimerListener()); // <editor-fold defaultstate="collapsed" desc="ValuedEvent support"> private ValuedEventSupport<Events, SMS> valuedSupport = new ValuedEventSupport<Events, SMS>(this); public void addValuedListener(ValuedListener<Events, SMS> valuedListener) { valuedSupport.addValuedListener(valuedListener); } public void removeValuedListener(ValuedListener<Events, SMS> valuedListener) { valuedSupport.removeValuedListener(valuedListener); } // </editor-fold> /** Disabled constructor */ private Queue() { } /** Get shared instance */ public static Queue getInstance() { return instance; } /** Get all SMS in the queue. * This is a shortcut for getAll(null). */ public List<SMS> getAll() { return getAll(null); } /** Get all SMS in the queue for specified gateway. * The queue is always sorted by the gateway name, the messages are not sorted. * @param gatewayName name of the gateway. May be null for any gateway. * @return unmodifiable list of SMS for specified gateway. */ public List<SMS> getAll(String gatewayName) { List<SMS> list = new ArrayList<SMS>(); synchronized(queue) { if (gatewayName == null) { //take all messages for (Collection<SMS> col : queue.values()) { list.addAll(col); } } else if (queue.containsKey(gatewayName)) { //take messages of that gateway list = queue.get(gatewayName); } else { //gateway not found, therefore empty list } } return Collections.unmodifiableList(list); } /** Get a collection of SMS with particular status. * This a shortcut for getAllWithStatus(status, null). */ public List<SMS> getAllWithStatus(SMS.Status status) { return getAllWithStatus(status, null); } /** Get a collection of SMS with particular status and gateway. * The queue is always sorted by the gateway name, the messages are not sorted. * @param status SMS status, not null * @param gatewayName name of the gateway of the SMS, may be null for any gateway * @return unmodifiable list of SMS with that status in the queue */ public List<SMS> getAllWithStatus(SMS.Status status, String gatewayName) { Validate.notNull(status, "status is null"); List<SMS> list = new ArrayList<SMS>(); synchronized(queue) { if (gatewayName == null) { //take every gateway for (Collection<SMS> col : queue.values()) { for (SMS sms : col) { if (sms.getStatus() == status) { list.add(sms); } } } } else if (queue.containsKey(gatewayName)) { //only one gateway for (SMS sms : queue.get(gatewayName)) { if (sms.getStatus() == status) { list.add(sms); } } } else { //gateway not found, therefore empty list } } return Collections.unmodifiableList(list); } /** Get all SMS (fragments) in the queue with a specified ID. * The queue is always sorted by the gateway name, the messages are not sorted. * @param id message ID. Must not be empty. * @return unmodifiable list of SMS with the specified ID. */ public List<SMS> getAllWithId(String id) { Validate.notEmpty(id); List<SMS> list = new ArrayList<SMS>(); synchronized(queue) { for (Collection<SMS> col : queue.values()) { for (SMS sms : col) { if (StringUtils.equals(sms.getId(), id)) { list.add(sms); } } } } return Collections.unmodifiableList(list); } /** Add new SMS to the queue. May not be null. * @return See {@link Collection#add}. */ public boolean add(SMS sms) { Validate.notNull(sms); sms.setStatus(SMS.Status.WAITING); String gateway = sms.getGateway(); boolean added = false; synchronized(queue) { if (queue.containsKey(gateway)) { //this gateway was already in the queue if (!queue.get(gateway).contains(sms)) { //sms not already present added = queue.get(gateway).add(sms); } } else { //new gateway List<SMS> list = new ArrayList<SMS>(); list.add(sms); queue.put(gateway, list); added = true; } } if (added) { logger.log(Level.FINE, "Added new SMS to queue: {0}", sms); valuedSupport.fireEventOccured(Events.SMS_ADDED, sms); markIfReady(sms); timer.start(); } return added; } /** Add collection of new SMS to the queue. * @param collection Collection of SMS. May not be null, may not contain null element. * @return See {@link Collection#addAll(java.util.Collection)} */ public boolean addAll(Collection<SMS> collection) { Validate.notNull(collection, "collection is null"); Validate.noNullElements(collection); logger.log(Level.FINE, "Adding {0} new SMS to the queue", collection.size()); boolean added = false; for (SMS sms : collection) { if (add(sms)) { added = true; } } return added; } /** Remove SMS from the queue. If the SMS is not present nothing happens. * @param sms SMS to be removed. Not null. * @return See {@link Collection#remove(java.lang.Object) } */ public boolean remove(SMS sms) { Validate.notNull(sms); String gateway = sms.getGateway(); boolean removed = false; synchronized(queue) { if (queue.containsKey(gateway)) { //only if we have this gateway removed = queue.get(gateway).remove(sms); } if (removed && queue.get(gateway).isEmpty()) { //if there are no more sms from that gateway, delete it from map queue.remove(gateway); gatewayDelay.remove(gateway); } } if (removed) { logger.log(Level.FINE, "Removed SMS from queue: {0}", sms); valuedSupport.fireEventOccured(Events.SMS_REMOVED, sms); markAllIfReady(); } return removed; } /** Remove all SMS with a specified ID from the queue. * @param id SMS to be removed. Not null. */ public void remove(String id) { Validate.notEmpty(id); synchronized(queue) { for (SMS sms : getAllWithId(id)) { remove(sms); } } } /** Remove all SMS from the queue. */ public void clear() { logger.fine("Clearing the queue."); synchronized(queue) { queue.clear(); gatewayDelay.clear(); } valuedSupport.fireEventOccured(Events.QUEUE_CLEARED, null); } /** Checks whether the SMS is in the queue. * @param sms SMS, not null * @return See {@link Collection#contains(java.lang.Object) } */ public boolean contains(SMS sms) { Validate.notNull(sms); String gateway = sms.getGateway(); synchronized(queue) { if (queue.containsKey(gateway)) { return queue.get(gateway).contains(sms); } else { //nowhere in the queue return false; } } } /** Get the number of SMS in the queue */ public int size() { int size = 0; synchronized(queue) { for (Collection<SMS> col : queue.values()) { size += col.size(); } } return size; } /** Check if the queue is empty */ public boolean isEmpty() { synchronized(queue) { for (Collection<SMS> col : queue.values()) { if (col.size() > 0) { return false; } } } return true; } /** Whether queue is currently paused */ public boolean isPaused() { return paused.get(); } /** Sets whether queue is currently paused */ public void setPaused(boolean paused) { this.paused.set(paused); if (paused) { logger.fine("Queue is now paused"); valuedSupport.fireEventOccured(Events.QUEUE_PAUSED, null); } else { logger.fine("Queue is now resumed"); valuedSupport.fireEventOccured(Events.QUEUE_RESUMED, null); } } /** Move SMS in the queue to another position. * Queue is always sorted by gateway, therefore SMS may be moved only within * section of its gateway. * @param sms sms to be moved, not null * @param positionDelta direction and amount of movement. Positive number moves * to the back of the queue, negative number moves to the front of the queue. * The number corresponds to the number of positions to change. If the number * is larger than current queue dimensions, the element will simply stop as the * first or as the last element. */ public void movePosition(SMS sms, int positionDelta) { Validate.notNull(sms, "sms is null"); String gateway = sms.getGateway(); synchronized(queue) { if (positionDelta == 0 || !queue.containsKey(gateway) || !queue.get(gateway).contains(sms)) { //nothing to move return; } logger.log(Level.FINE, "Moving sms {0} with delta {1}", new Object[]{sms, positionDelta}); List<SMS> list = queue.get(gateway); int currentPos = list.indexOf(sms); int newPos = currentPos + positionDelta; //check the boundaries of the queue if (newPos < 0) { newPos = 0; } if (newPos > list.size() - 1) { newPos = list.size() - 1; } if (currentPos == newPos) { return; } list.remove(currentPos); list.add(newPos, sms); } valuedSupport.fireEventOccured(Events.SMS_POSITION_CHANGED, sms); // we have moved an sms in the queue, that may have changed which sms // should be waiting and which should be ready now // mark all ready sms from this gateway as waiting and then compute // a new ready one List<SMS> messages = getAll(sms.getGateway()); for (SMS message : messages) { if (message.getStatus() == SMS.Status.READY) { message.setStatus(SMS.Status.WAITING); } } markAllIfReady(); } /** Return current delay for specified gateway. * @param gatewayName name of the gateway. May be null. * @return number of milliseconds next message from the gateway must wait. * If no such gateway found, return 0. */ public long getGatewayDelay(String gatewayName) { Long del = gatewayDelay.get(gatewayName); if (del != null) { return del; } Gateway gateway = Gateways.getInstance().get(gatewayName); long delay = 0; if (gateway == null) { //unknown gateway delay = 0; } else if (gateway.getDelayBetweenMessages() <= 0) { //gateway without delay delay = 0; } else { //search in history History.Record record = history.findLastRecord(gatewayName); if (record == null) { //no previous record delay = 0; } else { //compute the delay //FIXME: does not take various daylight saving time etc into account //A more complex library (eg. Joda Time) is needed to calculate true time differences. long difference = (new Date().getTime() - record.getDate().getTime()); //in milliseconds if (difference < 0) { //last message was sent in the future //that's clearly wrong, the user probably messed up system time //the best thing we can do is just try to send the message without a delay delay = 0; } else { //let's compute the real remaining delay delay = Math.max(gateway.getDelayBetweenMessages() * 1000 - difference, 0); } } } gatewayDelay.put(gatewayName, delay); return delay; } /** Return current delay for specified sms. * The delay is taking into account all previous messages from the same gateway * which are waiting to be sent. If sms is not found in the queue, it is * considered to be at the end of the queue. * @param sms sms, not null * @return number of milliseconds a message must wait */ public long getSMSDelay(SMS sms) { Validate.notNull(sms); String gatewayName = sms.getGateway(); long delay = getGatewayDelay(gatewayName); List<SMS> list = queue.get(gatewayName); if (list == null) { //no such gateway in the queue return delay; //therefore gateway delay is sms delay } int index = list.indexOf(sms); Gateway gateway = Gateways.getInstance().get(gatewayName); int opDelay = gateway != null ? gateway.getDelayBetweenMessages() * 1000 : 0; if (index >= 0) { //in the queue delay = delay + (index * opDelay); } else { //not in the queue, therefore after all sms's in the queue delay = delay + (list.size() * opDelay); } return delay; } /** Mark the SMS as successfully sent. * @param sms sent SMS, not null */ public void setSMSSent(SMS sms) { Validate.notNull(sms); logger.log(Level.FINE, "Marking sms as successfully sent: {0}", sms); sms.setStatus(SMS.Status.SENT); valuedSupport.fireEventOccured(Events.SMS_SENT, sms); updateGatewayDelay(sms.getGateway()); remove(sms); //remove it from the queue timer.start(); } /** Mark SMS as currently being sent. * @param sms SMS that is currently being sent, not null */ public void setSMSSending(SMS sms) { Validate.notNull(sms); logger.log(Level.FINE, "Marking SMS as currently being sent: {0}", sms); sms.setStatus(SMS.Status.SENDING); valuedSupport.fireEventOccured(Events.SENDING_SMS, sms); } /** Mark SMS as failed during sending. Pauses the queue. * @param sms SMS that has failed, not null */ public void setSMSFailed(SMS sms) { Validate.notNull(sms); logger.log(Level.FINE, "Marking SMS as failed during sending: {0}", sms); //pause the queue when sms fail setPaused(true); //set sms to be waiting again sms.setStatus(SMS.Status.WAITING); valuedSupport.fireEventOccured(Events.SMS_SENDING_FAILED, sms); updateGatewayDelay(sms.getGateway()); timer.start(); markIfReady(sms); } /** * Extract all message fragments (according to ID) from the queue and join them * into a full message. * @param id id of all the message fragments; not empty * @param remove whether to remove all the fragments from the queue in the process * @return sms the whole message with concatenated fragments' text; or null if * no such ID was present */ public SMS extractSMS(String id, boolean remove) { Validate.notEmpty(id); SMS sms; synchronized(queue) { List<SMS> fragments = getAllWithId(id); if (fragments.isEmpty()) { return null; } SMS head = fragments.get(0); sms = new SMS(head.getNumber(), "", head.getGateway(), head.getName(), null); for (SMS fragment : fragments) { sms.setText(sms.getText() + fragment.getText()); } if (remove) { remove(id); } } return sms; } /** Check if sms is ready and set status if it is */ private void markIfReady(SMS sms) { Validate.notNull(sms); long delay = getSMSDelay(sms); if (sms.getStatus() == SMS.Status.WAITING && delay <= 0) { logger.log(Level.FINER, "Marking SMS as ready: {0}", sms); sms.setStatus(SMS.Status.READY); valuedSupport.fireEventOccured(Events.NEW_SMS_READY, sms); } } /** Check all sms for that which are ready and set their status */ private void markAllIfReady() { ArrayList<SMS> ready = new ArrayList<SMS>(); synchronized(queue) { for (String gateway : queue.keySet()) { long delay = getGatewayDelay(gateway); if (delay > 0) { //any new sms can't be ready continue; } for (SMS sms : queue.get(gateway)) { long smsDelay = getSMSDelay(sms); if (smsDelay > 0) { break; } if (sms.getStatus() == SMS.Status.WAITING) { logger.log(Level.FINER, "Marking SMS as ready: {0}", sms); sms.setStatus(SMS.Status.READY); ready.add(sms); } } } } for (SMS sms : ready) { valuedSupport.fireEventOccured(Events.NEW_SMS_READY, sms); } } /** Remove gateway from delay cache and compute its delay again */ private void updateGatewayDelay(String gatewayName) { Validate.notEmpty(gatewayName); gatewayDelay.remove(gatewayName); getGatewayDelay(gatewayName); } /** Update the information about current message delays */ private class TimerListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { boolean timerNeeded = false; boolean checkSMSReady = false; synchronized(gatewayDelay) { //for every delay substract one second for (Iterator<Entry<String, Long>> iter = gatewayDelay.entrySet().iterator(); iter.hasNext(); ) { Entry<String, Long> delay = iter.next(); if (!queue.containsKey(delay.getKey())) { //if there is some gateway which is no longer in the queue, we don't need it anymore iter.remove(); continue; } if (delay.getValue() > 0) { long newDelay = Math.max(delay.getValue() - TIMER_TICK, 0); delay.setValue(newDelay); timerNeeded = true; //stil counting down for someone if (delay.getValue() <= 0) { //new gateway delay just dropped to 0 checkSMSReady = true; } } } } if (!timerNeeded) { //when everything is on 0, no need for timer to run, let's stop it timer.stop(); } if (checkSMSReady) { //we may have new ready sms, check it markAllIfReady(); } } } }