Package com.dmurph.mvc

Source Code of com.dmurph.mvc.MVC

/**
* 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.List;
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, List<IEventListener>> listeners = new HashMap<String, List<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 Iterator<IEventListener> currEventList;
  private MVCEvent currEvent;

  private MVC(int argNum) {
    super(mvcThreadGroup, "MVC Thread #" + argNum);
    threadCount = argNum;
    mvcThreads.add(this);
  }

  private MVC(int argNum, Iterator<IEventListener> currEventList,
      MVCEvent currEvent) {
    this(argNum);
    this.currEvent = currEvent;
    this.currEventList = currEventList;
  }

  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");
        }
      }
      List<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 ArrayList<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;
      }

      List<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)) {
        List<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,
              old.currEventList, old.currEvent);
          old.currEvent = null;
          old.currEventList = null;
          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);
    }
  }

  public static boolean isMainMVCThread() {
    MVC thread = (MVC) Thread.currentThread();
    return thread == mainThread;
  }

  @Override
  public void run() {
    running = true;
    log.info("MVC thread #" + threadCount + " starting up");
    while (running) {
      IEventListener listener;
      if (currEvent != null && currEventList != null
          && currEventList.hasNext() && currEvent.isPropagating()) {
        synchronized (listeners) {
          listener = currEventList.next();
        }
        tryPreMonitor(currEvent);
        tryTrackEvent(currEvent);
        try {
          if (!listener.eventReceived(currEvent)) {
            if (isMainMVCThread()) {
              synchronized (listeners) {
                currEventList.remove();
              }
            } else {
              log.error("Cannot remove the listener " + listener
                  + ", as we've been split off");
            }
          }
        } catch (Exception e) {
          synchronized (monitorLock) {
            if (monitor != null) {
              try {// why do I have to do this? monitors shouldn't
                  // throw
                  // exceptions
                monitor.exceptionThrown(currEvent, 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);
            }
          }
        }
        tryPostMonitor(currEvent);
      } else {
        // grab next event
        try {
          synchronized (eventQueue) {
            if (eventQueue.isEmpty()) {
              eventQueue.wait();
            }

            if (!eventQueue.isEmpty()) {
              currEvent = eventQueue.poll();
            }
          }

          if (currEvent != null) {
            synchronized (listeners) {
              currEventList = listeners.get(currEvent.key)
                  .iterator();
            }
          }
        } catch (Exception e) {
          log.error("Caught exception in dispatch thread", e);
        }
      }

    }
    mvcThreads.remove(this);
  }

  private void tryTrackEvent(MVCEvent argEvent) {
    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);
        }
      }
    }
  }

  private void tryPreMonitor(MVCEvent argEvent) {
    if (monitor != null) {
      synchronized (monitorLock) {
        try {
          monitor.beforeDispatch(argEvent);
        } catch (Exception e) {
          log.error("Exception caught from monitor", e);
        }
      }
    }
  }

  private void tryPostMonitor(MVCEvent argEvent) {
    synchronized (monitorLock) {
      if (monitor != null) {
        try {
          monitor.afterDispatch(argEvent);
        } catch (Exception e) {
          log.error("Exception caught from monitor", e);
        }
      }
    }
  }
}
TOP

Related Classes of com.dmurph.mvc.MVC

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.