/**
* Copyright (c) 2010 Daniel Murphy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Created at 2:19:39 AM, Mar 12, 2010
*/
package com.dmurph.mvc;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dmurph.mvc.monitor.EventMonitor;
import com.dmurph.mvc.monitor.LoggingMonitor;
import com.dmurph.mvc.monitor.WarningMonitor;
import com.dmurph.mvc.tracking.ICustomTracker;
import com.dmurph.mvc.tracking.ITrackable;
import com.dmurph.tracking.JGoogleAnalyticsTracker;
/**
* This stores all the listener information, dispatches events
* to the corresponding listeners. To dispatch events use {@link MVCEvent#dispatch()}
* .</br>
* </br>
* Also, look at {@link #splitOff()}. To set up Google analytics, call
* {@link #setTracker(JGoogleAnalyticsTracker)},
* or implement {@link ICustomTracker} in your events to be tracked, and then any event
* that implements {@link ITrackable} will be tracked. If
* {@link ITrackable#getTrackingCategory()} or {@link ITrackable#getTrackingAction()}
* returns <code>null</code>, then it will be ignored.
*
* @author Daniel Murphy
*/
public class MVC extends Thread {
private static final Logger log = LoggerFactory.getLogger(MVC.class);
private static final ThreadGroup mvcThreadGroup = new ThreadGroup("MVC Thread Group");
private static final ArrayList<MVC> mvcThreads = new ArrayList<MVC>();
private static final HashMap<String, LinkedList<IEventListener>> listeners = new HashMap<String, LinkedList<IEventListener>>();
private static final Queue<MVCEvent> eventQueue = new LinkedList<MVCEvent>();
private static final Object trackerLock = new Object();
private volatile static JGoogleAnalyticsTracker tracker = null;
private static final Object monitorLock = new Object();
private volatile static IGlobalEventMonitor monitor = new LoggingMonitor();
private static final Object mainThreadLock = new Object();
private volatile static MVC mainThread;
private volatile static String currKey = null;
private volatile boolean running = false;
private final int threadCount;
private MVC(int argNum) {
super(mvcThreadGroup, "MVC Thread #" + argNum);
threadCount = argNum;
mvcThreads.add(this);
}
public static void setTracker(JGoogleAnalyticsTracker argTracker) {
synchronized (trackerLock) {
tracker = argTracker;
}
}
public static JGoogleAnalyticsTracker getTracker() {
return tracker;
}
/**
* Adds a listener for the given event key. If the listener is already listening
* to that key, then nothing is done. On the rare occurrence that the key is being
* dispatched at the same time by the mvc thread, this call will wait till all the
* events
* of that key are dispatched before adding and returning. If that happens and the
* thead making this call is also the mvc thread,
* (a listener for a key adds another listener for the same key), then a runtime
* exception is thrown.
*
* @param argKey
* @param argListener
*/
public static void addEventListener(String argKey, IEventListener argListener) {
if (argKey == null) {
throw new RuntimeException("Key cannot be null");
}
synchronized (listeners) {
synchronized (mainThreadLock) {
if (argKey.equals(currKey) && Thread.currentThread() == mainThread) {
throw new RuntimeException("Cannot add a listener to the same key that's being dispatched");
}
}
LinkedList<IEventListener> fifo;
if (listeners.containsKey(argKey)) {
// return if we're already listening
if (listeners.get(argKey).contains(argListener)) {
log.debug("We already have that listener here", argListener);
return;
}
fifo = listeners.get(argKey);
}
else {
fifo = new LinkedList<IEventListener>();
listeners.put(argKey, fifo);
}
fifo.add(argListener);
}
}
/**
* Checks to see if the listener is listening to the given key.
*
* @param argKey
* @param argListener
* @return
*/
public static boolean isEventListener(String argKey, IEventListener argListener) {
if (argKey == null) {
throw new RuntimeException("Key cannot be null");
}
synchronized (listeners) {
if (!listeners.containsKey(argKey)) {
return false;
}
LinkedList<IEventListener> stack = listeners.get(argKey);
return stack.contains(argListener);
}
}
/**
* Gets a copy of the listeners for the given event key.
*
* @param argKey
* @return
*/
public static LinkedList<IEventListener> getListeners(String argKey) {
if (argKey == null) {
throw new RuntimeException("Key cannot be null");
}
synchronized (listeners) {
if (listeners.containsKey(argKey)) {
return new LinkedList<IEventListener>(listeners.get(argKey));
}
else {
return new LinkedList<IEventListener>();
}
}
}
/**
* removes a listener from the given key.
*
* @param argKey
* @param argListener
* @return true if the listener was removed, and false if it wasn't there to
* begin with
*/
public static boolean removeEventListener(String argKey, IEventListener argListener) {
if (argKey == null) {
throw new RuntimeException("Key cannot be null");
}
synchronized (listeners) {
synchronized (mainThreadLock) {
if (argKey.equals(currKey) && Thread.currentThread() == mainThread) {
throw new RuntimeException(
"Cannot remove a listener to the same key that's being dispatched. Return false instead.");
}
}
if (listeners.containsKey(argKey)) {
LinkedList<IEventListener> stack = listeners.get(argKey);
return stack.remove(argListener);
}
else {
return false;
}
}
}
/**
* Adds an event to the dispatch queue for the MVC thread.
* Used by {@link MVCEvent#dispatch()}.
*
* @param argEvent
*/
protected static void dispatchEvent(MVCEvent argEvent) {
boolean hasListeners;
synchronized (listeners) {
hasListeners = listeners.containsKey(argEvent.key);
}
if (hasListeners) {
synchronized (eventQueue) {
eventQueue.add(argEvent);
eventQueue.notify();
}
if (!isDispatchThreadRunning()) {
startDispatchThread();
}
}
else {
synchronized (monitorLock) {
if (monitor != null) {
try {
monitor.noListeners(argEvent);
} catch (Exception e) {
log.error("Exception caught from monitor", e);
}
}
}
}
}
/**
* Split off the current MVC thread, all queued events and future
* event dispatches are handled by a new MVC thread, while this one
* runs to completion. If the thread calling this is not the current
* core MVC thread, then an exception is thrown
*
* @throws IllegalThreadException
* if the thread calling this is not an MVC thread
* @throws IncorrectThreadException
* if the MVC thread calling this is not the main thread, e.g.
* it has already split off.
*/
public static void splitOff() throws IllegalThreadException, IncorrectThreadException {
if (Thread.currentThread() instanceof MVC) {
MVC thread = (MVC) Thread.currentThread();
synchronized (mainThreadLock) {
if (thread == mainThread) {
log.debug("Splitting off...");
MVC old = mainThread;
old.running = false;
mainThread = new MVC(old.threadCount + 1);
log.debug("Starting next MVC thread");
mainThread.start();
}
else {
log.error("Can't split off when this isn't the main thread");
throw new IncorrectThreadException();
}
}
}
else {
log.error("Can't split off, we're not in the MVC thread.");
throw new IllegalThreadException();
}
}
/**
* Wait for all remaining events to dispatch
*
* @param timeoutMillis
* The maximum number of milliseconds to wait.
*/
public static void completeRemainingEvents(long timeoutMillis) {
boolean fifoEmpty = false;
long absTimeout = System.currentTimeMillis() + timeoutMillis;
while (System.currentTimeMillis() < absTimeout) {
synchronized (eventQueue) {
fifoEmpty = (eventQueue.size() == 0);
}
if (fifoEmpty) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
/**
* Stops the dispatch thread, dispatching any remaining events
* before cleanly returning. Thread automatically gets started
* when new events are dispatched
*/
public static void stopDispatchThread(long argTimeoutMillis) {
synchronized (mainThreadLock) {
mainThread.running = false;
synchronized (eventQueue) {
eventQueue.notify();
}
if ((mainThread != null) && (argTimeoutMillis > 0)) {
try {
mainThread.join(argTimeoutMillis);
} catch (InterruptedException e) {}
mainThread = null;
}
}
}
public static boolean isDispatchThreadRunning() {
synchronized (mainThreadLock) {
return mainThread != null && (mainThread.running || mainThread.getState() == State.RUNNABLE);
}
}
/**
* Manually starts the dispatch thread.
*/
public static void startDispatchThread() {
synchronized (mainThreadLock) {
if (mainThread == null) {
mainThread = new MVC(0);
}
if (!mainThread.running) {
if (mainThread.getState() == State.NEW) {
mainThread.start();
}
}
}
}
/**
* Sets the global event monitor, which is called before and after each event is
* dispatched.
*
* @param argMonitor
* @see IGlobalEventMonitor
*/
public static void setGlobalEventMonitor(IGlobalEventMonitor argMonitor) {
synchronized (monitorLock) {
monitor = argMonitor;
}
}
/**
* Gets the global event monitor. Default is {@link WarningMonitor}.
*
* @return
* @see IGlobalEventMonitor
*/
public static IGlobalEventMonitor getGlobalEventMonitor() {
synchronized (monitorLock) {
return monitor;
}
}
private volatile static EventMonitor guiMonitor = null;
/**
* Convenience method to construct and show an {@link EventMonitor}. To
* have more control on how the {@link EventMonitor} is configured,
* you can just create it yourself and use
* {@link #setGlobalEventMonitor(IGlobalEventMonitor)} to have it be the global event
* monitor.
*
* @return the {@link EventMonitor}.
*/
public static EventMonitor showEventMonitor() {
if (guiMonitor == null) {
synchronized (monitorLock) {
guiMonitor = new EventMonitor(monitor);
setGlobalEventMonitor(guiMonitor);
}
}
guiMonitor.setVisible(true);
return guiMonitor;
}
/**
* Hides the event monitor, if you had used {@link #showEventMonitor()}.
*/
public static void hideEventMonitor() {
if (guiMonitor != null) {
guiMonitor.setVisible(false);
}
}
@Override
public void run() {
running = true;
log.info("MVC thread #" + threadCount + " starting up");
while (running) {
try {
MVCEvent event = null;
synchronized (eventQueue) {
if (eventQueue.isEmpty()) {
eventQueue.wait();
}
if (!eventQueue.isEmpty()) {
event = eventQueue.poll();
}
}
if (event != null) {
internalDispatchEvent(event);
}
} catch (Exception e) {
log.error("Caught exception in dispatch thread", e);
}
}
mvcThreads.remove(this);
}
private void internalDispatchEvent(MVCEvent argEvent) {
if (monitor != null) {
synchronized (monitorLock) {
try {
monitor.beforeDispatch(argEvent);
} catch (Exception e) {
e.printStackTrace();
log.error("Exception caught from monitor", e);
}
}
}
if (argEvent instanceof ITrackable) {
ITrackable event = (ITrackable) argEvent;
if (event.getTrackingCategory() != null && event.getTrackingAction() != null) {
if (event instanceof ICustomTracker) {
((ICustomTracker) event).getCustomTracker().trackEvent(event.getTrackingCategory(),
event.getTrackingAction(), event.getTrackingLabel(), event.getTrackingValue());
}
else if (tracker != null) {
synchronized (trackerLock) {
tracker.trackEvent(event.getTrackingCategory(), event.getTrackingAction(),
event.getTrackingLabel(), event.getTrackingValue());
}
}
else {
log.warn("Event could not be tracked, as the tracker is null", event);
}
}
}
currKey = argEvent.key;
LinkedList<IEventListener> stack;
synchronized (listeners) {
stack = listeners.get(argEvent.key);
Iterator<IEventListener> it = stack.iterator();
while (it.hasNext() && argEvent.isPropagating()) {
try {
if (!it.next().eventReceived(argEvent)) {
it.remove();
}
} catch (Exception e) {
synchronized (monitorLock) {
if (monitor != null) {
try {// why do I have to do this? monitors shouldn't throw
// exceptions
monitor.exceptionThrown(argEvent, e);
} catch (Exception e2) {
log.error("Exception caught from event dispatch", e);
log.error("Exception caught from monitor", e2);
}
}
else {
log.error("Exception caught from event dispatch", e);
}
}
}
}
currKey = null;
}
synchronized (monitorLock) {
if (monitor != null) {
try {
monitor.afterDispatch(argEvent);
} catch (Exception e) {
log.error("Exception caught from monitor", e);
}
}
}
}
}