All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.odell.glazedlists.event.SequenceDependenciesEventPublisher Maven / Gradle / Ivy

/* Glazed Lists                                                 (c) 2003-2006 */
/* http://publicobject.com/glazedlists/                      publicobject.com,*/
/*                                                     O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.event;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.impl.adt.IdentityMultimap;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.*;

/**
 * Manage listeners, firing events, and making sure that events arrive in order.
 *
 * 

This manages listeners across multiple objects in a pipeline of observables * and their listeners. It implements Martin Fowler's * EventAggregator * design. * *

To guarantee a safe notification order, this class makes sure that all an * object's dependencies have been notified of a particular event before that * object is itself notified. This is tricky because it requires us to interrupt * the event flow and control its flow. In this class, event flow is controlled * by queueing events and not necessarily firing them during the {@link #fireEvent} * method. * * @author Jesse Wilson */ final class SequenceDependenciesEventPublisher implements ListEventPublisher, Serializable { /** For versioning as a {@link Serializable} */ private static final long serialVersionUID = -8228256898169043019L; /** keep track of how many times the fireEvent() method is on the stack */ private transient int reentrantFireEventCount; /** subject to cleanup when this event is completely distributed */ private transient final Map subjectsToCleanUp = new IdentityHashMap(); /** for proper dependency management, when a listener and subject aren't the same identity */ private transient final Map listenersToRelatedSubjects = new IdentityHashMap(); /** the last listener notified, the next one will be beyond it in the list */ private transient int nextToNotify; /** * A mix of different subjects and listeners pairs in a deliberate order. * We should be careful not to make changes to this list directly and instead * create a copy as necessary */ private transient List subjectAndListeners = Collections.emptyList(); /** * We use copy-on-write on the listeners list. This is a copy of the * listeners list as it looked immediately before the current change * started. If there is no change going on (reentrantFireEventCount == 0), * then this should be null. */ private transient List subjectsAndListenersForCurrentEvent; /** Returns a proper initialized publisher object during deserialization. */ private Object readResolve() throws ObjectStreamException { return new SequenceDependenciesEventPublisher(); } /** * Rebuild the subject and listeners list so that all required invariants * are met with respect to notification order. That is, for any listener * T, all of the subjects S that T listens to have been updated before T * receives a change event from any S. * *

This implementation still has some problems and work left to do: *

  • it's big! Can we optimize it? Perhaps shortcutting all the graph * work for simple cases (the 99% case) *
  • it's complex! Can we simplify it? *
  • could we keep the datastructures around? it may be wasteful to * reconstruct them every single time a listener is added */ private List orderSubjectsAndListeners(List subjectsAndListeners) { // since we're regenerating the subjectAndListeners list, clear it and re-add the elements List result = new ArrayList(); // HashMaps of unprocessed elements, keyed by both source and target IdentityMultimap sourceToPairs = new IdentityMultimap(); IdentityMultimap targetToPairs = new IdentityMultimap(); // everything that has all of its listeners already notified in subjectAndListeners Map satisfied = new IdentityHashMap(); // everything that has a listener already notified List satisfiedToDo = new ArrayList(); // prepare the initial collections: maps that show how each element is // used as source and target in directed edges, plus a list of nodes // that have no incoming edges for(int i = 0, size = subjectsAndListeners.size(); i < size; i++) { SubjectAndListener subjectAndListener = subjectsAndListeners.get(i); Object source = subjectAndListener.subject; Object target = getRelatedSubject(subjectAndListener.listener); sourceToPairs.addValue(source, subjectAndListener); targetToPairs.addValue(target, subjectAndListener); satisfied.remove(target); if(targetToPairs.count(source) == 0) { satisfied.put(source, Boolean.TRUE); } } // start with the initial set of sources that don't have dependencies satisfiedToDo.addAll(satisfied.keySet()); // We have a subject which has all of its dependencies satisfied. // ie. all edges where this subject is a target are already in // subjectAndListeners. Now we want to find further edges from this // subject to further objects. We find all of its listeners, and look // for one of them where all of its dependencies are in the // satisfied list. If we find such a listener, add all its edges // to subjectAndListeners and enque it to find it's listeners // iteratively while(!satisfiedToDo.isEmpty()) { // for everything that's not a target, Object subject = satisfiedToDo.remove(0); // get all listeners to this subject, we try this set because // we know at least one of their edges is satisfied, and // we hope that all of their edges is satisfied. List sourceTargets = sourceToPairs.get(subject); // can we satisfy this target? tryEachTarget: for(int t = 0, targetsSize = sourceTargets.size(); t < targetsSize; t++) { Object sourceTarget = getRelatedSubject(sourceTargets.get(t).listener); // make sure we can satisfy this if all its sources are in satisfiedSources List allSourcesForSourceTarget = targetToPairs.get(sourceTarget); // we've since processed this entire target, we shouldn't process it twice if(allSourcesForSourceTarget.size() == 0) continue; for(int s = 0, sourcesSize = allSourcesForSourceTarget.size(); s < sourcesSize; s++) { SubjectAndListener sourceAndTarget = allSourcesForSourceTarget.get(s); if(!satisfied.containsKey(sourceAndTarget.subject)) { continue tryEachTarget; } } // we know we can satisfy this target, add all its edges result.addAll(allSourcesForSourceTarget); targetToPairs.remove(sourceTarget); // this target is no longer considered a target, since all // its dependencies are satisfied satisfiedToDo.add(sourceTarget); satisfied.put(sourceTarget, Boolean.TRUE); } } // if there's remaining targets, we never covered everything if(!targetToPairs.isEmpty()) { throw new IllegalStateException("Listener cycle detected, " + targetToPairs.values()); } // success! return result; } private Object getRelatedSubject(Object listener) { Object subject = listenersToRelatedSubjects.get(listener); if(subject == null) return listener; return subject; } /** * Register the specified listener to receive events from the specified * subject whenever they are fired. */ public synchronized void addListener(Subject subject, Listener listener, EventFormat eventFormat) { List unordered = updateListEventListeners(subject, listener, null, eventFormat); subjectAndListeners = orderSubjectsAndListeners(unordered); } /** * Deregister the specified listener from recieving events from the specified * subject. */ public synchronized void removeListener(Object subject, Object listener) { subjectAndListeners = updateListEventListeners(subject, null, listener, null); } /** * Support method for adding and removing listeners, that also cleans up * stale listeners, such as those from weak references. * * @param listenerToAdd a listener to be added, or null * @param listenerToRemove a listener to be removed, or null */ private List updateListEventListeners(Subject subject, Listener listenerToAdd, Listener listenerToRemove, EventFormat eventFormat) { // we'll want to output a copy of all the listeners int anticipatedSize = this.subjectAndListeners.size() + (listenerToAdd == null ? - 1 : 1); List result = new ArrayList(anticipatedSize); // walk through, adding all the old listeners to the new listeners list, // unless a particular listener is slated for removal for some reaosn for(int i = 0, n = subjectAndListeners.size(); i < n; i++) { final SubjectAndListener originalSubjectAndListener = subjectAndListeners.get(i); // if we're supposed to remove this listener, skip it if(originalSubjectAndListener.listener == listenerToRemove && originalSubjectAndListener.subject == subject) { listenerToRemove = null; continue; } // if this listener is stale, skip it if(originalSubjectAndListener.eventFormat.isStale(originalSubjectAndListener.subject, originalSubjectAndListener.listener)) { continue; } // this listener's still good, keep it! result.add(originalSubjectAndListener); } // sanity check to ensure we found the listener we were asked to remove, if any if(listenerToRemove != null) { throw new IllegalArgumentException("Cannot remove nonexistent listener " + listenerToRemove); } // add the listener we were asked to add, if any if(listenerToAdd != null) { result.add(new SubjectAndListener(subject, listenerToAdd, eventFormat)); } return result; } /** {@inheritDoc} */ public void setRelatedListener(Object subject, Object relatedListener) { // force the dependency by adding a listener that just doesn't // do anything. This will make sure that subject is always after // related listener in the dependencies graph addListener(relatedListener, subject, NoOpEventFormat.INSTANCE); } /** {@inheritDoc} */ public void clearRelatedListener(Object subject, Object relatedListener) { removeListener(relatedListener, subject); } /** {@inheritDoc} */ public void addDependency(EventList dependency, ListEventListener listener) { // do nothing } /** {@inheritDoc} */ public void removeDependency(EventList dependency, ListEventListener listener) { // do nothing } /** {@inheritDoc} */ public void setRelatedSubject(Object listener, Object relatedSubject) { if(relatedSubject != null) { listenersToRelatedSubjects.put(listener, relatedSubject); } else { listenersToRelatedSubjects.remove(listener); } } /** {@inheritDoc} */ public void clearRelatedSubject(Object listener) { listenersToRelatedSubjects.remove(listener); } /** * Get all listeners of the specified object. */ public synchronized List getListeners(Object subject) { List result = new ArrayList(); for(int i = 0, size = subjectAndListeners.size(); i < size; i++) { SubjectAndListener subjectAndListener = subjectAndListeners.get(i); if(subjectAndListener.subject != subject) continue; result.add(subjectAndListener.listener); } return result; } /** * Notify all listeners of the specified subject of the specified event. * * @param subject the event's source * @param event the event to send to all listeners * @param eventFormat the mechanism to notify listeners of the event, also * used for a callback when this event is complete */ public void fireEvent(Subject subject, Event event, EventFormat eventFormat) { // keep the subjects and listeners as they are at the beginning of // the topmost event, the list won't change because we copy on write if(reentrantFireEventCount == 0) { subjectsAndListenersForCurrentEvent = subjectAndListeners; nextToNotify = Integer.MAX_VALUE; } // keep track of whether this method is being reentered because one // event caused another event. If so, we'll fire later reentrantFireEventCount++; try { // record this subject as firing an event, so we can clean up later EventFormat previous = subjectsToCleanUp.put(subject, eventFormat); if(previous != null) throw new IllegalStateException("Reentrant fireEvent() by \"" + subject + "\""); // Mark the listeners who need this event int subjectAndListenersSize = subjectsAndListenersForCurrentEvent.size(); // was i = lastNotified + 1 for(int i = 0; i < subjectAndListenersSize; i++) { SubjectAndListener subjectAndListener = subjectsAndListenersForCurrentEvent.get(i); if(subjectAndListener.subject != subject) continue; if(i < nextToNotify) nextToNotify = i; subjectAndListener.addPendingEvent(event); } // If this method is reentrant, let someone higher up the stack handle this if(reentrantFireEventCount != 1) return; // remember any runtime exceptions thrown to rethrow later RuntimeException toRethrow = null; // fire events to listeners in order while(true) { SubjectAndListener nextToFire = null; // find the next listener still pending for(int i = nextToNotify; i < subjectAndListenersSize; i++) { SubjectAndListener subjectAndListener = subjectsAndListenersForCurrentEvent.get(i); if(subjectAndListener.hasPendingEvent()) { nextToFire = subjectAndListener; nextToNotify = i + 1; break; } } // there's nobody to notify, we're done firing events if(nextToFire == null) break; // notify this listener try { nextToFire.firePendingEvent(); } catch(RuntimeException e) { if(toRethrow == null) toRethrow = e; } } // clean up all the subjects now that we're done firing events for(Iterator> i = subjectsToCleanUp.entrySet().iterator(); i.hasNext(); ) { Map.Entry subjectAndEventFormat = i.next(); try { subjectAndEventFormat.getValue().postEvent(subjectAndEventFormat.getKey()); } catch(RuntimeException e) { if(toRethrow == null) toRethrow = e; } } subjectsToCleanUp.clear(); // this event is completely finished subjectsAndListenersForCurrentEvent = null; // rethrow any exceptions if(toRethrow != null) throw toRethrow; } finally { reentrantFireEventCount--; } } /** * Adapt any observer-style interface to a common format. */ public interface EventFormat { /** * Fire the specified event to the specified listener. */ void fire(Subject subject, Event event, Listener listener); /** * A callback made only after all listeners of the specified subject * have been notified of the specified event. This can be used as * a hook to clean up temporary datastructures for that event. */ void postEvent(Subject subject); /** * Whether the listener is still valid. Usually a listener becomes stale * when a weak reference goes out of scope. If this method returns true, * the listener will be silently removed and no longer receive events. */ boolean isStale(Subject subject, Listener listener); } /** * An EventFormat used to specify explicit dependencies, but that doesn't * actually fire events. * * @see {@link ListEventPublisher#setRelatedListener} */ private static class NoOpEventFormat implements SequenceDependenciesEventPublisher.EventFormat { public static final SequenceDependenciesEventPublisher.EventFormat INSTANCE = new NoOpEventFormat(); public void fire(Object subject, Object event, Object listener) { throw new UnsupportedOperationException(); } public void postEvent(Object subject) { throw new UnsupportedOperationException(); } public boolean isStale(Object subject, Object listener) { return false; } } /** * Manage a subject/listener pair, plus a possible event that is queued to * be fired to the listener from the subject. */ private static class SubjectAndListener { private final Subject subject; private final Listener listener; private final EventFormat eventFormat; private Event pendingEvent; public SubjectAndListener(Subject subject, Listener listener, EventFormat eventFormat) { this.subject = subject; this.listener = listener; this.eventFormat = eventFormat; } public boolean hasPendingEvent() { return pendingEvent != null; } public void addPendingEvent(Event pendingEvent) { if(this.pendingEvent != null) throw new IllegalStateException(); if(pendingEvent == null) throw new IllegalStateException(); this.pendingEvent = pendingEvent; } public void firePendingEvent() { assert(pendingEvent != null); try { eventFormat.fire(subject, pendingEvent, listener); } finally { pendingEvent = null; } } @Override public String toString() { String separator = hasPendingEvent() ? ">>>" : "-->"; return subject + separator + listener; } } }