/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.*; import tufts.Util; import static tufts.Util.*; /** * This provides for tracking the single selection of a given typed * object, and providing notifications for interested listeners when this selection * changes. The one drawback to using a generics approach here is that once * type-erasure is complete, all the listener signatures are the same, so that a * single object can only ever listen for activeChanged calls for a single type * if it is declared as implementing an ActiveListener that includes type information. In * practice, it's easy to deal with this limitation using anonymous classes, or * by using the same handler method, and checking the class type in the passed * ActiveEvent (we can't rely on checking the type of the what's currently active, * as it may be null). * * Instances of this can be created on the fly by calling the static * method getHandler for a given class type, which will automatically * create a new handler for the given type if one doesn't exist. * If special handlers have been created that have side-effects (e.g., in onChange), * make sure they're instantiated before anyone asks for a handler * for the type that they handle. * @author Scott Fraize 2007-05-05 * @version $Revision: 1.29 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ */ public class ActiveInstance<T> { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ActiveInstance.class); private static final Map<Class,ActiveInstance> AllActiveHandlers = new HashMap(); private static final List<ActiveListener> ListenersForAllActiveEvents = new CopyOnWriteArrayList(); protected static int depth = -1; // event delivery depth private final CopyOnWriteArrayList<ActiveListener> mListeners = new CopyOnWriteArrayList(); private final Set<T> allInstances = Collections.synchronizedSet(new HashSet()); protected final Class itemType; protected final String itemTypeName; // for debug protected final boolean itemIsMarkable; protected final boolean itemsAreTracked; private volatile T nowActive; private ActiveEvent lastEvent; private boolean inNotify; /** * If the active item itself wants to be told when it's been set to active or has lost it's active status, * it can implement this interface, and it will be told as it goes active / inactive. */ public interface Markable { public void markActive(boolean active); } /** Add's a lister for ALL active item events */ public static void addAllActiveListener(ActiveListener l) { ListenersForAllActiveEvents.add(l); } public static void remoteAllActiveListener(ActiveListener l) { ListenersForAllActiveEvents.remove(l); } public static void addListener(Class clazz, ActiveListener listener) { getHandler(clazz).addListener(listener); } public static void addListener(Class clazz, Object reflectedListener) { getHandler(clazz).addListener(reflectedListener); } public static void removeListener(Class clazz, ActiveListener listener) { getHandler(clazz).removeListener(listener); } public static void removeListener(Class clazz, Object reflectedListener) { getHandler(clazz).removeListener(reflectedListener); } public static void set(Class clazz, Object source, Object item) { getHandler(clazz).setActive(source, item); } public static ActiveInstance getHandler(Class type) { ActiveInstance handler = null; lock(null, "getHandler"); synchronized (AllActiveHandlers) { handler = AllActiveHandlers.get(type); if (handler == null) handler = new ActiveInstance(type); } unlock(null, "getHandler"); return handler; } // public static void get(Class clazz) { // getHandler(clazz).getActive(); // } protected ActiveInstance(Class clazz) { this(clazz, false); } protected ActiveInstance(Class clazz, boolean trackInstances) { itemType = clazz; itemTypeName = "<" + itemType.getName() + ">"; itemIsMarkable = clazz.isInstance(Markable.class); itemsAreTracked = trackInstances; lock(clazz, "INIT"); synchronized (AllActiveHandlers) { if (AllActiveHandlers.containsKey(itemType)) { // tho this is not ideal, the safest thing to do is blow away the old one, // as it's likely this accidentally happened by a request for a generic // listener before a specialized side-effecting type-handler was initiated. // We copy over the listeners from the old handler if there were any. Log.warn("ignoring prior active change handler for " + getClass() + " and taking over listeners", new Throwable("HERE")); mListeners.addAll(getHandler(itemType).mListeners); } AllActiveHandlers.put(itemType, this); } unlock(clazz, "INIT"); if (DEBUG.INIT || DEBUG.EVENTS) Log.debug("created " + this); } public synchronized void refreshListeners() { notifyListeners(lastEvent); } public int instanceCount() { return allInstances.size(); } public Set<T> getAllInstances() { return Collections.unmodifiableSet(allInstances); } /** @return true if the item was being tracked */ public boolean stopTracking(T instance) { return allInstances.remove(instance); } private static String sourceName(Object s) { if (s == null) return "null"; else if (s instanceof ActiveEvent || s instanceof ActiveInstance) return s.toString(); else return s.getClass().getName() + ":" + s; } public void setActive(final Object source, final T newActive) { if (newActive != null && !itemType.isInstance(newActive)) { Util.printStackTrace(this + ": setActive(" + newActive + ") by " + source + "; not an instance of " + itemType); return; } // nowActive is volatile: all threads guaranteed to see it's current value w/out a synchronization if (nowActive == newActive) return; lock(this, "setActive"); synchronized (this) { // double-checked locking of volatile: if (nowActive == newActive) return; final T oldActive = nowActive; this.nowActive = newActive; setActiveAndNotify(source, oldActive); unlock(this, "setActive"); } } private synchronized void setActiveAndNotify(final Object source, final T oldActive) { if (DEBUG.EVENTS) { Log.debug(TERM_YELLOW + itemTypeName); System.out.println( "\toldActive: " + oldActive + "\n\tnewActive: " + nowActive + "\n\t source: " + sourceName(source) + "\n\tlisteners: " + mListeners.size() + " in " + Thread.currentThread().getName() + TERM_CLEAR ); } if (itemsAreTracked) { allInstances.add(nowActive); } if (itemIsMarkable) { markActive( (Markable) oldActive, false); markActive( (Markable) nowActive, true); } final ActiveEvent e = new ActiveEvent(itemType, source, oldActive, nowActive); notifyListeners(e); try { onChange(e); } catch (Throwable t) { tufts.Util.printStackTrace(t, this + " onChange failed in implementation subclass: " + getClass()); } this.lastEvent = e; } public void redeliver(Object source) { setActiveAndNotify(source, nowActive); // we want diagnostics // final ActiveEvent e = new ActiveEvent(itemType, source, nowActive, nowActive); // notifyListeners(e); // onChange(e); // lastEvent = e; } private void markActive(Markable markableItem, boolean active) { try { markableItem.markActive(active); } catch (Throwable t) { tufts.Util.printStackTrace(t, this + " marking active state to " + active + " on " + markableItem); } } protected void onChange(ActiveEvent<T> e) { onChange(e, e.active); } protected void onChange(ActiveEvent<T> e, T nowActive) {} protected void notifyListeners(ActiveEvent<T> e) { if (inNotify) { String msg = this + " event loop: skipping delivery of: " + e; if (DEBUG.EVENTS) Log.warn(msg, new Throwable("HERE")); else Log.warn(msg); return; } inNotify = true; try { depth++; if (mListeners.size() > 0) notifyListenerList(this, e, mListeners); if (ListenersForAllActiveEvents.size() > 0) notifyListenerList(this, e, ListenersForAllActiveEvents); } finally { depth--; inNotify = false; } } protected static void notifyListenerList(ActiveInstance handler, ActiveEvent e, Collection<ActiveListener> listenerList) { // final ActiveListener[] listeners; // if (DEBUG.Enabled) lock(handler, "NOTIFY " + listenerList.size()); // synchronized (listenerList) { // // Allow concurrent modifiation w/out synchronization: // // (todo performance: keep an array in the handler to write this into instead // // of having to construct if every time). // listeners = listenerList.toArray(new ActiveListener[listenerList.size()]); // } // if (DEBUG.Enabled) unlock(handler, "NOTIFY " + listenerList.size()); int count = 0; Object target; Method method; for (ActiveListener listener : listenerList) { if (listener instanceof MethodProxy) { final MethodProxy proxy = (MethodProxy) listener; target = proxy.target; method = proxy.method; } else { target = listener; method = null; } count++; // checking the deep source might prevent us from uncovering a loop where // the active item is cycling -- actually, we could only get here again if // the active item IS cycling, because setActive of something already active // does nothing... //if (e.hasSource(target)) if (e.source == target) { if (DEBUG.EVENTS) outf(" %2dskipSrc %s -- %s\n", count, handler.itemTypeName, target); continue; } else { if (DEBUG.EVENTS) outf(" %2d notify %s -> %s\n", count, handler.itemTypeName, target); } try { if (method != null) method.invoke(target, e, e.active); else listener.activeChanged(e); } catch (java.lang.reflect.InvocationTargetException ex) { tufts.Util.printStackTrace(ex.getCause(), handler + " exception notifying " + target + " with " + e); } catch (Throwable t) { tufts.Util.printStackTrace(t, handler + " exception notifying " + target + " with " + e); } } } public T getActive() { return nowActive; } public void addListener(ActiveListener listener) { if (mListeners.addIfAbsent(listener)) { if (DEBUG.EVENTS) Log.debug(String.format(TERM_YELLOW + "%-50s added listener %s" + TERM_CLEAR, itemTypeName, listener)); } else { Log.warn(this + "; add: is already listening: " + listener); if (DEBUG.EVENTS) Util.printStackTrace(this + "; FYI: already listening: " + listener); } } public void addListener(Object reflectedListener) { Method method = null; try { // We could cache the method for the class of the given listener // so future instance's of the class don't have to do the method lookup, // but this type of listener is not frequently added. method = reflectedListener.getClass().getMethod("activeChanged", ActiveEvent.class, itemType); } catch (Throwable t) { tufts.Util.printStackTrace(t, this + ": " + reflectedListener.getClass() + " must implement activeChanged(ActiveEvent, " + itemType + ")" + " to be a listener for the active instance of " + itemType); return; } addListener(new MethodProxy(reflectedListener, method)); } public void removeListener(ActiveListener listener) { if (mListeners.remove(listener)) { if (DEBUG.EVENTS) outf(TERM_YELLOW + "%-50s removed listener %s\n" + TERM_CLEAR, this, listener); } else if (DEBUG.EVENTS) { Log.warn(this + "; remove: didn't contain listener " + listener); } } public void removeListener(Object reflectedListener) { removeListener(new MethodProxy(reflectedListener, null)); } private static void lock(Object o, String msg) { if (DEBUG.THREAD) System.err.println((o == null ? "ActiveInstance" : o) + " " + msg + " LOCK"); } private static void unlock(Object o, String msg) { if (DEBUG.THREAD) System.err.println((o == null ? "ActiveInstance" : o) + " " + msg + " UNLOCK"); } private static void outf(String fmt, Object... args) { for (int x = 0; x < depth; x++) System.out.print(" "); System.out.format(fmt, args); //Log.debug(String.format(fmt, args)); } public String toString() { return "ActiveInstance" + itemTypeName; } private static final class MethodProxy implements ActiveListener { final Object target; final Method method; MethodProxy(Object t, Method m) { target = t; method = m; } @Override public int hashCode() { return System.identityHashCode(target); } @Override public boolean equals(Object o) { if (o instanceof MethodProxy) return target == ((MethodProxy)o).target; else return false; } @Override public String toString() { if (method == null) return String.format("%s[%s]", getClass().getSimpleName(), target); else return String.format("%s[%s.%s]", getClass().getSimpleName(), target, method.getName()); //return String.format("[%s]", method); } public void activeChanged(ActiveEvent e) { /* try { method.invoke(target, e, e.active); } catch (java.lang.IllegalAccessException ex) { throw new Error(ex); } catch (java.lang.reflect.InvocationTargetException ex) { throw new Error(ex.getCause()); } */ } } }