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

ca.odell.glazedlists.ObservableElementList Maven / Gradle / Ivy

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

import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.impl.adt.Barcode;
import ca.odell.glazedlists.impl.adt.BarcodeIterator;

import java.util.ArrayList;
import java.util.EventListener;
import java.util.List;

/**
 * A list that fires update events whenever elements are modified in place.
 * Changes to list elements are detected by registering an appropriate listener
 * on every list element. Listeners are registered as elements are added to
 * this list and unregistered as elements are removed from this list. Users
 * must specify an implementation of a {@link Connector} in the constructor
 * which contains the necessary logic for registering and unregistering a
 * listener capable of detecting modifications to an observable list element.
 *
 * 

Warning: This class is * thread ready but not thread safe. See {@link EventList} for an example * of thread safe code. * *

* * * * * * * *
EventList Overview
Writable:yes
Concurrency:thread ready, not thread safe; elementChanged(), however, is thread ready
Performance:inserts: O(1), deletes: O(1), updates: O(1), elementChanged: O(n)
Memory:8 bytes per element
Unit Tests:ObservableElementListTest
Issues:N/A
* * @see GlazedLists#beanConnector(Class) * @see GlazedLists#beanConnector(Class, String, String) * @see RFE 157 * * @author Jesse Wilson * @author James Lemieux */ public class ObservableElementList extends TransformedList { /** * A list of the observed elements. It is necessary to track the observed * elements since list removals broadcast ListEvents which do not include * the removed element as part of the ListEvent. We use this list to locate * removed elements for the purpose of unregistering listeners from them. * todo remove this list when ListEvent can reliably furnish us with a deleted value */ private List observedElements; /** * The connector object containing the logic for registering and * unregistering a listener that detects changes within the observed * list elements and notifies this list of the change. The registered * listener is responsible for calling {@link #elementChanged(Object)} * to notify this list of the changed object. */ private Connector elementConnector = null; /** * true indicates a single shared EventListener is used for each * element being observed. Consequently, {@link #singleEventListenerRegistry} * is the compact data structure used to track which elements are being * listened to by the {@link #singleEventListener}. false * indicates {@link #multiEventListenerRegistry} is used to track each * individual EventListener installed on each individual list element. */ private boolean singleListenerMode = true; /** * A list which parallels {@link #observedElements}. It stores the unique * {@link EventListener} associated with the observed element at the same * index within {@link #observedElements}. */ private List multiEventListenerRegistry = null; /** * The single {@link EventListener} shared by all list elements if a * common listener is returned from the {@link Connector} of this list. */ private EventListener singleEventListener = null; /** * The compact data structure which identifies the observed elements that * have had the {@link #singleEventListener} registered on them. * {@link Barcode#BLACK} indicates the {@link #singleEventListener} has * been registered on the element at the index; {@link Barcode#WHITE} * indicates no listener was registered on the element at the index. */ private Barcode singleEventListenerRegistry = null; /** * Constructs an ObservableElementList which wraps the given * source and uses the given elementConnector to * register/unregister change listeners on elements of the * source. * * @param source the {@link EventList} to transform * @param elementConnector the {@link Connector} to consult when list * elements are added or removed and thus element listeners must be * registered or unregistered. Note that this constructor attachs * this list to the given elementConnector by calling * {@link Connector#setObservableElementList(ObservableElementList)}. */ public ObservableElementList(EventList source, Connector elementConnector) { super(source); this.elementConnector = elementConnector; // attach this list to the element connector so the listeners know // which List to notify of their modifications this.elementConnector.setObservableElementList(this); // for speed, we add all source elements together, rather than individually this.observedElements = new ArrayList(source); // we initialize the single EventListener registry, as we optimistically // assume we'll be using a single listener for all observed elements this.singleEventListenerRegistry = new Barcode(); this.singleEventListenerRegistry.addWhite(0, source.size()); // add listeners to all source list elements for (int i = 0, n = size(); i < n; i++) { // connect a listener to the element final EventListener listener = this.connectElement(get(i)); // record the listener in the registry this.registerListener(i, listener, false); } // begin listening to the source list source.addListEventListener(this); } @Override public void listChanged(ListEvent listChanges) { if (this.observedElements == null) throw new IllegalStateException("This list has been disposed and can no longer be used."); // add listeners to inserted list elements and remove listeners from deleted elements while(listChanges.next()) { final int changeIndex = listChanges.getIndex(); final int changeType = listChanges.getType(); // register a listener on the inserted object if (changeType == ListEvent.INSERT) { final E inserted = get(changeIndex); this.observedElements.add(changeIndex, inserted); // connect a listener to the freshly inserted element final EventListener listener = this.connectElement(inserted); // record the listener in the registry this.registerListener(changeIndex, listener, false); // unregister a listener on the deleted object } else if (changeType == ListEvent.DELETE) { // try to get the previous value through the ListEvent E deleted = listChanges.getOldValue(); E deletedElementFromPrivateCopy = this.observedElements.remove(changeIndex); // if the ListEvent could give us the previous value, use the value from our private copy of the source if (deleted == ListEvent.UNKNOWN_VALUE) deleted = deletedElementFromPrivateCopy; // remove the listener from the registry final EventListener listener = this.unregisterListener(changeIndex); // disconnect the listener from the freshly deleted element this.disconnectElement(deleted, listener); // register/unregister listeners if the value at the changeIndex is now a different object } else if (changeType == ListEvent.UPDATE) { E previousValue = listChanges.getOldValue(); // if the ListEvent could give us the previous value, use the value from our private copy of the source if (previousValue == ListEvent.UNKNOWN_VALUE) previousValue = this.observedElements.get(changeIndex); final E newValue = get(changeIndex); // if a different object is present at the index if (newValue != previousValue) { this.observedElements.set(changeIndex, newValue); // disconnect the listener from the previous element at the index this.disconnectElement(previousValue, this.getListener(changeIndex)); // connect the listener to the new element at the index final EventListener listener = this.connectElement(newValue); // replace the old listener in the registry with the new listener for the new element this.registerListener(changeIndex, listener, true); } } } listChanges.reset(); this.updates.forwardEvent(listChanges); } /** * A convenience method for adding a listener into the appropriate listener * registry. The listener will be registered at the specified * index and will be added if replace is true * or will replace any existing listener at the index if * replace is false. * * @param index the index of the observed element the listener * is attached to * @param listener the {@link EventListener} registered to the observed * element at the given index * @param replace true indicates the listener should be replaced * at the given index; false indicates it should be added */ private void registerListener(int index, EventListener listener, boolean replace) { if (replace) { // if replace is false, we should call set() on the appropriate registry if (this.singleListenerMode) this.singleEventListenerRegistry.set(index, listener == null ? Barcode.WHITE : Barcode.BLACK, 1); else this.multiEventListenerRegistry.set(index, listener); } else { // if replace is true, we should call replace() on the appropriate registry if (this.singleListenerMode) this.singleEventListenerRegistry.add(index, listener == null ? Barcode.WHITE : Barcode.BLACK, 1); else this.multiEventListenerRegistry.add(index, listener); } } /** * Returns the {@link EventListener} at the given index. * * @param index the location of the {@link EventListener} to be returned * @return the {@link EventListener} at the given index */ private EventListener getListener(int index) { EventListener listener = null; if (this.singleListenerMode) { if (this.singleEventListenerRegistry.get(index) == Barcode.BLACK) listener = this.singleEventListener; } else { listener = this.multiEventListenerRegistry.get(index); } return listener; } /** * A convenience method for removing a listener at the specified * index from the appropriate listener registry. * * @param index the index of the {@link EventListener} to be unregistered * @return the EventListener that was unregistered or null if * no EventListener existed at the given index */ private EventListener unregisterListener(int index) { EventListener listener = null; if (this.singleListenerMode) { if (this.singleEventListenerRegistry.get(index) == Barcode.BLACK) listener = this.singleEventListener; this.singleEventListenerRegistry.remove(index, 1); } else { listener = this.multiEventListenerRegistry.remove(index); } return listener; } /** * A convenience method to connect listeners to the given * listElement. * * @param listElement the list element to connect change listeners to * @return the listener that was connected to the listElement * or null if no listener was registered * @throws IllegalStateException if this list has been disposed and is * thus no longer in a state to be managing listener registrations on * list elements */ private EventListener connectElement(E listElement) { // listeners cannot be installed on null listElements if (listElement == null) return null; // use the elementConnector to install a listener on the listElement final EventListener listener = this.elementConnector.installListener(listElement); // test if the new listener transfers us from single event mode to multi event mode if (this.singleListenerMode && listener != null) { if (this.singleEventListener == null) this.singleEventListener = listener; else if (listener != this.singleEventListener) this.switchToMultiListenerMode(); } return listener; } /** * A convenience method to disconnect the listener from the * given listElement. * * @param listElement the list element to disconnect the * listener from * @throws IllegalStateException if this list has been disposed and is * thus no longer in a state to be managing listener registrations on * list elements */ private void disconnectElement(E listElement, EventListener listener) { if (listElement != null && listener != null) this.elementConnector.uninstallListener(listElement, listener); } /** * This method converts the data structures which are optimized for storing * a single instance of an EventListener shared amongst all observed * elements into data structures which are appropriate for storing * individual instances of EventListeners for each observed element. * *

Note: this is a one-time switch only and cannot be reversed */ private void switchToMultiListenerMode() { if (!this.singleListenerMode) throw new IllegalStateException(); // build a new data structure appropriate for storing individual // listeners for each observed element this.multiEventListenerRegistry = new ArrayList(this.source.size()); for (int i = 0; i < source.size(); i++) this.multiEventListenerRegistry.add(null); // for each black entry in the singleEventListenerRegistry create an // entry in the multiEventListenerRegistry at the corresponding index // for the singleEventListener for (BarcodeIterator iter = this.singleEventListenerRegistry.iterator(); iter.hasNextBlack();) { iter.nextBlack(); this.multiEventListenerRegistry.set(iter.getIndex(), this.singleEventListener); } // null out the reference to the single EventListener, // since we'll now track the EventListener for each element this.singleEventListener = null; // null out the reference to the single EventList registry, since we // are replacing its listener tracking mechanism with the multiEventListenerRegistry this.singleEventListenerRegistry = null; // indicate this list is no longer in single listener mode meaning we // no longer assume the same listener is installed on every element this.singleListenerMode = false; } @Override protected boolean isWritable() { return true; } /** * Releases the resources consumed by this {@link TransformedList} so that * it may eventually be garbage collected. * * In this case of this {@link TransformedList}, it uses the * {@link Connector} to remove all listeners from their associated list * elements and finally removes the reference to this list from the * Connector by calling * {@link Connector#setObservableElementList(ObservableElementList)} with a * null argument. * *

Warning: It is an error * to call any method on a {@link TransformedList} after it has been disposed. */ @Override public void dispose() { // remove all listeners from all list elements for (int i = 0, n = this.observedElements.size(); i < n; i++) { final E element = this.observedElements.get(i); final EventListener listener = this.getListener(i); this.disconnectElement(element, listener); } // clear out the reference to this list from the associated connector this.elementConnector.setObservableElementList(null); // null out all references to internal data structures this.observedElements = null; this.multiEventListenerRegistry = null; this.singleEventListener = null; this.singleEventListenerRegistry = null; this.elementConnector = null; super.dispose(); } /** * Handle a listener being notified for the specified listElement. * This method causes a ListEvent to be fired from this EventList indicating * an update occurred at all locations of the given listElement. * *

Note that listElement must be the exact object located within this list * (i.e. listElement == get(i) for some i >= 0). * *

This method acquires the write lock for this list before locating the * listElement and broadcasting its update. It is assumed that * this method may be called on any Thread, so to decrease the burdens of * the caller in achieving multi-threaded correctness, this method is * Thread ready. * * @param listElement the list element which has been modified */ public void elementChanged(Object listElement) { if (this.observedElements == null) throw new IllegalStateException("This list has been disposed and can no longer be used."); getReadWriteLock().writeLock().lock(); try { this.updates.beginEvent(); // locate all indexes containing the given listElement for (int i = 0, n = size(); i < n; i++) { final E currentElement = get(i); if (listElement == currentElement) { this.updates.elementUpdated(i, currentElement); } } this.updates.commitEvent(); } finally { getReadWriteLock().writeLock().unlock(); } } /** * An interface defining the methods required for registering and * unregistering change listeners on list elements within an * {@link ObservableElementList}. Implementations typically install a * single listener, such as a {@link java.beans.PropertyChangeListener} on * list elements to detect changes in the state of the element. The * installed listener implementation in turn calls * {@link ObservableElementList#elementChanged(Object)} in order to have * the list broadcast an update at the index of the object. */ public interface Connector { /** * Start listening for events from the specified element. * Alternatively, if the element does not require a * listener to be attached to it (e.g. the element is * immutable), null may be returned to signal that no * listener was installed. * * @param element the element to be observed * @return the listener that was installed on the element * to be used as a parameter to {@link #uninstallListener(Object, EventListener)}. * null is taken to mean no listener was installed * and thus {@link #uninstallListener(Object, EventListener)} need * not be called. */ public EventListener installListener(E element); /** * Stop listening for events from the specified element. * * @param element the element to be observed * @param listener the listener as returned by {@link #installListener(Object)}. */ public void uninstallListener(E element, EventListener listener); /** * Sets the {@link ObservableElementList} to notify when changes occur * on elements. * * @param list the ObservableElementList containing the elements to * observe */ public void setObservableElementList(ObservableElementList list); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy