/*
* @copyright 2012 Philip Warner
* @license GNU General Public License
*
* This file is part of Book Catalogue.
*
* Book Catalogue is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Book Catalogue 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue.messaging;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.concurrent.LinkedBlockingQueue;
import android.os.Handler;
import com.eleybourn.bookcatalogue.utils.Logger;
/**
* Switchboard class for disconnecting listener instances from task instances. Maintains
* separate lists and each 'sender' queue maintains a last-message for re-transmission
* when a listener instance (re)connects.
*
* Usage:
*
* A sender (typically a background task, thread or thread manager) registers itself and is assigned
* a unique ID. The creator of the sender uses the ID as the key to later retrieval.
*
* The listener must have access to the unique ID and use that to register themselves.
*
* The listener should call addListener, removeListener, reply or getController as necessary.
*
* ENHANCE: Allow fixed sender IDs to ensure uniqueness and/or allow multiple senders for specific IDs
*
* @author pjw
*
* @param <T> The Class of message that this switchboard sends
* @param <U> The Class of controller object made available to listeners. The controller gives access to the sender.
*/
public class MessageSwitch<T,U> {
/** ID counter for unique sender IDs; set > 0 to allow for possible future static senders **/
private static Long mSenderIdCounter = 1024L;
/** List of message sources */
private Hashtable<Long,MessageSender<U>> mSenders = new Hashtable<Long,MessageSender<U>>();
/** List of all messages in the message queue, both messages and replies */
private LinkedBlockingQueue<RoutingSlip> mMessageQueue = new LinkedBlockingQueue<RoutingSlip>();
/** List of message listener queues */
private Hashtable<Long, MessageListeners> mListeners = new Hashtable<Long, MessageListeners>();
/** Handler object for posting to main thread and for testing if running on UI thread */
private static Handler mHandler = new Handler();
/** Interface that must be implemented by any message that will be sent via send() */
public interface Message<T> {
/**
* Method to deliver a message.
*
* @param listener Listener to who message must be delivered
*
* @return true if message should not be delievered to any other listeners or stored for delievery as 'last message'
* should only return true if the message has been handled and would break the app if delivered more than once.
*/
public boolean deliver(T listener);
}
/** Register a new sender and it's controller object; return the unique ID for this sender */
public Long createSender(U controller) {
MessageSenderImpl s = new MessageSenderImpl(controller);
mSenders.put(s.getId(), s);
return s.getId();
}
/**
* Add a listener for the specified sender ID
*
* @param senderId ID of sender to which the listener listens
* @param listener Listener object
* @param deliverLast If true, send the last message (if any) to this listener
*/
public void addListener(Long senderId, final T listener, boolean deliverLast) {
// Add the listener to the queue, creating queue if necessary
MessageListeners queue;
synchronized(mListeners) {
queue = mListeners.get(senderId);
if (queue == null) {
queue = new MessageListeners();
mListeners.put(senderId, queue);
}
queue.add(listener);
}
// Try to deliver last message if requested
if (deliverLast) {
final MessageRoutingSlip r = queue.getLastMessage();
// If there was a message then send to the passed listener
if (r != null) {
// Do it on the UI thread.
if (mHandler.getLooper().getThread() == Thread.currentThread()) {
r.message.deliver(listener);
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
r.message.deliver(listener);
}
});
}
}
}
}
/**
* Remove the specified listener from the specified queue
*
* @param senderId
* @param l
*/
public void removeListener(Long senderId, T l) {
synchronized(mListeners) {
MessageListeners queue = mListeners.get(senderId);
if (queue != null)
queue.remove(l);
}
}
/**
* Send a message to a queue
*
* @param senderId Queue ID
* @param message Message to send
*/
public void send(long senderId, Message<T> message) {
// Create a routing slip
RoutingSlip m = new MessageRoutingSlip(senderId, message);
// Add to queue
synchronized(mMessageQueue) {
mMessageQueue.add(m);
}
// Process queue
startProcessingMessages();
}
// /**
// * Send a 'reply'
// * @param senderId
// * @param reply
// */
// public void reply(long senderId, Message<U> reply) {
// RoutingSlip m = new ReplyRoutingSlip(senderId, reply);
// synchronized(mMessageQueue) {
// mMessageQueue.add(m);
// }
// startProcessingMessages();
// }
/**
* Get the controller object associated with a sender ID
*
* @param senderId ID of sender
*
* @return Controller object of type 'U'
*/
public U getController(long senderId) {
MessageSender<U> sender = mSenders.get(senderId);
if (sender != null) {
return sender.getController();
} else {
return null;
}
}
/**
* Interface for all messages sent to listeners
*
* @author pjw
*
* @param <U> Arbitrary class that will be responsible for the message
*/
private interface MessageSender<U> {
public long getId();
public void close();
public U getController();
}
/**
* Class used to hold a list of listener objects
*
* @author pjw
*/
private class MessageListeners implements Iterable<T> {
/** Last message sent */
private MessageRoutingSlip mLastMessage = null;
/** Weak refs to all listeners */
private ArrayList<WeakReference<T>> mList = new ArrayList<WeakReference<T>>();
/** Accessor */
public void setLastMessage(MessageRoutingSlip m) {
mLastMessage = m;
}
/** Accessor */
public MessageRoutingSlip getLastMessage() {
return mLastMessage;
}
/** Add a listener to this queue */
public void add(T listener) {
synchronized(mList) {
mList.add(new WeakReference<T>(listener));
}
}
/**
* Remove a listener from this queue; also removes dead references
*
* @param listener Listener to be removed
*/
public void remove(T listener) {
synchronized(mList) {
// List of refs to be removed
ArrayList<WeakReference<T>> toRemove = new ArrayList<WeakReference<T>>();
// Scan the list for matches or dead refs
for(WeakReference<T> w: mList) {
T l = w.get();
if (l == null || l == listener) {
toRemove.add(w);
}
}
// Remove all listeners we found
for(WeakReference<T> w: toRemove)
mList.remove(w);
}
}
/**
* Return an iterator to a *copy* of the valid underlying elements. This means that
* callers can make changes to the underlying list with impunity, and more importantly
* they can iterate over type T, rather than a bunch of weak references to T.
*
* Side-effect: removes invalid listeners
*/
@Override
public Iterator<T> iterator() {
ArrayList<T> list = new ArrayList<T>();
ArrayList<WeakReference<T>> toRemove = null;
synchronized(mList) {
for(WeakReference<T> w: mList) {
T l = w.get();
if (l != null) {
list.add(l);
} else {
if (toRemove == null)
toRemove = new ArrayList<WeakReference<T>>();
toRemove.add(w);
}
}
if (toRemove != null)
for(WeakReference<T> w: toRemove)
mList.remove(w);
}
return list.iterator();
}
//private final ReentrantLock mPopLock = new ReentrantLock();
//ReentrantLock getLock() {
// return mPopLock;
//}
}
/**
* Remove a sender and it's queue
*
* @param s
*/
private void removeSender(MessageSender<U> s) {
synchronized(mSenders) {
mSenders.remove(s.getId());
}
}
/**
* Interface implemented by all routing slips objects
*
* @author pjw
*
*/
private interface RoutingSlip {
public void deliver();
}
/**
* RoutingSlip to deliver a Message object to all associated listeners
*
* @author pjw
*/
private class MessageRoutingSlip implements RoutingSlip {
/** Destination queue (sender ID) */
long destination;
/** Message to deliver */
Message<T> message;
/** Constructor */
public MessageRoutingSlip(long destination, Message<T> message) {
this.destination = destination;
this.message = message;
}
/**
* Deliver message to all members of queue of sender
*/
@Override
public void deliver() {
// Iterator for iterating queue
Iterator<T> i = null;
MessageListeners queue = null;
// Get the queue and find the iterator
synchronized(mListeners) {
// Queue for given ID
queue = mListeners.get(destination);
if (queue != null) {
queue.setLastMessage(this);
i = queue.iterator();
}
}
// If we have an iterator, send the message to each listener
if (i != null) {
boolean handled = false;
while(i.hasNext()) {
T l = i.next();
try {
if (message.deliver(l)) {
handled = true;
break;
}
} catch (Exception e) {
Logger.logError(e, "Error delivering message to listener");
}
}
if (handled) {
queue.setLastMessage(null);
}
}
}
}
// private class ReplyRoutingSlip implements RoutingSlip {
// Long destination;
// Message<U> message;
// public ReplyRoutingSlip(Long destination, Message<U> message) {
// this.destination = destination;
// this.message = message;
// }
// @Override
// public void deliver() {
// synchronized(mSenders) {
// MessageSender<U> sender = mSenders.get(this.destination);
// if (sender != null) {
// message.deliver(sender.getReplyHandler());
// }
// }
// }
// }
/**
* Implementation of Message sender object
*
* @author pjw
*/
private class MessageSenderImpl implements MessageSender<U> {
private final long mId = ++mSenderIdCounter;
private final U mController;
/** Constructor */
public MessageSenderImpl(U controller) {
mController = controller;
}
/** accessor */
@Override
public long getId() {
return mId;
}
/** Accessor */
@Override
public U getController() {
return mController;
}
/** Close and delete this sender */
@Override
public void close() {
synchronized(mSenders) {
MessageSwitch.this.removeSender(this);
}
}
}
/**
* If in UI thread, then process the queue, otherwise post a new runnable
* to process the queued messages
*/
private void startProcessingMessages() {
if (mHandler.getLooper().getThread() == Thread.currentThread()) {
processMessages();
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
processMessages();
}}
);
}
}
/**
* Process the queued messages
*/
private void processMessages() {
RoutingSlip m = null;
do {
synchronized(mMessageQueue) {
m = mMessageQueue.poll();
}
if (m == null)
break;
m.deliver();
} while (true);
}
/**
* Accessor. Sometimes senders (or receivers) need to check which thread they are on and possibly post runnables.
*
* @return the Handler object
*/
public static Handler getHandler() {
return mHandler;
}
}