ca.odell.glazedlists.matchers.ThreadedMatcherEditor Maven / Gradle / Ivy
Show all versions of glazedlists_java15 Show documentation
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.matchers;
import java.util.*;
/**
* A MatcherEditor which decorates a source MatcherEditor with functionality.
* Specifically, this MatcherEditor is meant to act as a buffer for
* MatcherEvents to smooth out disparities between the rate at which
* MatcherEvents are produced by the source MatcherEditor and the rate at
* which they are consumed by registered MatcherEditorListeners.
*
* Internally, a {@link ThreadedMatcherEditor} enqueues MatcherEvents as they
* they are received from the source MatcherEditor. The MatcherEvents on the
* queue are fired by another Thread as rapidly as the MatcherEditorListeners
* can consume them. Two methods exist on this class which enable subclasses
* to customize aspects of processing queued MatcherEvents:
*
*
* - {@link #executeMatcherEventQueueRunnable(Runnable)} is consulted when
* a Thread must be selected to execute the given Runnable which will
* drain the queue of MatcherEvents. Subclasses may override to
* customize which Thread is used.
*
*
- {@link #coalesceMatcherEvents(List)} is used to compress
* many enqueued MatcherEvents into a single representative
* MatcherEvent. This implies a contract between all registered
* MatcherEditorListeners and this {@link ThreadedMatcherEditor} that
* guarantees that processing the coalesced MatcherEvent is equivalent
* to processing all MatcherEvents sequentially.
*
*
* Typical usage patterns of ThreadedMatcherEditor resemble:
*
*
* MatcherEditor threadedMatcherEditor = new ThreadedMatcherEditor(new AnyMatcherEditor());
* FilterList filterList = new FilterList(new BasicEventList(), threadedMatcherEditor);
*
*
* @author James Lemieux
*/
public class ThreadedMatcherEditor extends AbstractMatcherEditor {
/** The underlying MatcherEditor whose MatcherEvents are being queued and fired on an alternate Thread. */
private final MatcherEditor source;
/**
* The LinkedList acting as a queue of MatcherEditor.Event in the order in which they are received
* from {@link #source}. We take great care to ensure that the queue's monitor is held before it
* is queried or mutated.
*/
private final List> matcherEventQueue = new LinkedList>();
/**
* The MatcherEditorListener which reacts to MatcherEvents from the {@link #source}
* by enqueuing them for firing on another Thread at some later time.
*/
private MatcherEditor.Listener queuingMatcherEditorListener = new QueuingMatcherEditorListener();
/**
* true indicates a Thread is currently executing the
* {@link #drainMatcherEventQueueRunnable} to drain the {@link #matcherEventQueue}.
*/
private boolean isDrainingQueue = false;
/**
* The {@link Runnable} containing the logic to drain the queue of MatcherEvents until it is empty.
* The Runnable is executed on a Thread using {@link #executeMatcherEventQueueRunnable(Runnable)}.
*/
private Runnable drainMatcherEventQueueRunnable = new DrainMatcherEventQueueRunnable();
/**
* Creates a ThreadedMatcherEditor which wraps the given source
.
* MatcherEvents fired from the source
will be enqueued within
* this MatcherEditor until they are processed on an alternate Thread.
*
* @param source the MatcherEditor to wrap with buffering functionality
* @throws NullPointerException if source
is null
*/
public ThreadedMatcherEditor(MatcherEditor source) {
if (source == null)
throw new NullPointerException("source may not be null");
this.source = source;
this.source.addMatcherEditorListener(this.queuingMatcherEditorListener);
}
/**
* Returns the current Matcher specified by the source {@link MatcherEditor}.
*
* @return the current Matcher specified by the source {@link MatcherEditor}
*/
public Matcher getMatcher() {
return this.source.getMatcher();
}
/**
* This method implements the strategy for coalescing many queued
* MatcherEvents into a single representative MatcherEvent. Listeners which
* process the MatcherEvent returned from this method should match the state
* that would exist if each of the matcherEvents
were fired
* sequentially. In general, any group of matcherEvents
can be
* succesfully coalesced as a single MatcherEvent with a type of
* changed
, however, this method's default implementation
* uses a few heuristics to do more intelligent coalescing in order to
* gain speed improvements:
*
*
* - if
matcherEvents
ends in a MatcherEvent which is a
* {@link MatcherEditor.Event#MATCH_ALL} or {@link MatcherEditor.Event#MATCH_NONE}
* type, the last MatcherEvent is returned, regardless of previous
* MatcherEvents
*
*
- if
matcherEvents
only contains a series of
* monotonically constraining MatcherEvents, the final MatcherEvent
* is returned
*
*
- if
matcherEvents
only contains a series of
* monotonically relaxing MatcherEvents, the final MatcherEvent is
* returned
*
*
- if
matcherEvents
contains both constraining and
* relaxing MatcherEvents, the final MatcherEvent is returned with
* its type as {@link MatcherEditor.Event#CHANGED}
*
*
* Note that 1, 2,
and 3
above merely represent
* safe optimizations of the type of MatcherEvent that can be returned.
* It could also have been returned as a MatcherEvent with a type of
* {@link MatcherEditor.Event#CHANGED} and be assumed to work correctly, though
* potentially less efficiently, since it is a more generic type of change.
*
*
* Subclasses with the ability to fire precise MatcherEvents with fine grain
* types (i.e. relaxed
or constrained
) when
* coalescing matcherEvents
in situations not recounted above
* may do so by overiding this method.
*
* @param matcherEvents an array of MatcherEvents recorded in the order
* they were received from the source MatcherEditor
* @return a single MatcherEvent which, when fired, will result in the
* same state as if all matcherEvents
had been fired
* sequentially
*/
protected Event coalesceMatcherEvents(List> matcherEvents) {
boolean changeType = false;
// fetch the last matcher event - it is the basis of the MatcherEvent which must be returned
// all that remains is to determine the type of the MatcherEvent to return
final Event lastMatcherEvent = matcherEvents.get(matcherEvents.size()-1);
final int lastMatcherEventType = lastMatcherEvent.getType();
// if the last MatcherEvent is a MATCH_ALL or MATCH_NONE type, we can safely return it immediately
if (lastMatcherEventType != Event.MATCH_ALL && lastMatcherEventType != Event.MATCH_NONE) {
// otherwise determine if any constraining and/or relaxing MatcherEvents exist
boolean constrained = false;
boolean relaxed = false;
for (Iterator> i = matcherEvents.iterator(); i.hasNext(); ) {
switch (i.next().getType()) {
case Event.MATCH_ALL: relaxed = true; break;
case Event.MATCH_NONE: constrained = true; break;
case Event.RELAXED: relaxed = true; break;
case Event.CONSTRAINED: constrained = true; break;
case Event.CHANGED: constrained = relaxed = true; break;
}
}
changeType = constrained && relaxed;
}
// if both constraining and relaxing MatcherEvents exist, ensure we must return a CHANGED MatcherEvent
// otherwise the last MatcherEvent must represent the coalesced MatcherEvent
return new MatcherEditor.Event(this, changeType ? Event.CHANGED : lastMatcherEventType, lastMatcherEvent.getMatcher());
}
/**
* This method executes the given runnable
on a Thread. The
* particular Thread chosen to execute the Runnable is left as an
* implementation detail. By default, a new Thread named
* MatcherQueueThread
is constructed to execute the
* runnable
each time this method is called. Subclasses may
* override this method to use any Thread selection strategy they wish.
*
* @param runnable a Runnable to execute on an alternate Thread
*/
protected void executeMatcherEventQueueRunnable(Runnable runnable) {
new Thread(runnable, "MatcherQueueThread").start();
}
/**
* This MatcherEditorListener enqueues each MatcherEvent it receives in the
* order it is received and then schedules a Runnable to drain the queue of
* MatcherEvents as soon as possible.
*/
private class QueuingMatcherEditorListener implements MatcherEditor.Listener {
public void changedMatcher(Event matcherEvent) {
synchronized(matcherEventQueue) {
matcherEventQueue.add(matcherEvent);
// if necessary, start a Thread to drain the queue
if (!isDrainingQueue) {
isDrainingQueue = true;
executeMatcherEventQueueRunnable(drainMatcherEventQueueRunnable);
}
}
}
}
/**
* This Runnable contains logic which continues to process batches of
* MatcherEvents from the matcherEventQueue until the queue is empty. Each
* batch of MatcherEvents includes all MatcherEvents available at the time
* the queue is inspected. The MatcherEvents are then coalesced and the
* resulting singular MatcherEvent is fired to MatcherEditorListeners
* attached to this ThreadedMatcherEditor on a different Thread. When the
* fire method returns, the queue is drained again if it has accumulated
* MatcherEvents otherwise the DrainMatcherEventQueueRunnable exits.
*/
private class DrainMatcherEventQueueRunnable implements Runnable {
public void run() {
while (true) {
// acquire the monitor that guards assigning the drainMatcherEventQueueRunnable
// to a processing Thread as well as exiting the drainMatcherEventQueueRunnable
final Event matcherEvent;
synchronized (matcherEventQueue) {
// if no work exists in the queue, exit the Runnable
if (matcherEventQueue.isEmpty()) {
// no matter the circumstance for us exiting the Runnable,
// ensure we indicate we are no longer draining the queue
isDrainingQueue = false;
return;
}
// fetch a copy of all MatcherEvents currently in the queue
matcherEvent = coalesceMatcherEvents(matcherEventQueue);
matcherEventQueue.clear();
}
try {
// coalesce all of the current MatcherEvents to a single representative MatcherEvent
// and fire the single coalesced MatcherEvent
fireChangedMatcher(matcherEvent);
} catch(Error e) {
synchronized(matcherEventQueue) { isDrainingQueue = false; }
throw e;
} catch(RuntimeException e) {
synchronized(matcherEventQueue) { isDrainingQueue = false; }
throw e;
}
}
}
}
}