/*
* 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());
}
*/
}
}
}