/*
* 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 tufts.Util;
import static tufts.Util.*;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Handle dispatching of LWCEvents, mainly for LWComponents, but any client
* class can use this for event dispatch, selective listening, and the heavy-duty
* diagnostic support.
*
* @version $Revision: 1.10 $ / $Date: 2007/11/19 06:20:27 $ / $Author: sfraize $
* @author Scott Fraize
*/
public class LWChangeSupport
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWChangeSupport.class);
private static int sEventDepth = 0;
private List listeners;
private Object mClient;
private boolean mEventsDisabled = false;
private int mEventSuspensions = 0;
LWChangeSupport(Object client) {
mClient = client;
}
private static class LWCListenerProxy implements LWComponent.Listener
{
private final LWComponent.Listener listener;
private final Object eventMask;
public LWCListenerProxy(LWComponent.Listener listener, Object eventsDesired) {
this.listener = listener;
this.eventMask = eventsDesired;
}
/** this should never actually get used, as we pluck the real listener
out of the proxy in dispatch */
public void LWCChanged(LWCEvent e) {
listener.LWCChanged(e);
}
public boolean isListeningFor(LWCEvent e)
{
if (eventMask instanceof Object[]) {
for (Object desiredKey : (Object[]) eventMask) {
if (e.key == desiredKey)
return true;
}
return false;
} else {
return eventMask == e.key;
}
}
public String toString() {
String s = listener.toString();
if (eventMask != null) {
s += ":only<";
if (eventMask instanceof Object[]) {
Object[] eventKeys = (Object[]) eventMask;
for (int i = 0; i < eventKeys.length; i++) {
if (i>0) s+= ",";
s += eventKeys[i];
}
} else {
s += eventMask;
}
s += ">";
}
return s;
}
}
private class LWCListenerList extends java.util.Vector {
public synchronized int indexOf(Object elem, int index) {
if (elem == null) {
for (int i = index ; i < elementCount ; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = index ; i < elementCount ; i++) {
Object ed = elementData[i];
if (elem.equals(ed))
return i;
if (ed instanceof LWCListenerProxy && ((LWCListenerProxy)ed).listener == elem)
return i;
}
}
return -1;
}
public synchronized int lastIndexOf(Object elem, int index) {
throw new UnsupportedOperationException("lastIndexOf");
}
}
/**
* Move @param listener - existing listener, to the front of the
* notification list, so it get's notifications before anyone else.
*/
/* Todo: create ChangeSupport style class for managing & notifying
* listeners, with setPriority, etc, that LWSelection, the VUE
* map & viewer listeners, etc, can all use.
*/
public synchronized void setPriorityListener(LWComponent.Listener listener) {
if (listeners == null) {
Log.error("Attempting to set priorty listener with no listeners at all for client " + mClient);
return;
}
int i = listeners.indexOf(listener);
if (i > 0) {
if (i == 1) {
// swap first two
listeners.set(1, listeners.set(0, listener));
} else {
// remove & add back at the front
listeners.remove(listener);
listeners.add(0, listener);
}
} else if (i == 0) {
; // already priority listener
} else
throw new IllegalArgumentException(mClient + " " + listener + " not already a listener");
}
public synchronized void addListener(LWComponent.Listener listener) {
addListener(listener, null);
}
public synchronized void addListener(LWComponent.Listener listener, Object eventMask)
{
if (listeners == null)
listeners = new LWCListenerList();
if (listeners.contains(listener)) {
// do nothing (they're already listening to us)
if (DEBUG.EVENTS) {
if (DEBUG.META) System.out.println("already listening to us: " + listener + " " + mClient);
//if (DEBUG.META) new Throwable("already listening to us:" + listener + " " + mClient).printStackTrace();
}
} else if (listener == mClient) {
// This is likely to produce event loops, and the exception is here as a safety measure.
throw new IllegalArgumentException(mClient + " attempt to add self as LWCEvent listener: not allowed");
} else {
if (DEBUG.EVENTS && DEBUG.META)
outln("*** LISTENER " + listener + "\t+++ADDS " + mClient + (eventMask==null?"":(" eventMask=" + eventMask)));
if (eventMask == null) {
listeners.add(listener);
} else
listeners.add(new LWCListenerProxy(listener, eventMask));
}
}
public synchronized void removeListener(LWComponent.Listener listener)
{
if (listeners == null)
return;
if (DEBUG.EVENTS && DEBUG.META) System.out.println("*** LISTENER " + listener + "\tREMOVES " + mClient);
listeners.remove(listener);
}
public synchronized void removeAllListeners()
{
if (listeners != null) {
if (DEBUG.EVENTS) System.out.println(mClient + " *** CLEARING ALL LISTENERS " + listeners);
listeners.clear();
}
}
private void setEventsEnabled(boolean t) {
if (DEBUG.EVENTS&&DEBUG.META) System.out.println(mClient + " *** EVENTS ENABLED: from " + !mEventsDisabled + " to " + t);
mEventsDisabled = !t;
}
protected synchronized void setEventsSuspended() {
mEventSuspensions++;
setEventsEnabled(false);
}
protected synchronized void setEventsResumed() {
mEventSuspensions--;
if (mEventSuspensions < 0)
throw new IllegalStateException("events suspend/resume unpaired");
if (mEventSuspensions == 0)
setEventsEnabled(true);
}
public boolean eventsDisabled() {
return mEventsDisabled;
}
private static final String DELIVERY_ARROW = TERM_GREEN + " => " + TERM_CLEAR;
/**
* This method for clients that are LWComponent's ONLY. Otherwise call dispatchLWCEvent
* directly.
*/
synchronized void notifyListeners(LWComponent client, LWCEvent e)
{
if (mEventsDisabled) {
if (DEBUG.EVENTS) System.out.println(e + " (dispatch skipped: events disabled)");
return;
}
if (client.isDeleted() && !client.permitZombieEvent(e)) {
String msg =
"FYI, ZOMBIE EVENT: notifyListeners; deleted component attempting event notification:"
+ "\n\t deleted client: " + client
+ "\n\tattempting delivery of: " + e
+ "\n\t current listeners: " + listeners
+ "\n\tparent (ought be null): " + client.getParent()
+ "\n"
;
// this situation not so serious at this point: we may have no listeners
if (DEBUG.Enabled)
Log.warn(msg, new Throwable("FYI"));
//Util.printStackTrace(msg);
else
Log.warn(msg);
}
//if (DEBUG.EVENTS && (DEBUG.META || !DEBUG.THREAD)) {
if (DEBUG.EVENTS && (DEBUG.META || e.isUndoable()) && (DEBUG.META || listeners != null)) {
final String ldesc = (listeners == null
? " -> <no listeners>"
: ((listeners.size()>0?TERM_GREEN:"") + " => (" + listeners.size() + " listeners)" + TERM_CLEAR
//+ " " + Arrays.asList(listeners)
));
//: (DELIVERY_ARROW + "(" + listeners.size() + " listeners)"));a
if (client != e.getSource())
eoutln(e + " => " + client + ldesc);
else
eoutln(e + ldesc);
}
if (listeners != null && listeners.size() > 0) {
if (DEBUG.EVENTS && DEBUG.META) eoutln(e + " dispatching for client " + client + " to listeners " + Arrays.asList(listeners));
dispatchLWCEvent(client, listeners, e);
} else {
//if (DEBUG.EVENTS && DEBUG.THREAD && (DEBUG.META || DEBUG.CONTAINMENT))
if (DEBUG.EVENTS && DEBUG.THREAD)
eoutln(e + " -> " + "<NO LISTENERS>" + (client.isOrphan() ? " (orphan)":""));
}
// todo: have a seperate notifyParent? -- every parent
// shouldn't have to be a listener
// todo: "added" events don't need to go thru parent chain as
// a "childAdded" event has already taken place (but
// listeners, eg, inspectors, may need to know to see if the
// parent changed)
if (client.getParent() != null) {
if (DEBUG.EVENTS && DEBUG.META) {
eoutln(e + " " + client.getParent() + " ** PARENT UP-NOTIFICATION");
}
client.getParent().broadcastChildEvent(e);
} else if (client.isOrphan() && !client.permitZombieEvent(e)) {
if (listeners != null && listeners.size() > 0) {
Log.info("ORPHAN NODE w/LISTENERS DELIVERED EVENTS:"
+ "\n\torphan=" + client
+ "\n\tevent=" + e
+ "\n\tlisteners=" + listeners);
if (DEBUG.PARENTING) new Throwable().printStackTrace();
}
/*else if (DEBUG.META && (DEBUG.EVENTS || DEBUG.PARENTING) && !(this instanceof LWGroup))
// dragged selection group is a null parented object, so we're
// ignoring all groups for purposes of this diagnostic for now.
System.out.println(e + " (FYI: orphan node event)");
*/
}
}
private static void eout(String s) {
synchronized (System.err) {
//if (DEBUG.THREAD) System.err.format("%-27s", Thread.currentThread().toString().substring(6));
if (!javax.swing.SwingUtilities.isEventDispatchThread())
System.err.format("[%s]", Thread.currentThread().getName());
for (int x = 0; x < sEventDepth; x++) System.err.print("--->");
System.err.print(s);
}
}
private static void eoutln(String s) {
eout(s + "\n");
}
private static void outln(String s) {
System.err.println(s);
}
public void dispatchEvent(LWCEvent e) {
if (listeners != null)
dispatchLWCEvent(mClient, listeners, e);
}
/**
* Deliver LWCEvent @param e to all the @param listeners
*/
static synchronized void dispatchLWCEvent(Object source, List listeners, LWCEvent e)
{
if (sEventDepth > 5) // guestimate max based on current architecture -- increase if you need to
throw new IllegalStateException("eventDepth=" + sEventDepth
+ ", assumed looping on delivery of "
+ e + " in " + source + " to " + listeners);
if (source instanceof LWComponent) {
final LWComponent client = (LWComponent) source;
if ((client.isDeleted() && !client.permitZombieEvent(e)) || listeners == null) {
System.err.println("ZOMBIE DISPATCH: deleted component or null listeners attempting event dispatch:"
+ "\n\tsource=" + source
+ "\n\tlisteners=" + listeners
+ "\n\tattempted notification=" + e);
new Throwable("ZOMBIE DISPATCH").printStackTrace();
return;
}
}
// todo perf: take array code out and see if can fix all
// concurrent mod exceptions (e.g., delete out from under a
// pathway was giving us some problems, tho I think that may
// have gone away) or: allow listener removes via nulling, tho
// that's not really a concern anyway in that a component that
// removes itself as a listener after having been notified of
// an event has already had it's notification and we don't
// need to make sure it doesn't get one further down the list.
//int nlistener = listeners.size();
//Listener[] listener_array = (Listener[]) listeners.toArray(listener_buf);
//if (listener_array != listener_buf)
// out("FYI: listener count " + nlistener + " exceeded performance buffer.");
// Of course, using a static buf of course doesn't work the second we have any event depth!
// and we can't make it a member as this is a static method...
// We could actually just only allocate a new array if there IS any event depth...
LWComponent.Listener[] listener_array = new LWComponent.Listener[listeners.size()];
listeners.toArray(listener_array);
for (int i = 0; i < listener_array.length; i++) {
if (DEBUG.EVENTS && DEBUG.META) {
if (e.getSource() != source)
eout(e + " " + i + " => " + source + " >> ");
else
eout(e + " " + i + " >> ");
}
LWComponent.Listener target = listener_array[i];
//-------------------------------------------------------
// If a listener proxy, extract the real listener
//-------------------------------------------------------
if (target instanceof LWCListenerProxy) {
LWCListenerProxy lp = (LWCListenerProxy) target;
if (!lp.isListeningFor(e)) {
if (DEBUG.EVENTS && DEBUG.META)
outln(target + " (filtered)");
continue;
}
target = lp.listener;
}
//-------------------------------------------------------
// now we know we have the real listener
//-------------------------------------------------------
if (DEBUG.EVENTS && DEBUG.THREAD) {
if (DEBUG.META) {
if (e.getSource() == target)
outln(target + " (SKIPPED: source)");
} else if (e.getSource() != target) {
if (e.getSource() != source)
eout(e + " => " + source + DELIVERY_ARROW);
else
eout(e + DELIVERY_ARROW);
}
}
if (e.getSource() == target) // this prevents events from going back to their source
continue;
sEventDepth++;
try {
if (DEBUG.EVENTS && DEBUG.THREAD)
outln(target + "");
//-------------------------------------------------------
// deliver the event
//-------------------------------------------------------
target.LWCChanged(e);
} catch (Throwable t) {
tufts.Util.printStackTrace
(t,
"dispatchLWCEvent: exception during LWCEvent notification:"
+ "\n\tnotifying component: " + source
+ "\n\t event was: " + e
+ "\n\t failing listener: " + target);
} finally {
sEventDepth--;
}
if (DEBUG.EVENTS && DEBUG.META) eoutln(e + " disptach returned from: " + target);
}
}
}