File: ObservableElementList.java

package info (click to toggle)
libglazedlists-java 1.9.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 3,024 kB
  • ctags: 4,252
  • sloc: java: 22,561; xml: 818; sh: 51; makefile: 5
file content (480 lines) | stat: -rw-r--r-- 21,959 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
/* 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.
 *
 * <p><strong><font color="#FF0000">Warning:</font></strong> This class is
 * thread ready but not thread safe. See {@link EventList} for an example
 * of thread safe code.
 *
 * <p><table border="1" width="100%" cellpadding="3" cellspacing="0">
 * <tr class="TableHeadingColor"><td colspan=2><font size="+2"><b>EventList Overview</b></font></td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Writable:</b></td><td>yes</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Concurrency:</b></td><td>thread ready, not thread safe; elementChanged(), however, is thread ready</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Performance:</b></td><td>inserts: O(1), deletes: O(1), updates: O(1), elementChanged: O(n)</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Memory:</b></td><td>8 bytes per element</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Unit Tests:</b></td><td>ObservableElementListTest</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Issues:</b></td><td>N/A</td></tr>
 * </table>
 *
 * @see GlazedLists#beanConnector(Class)
 * @see GlazedLists#beanConnector(Class, String, String)
 * @see <a href="https://glazedlists.dev.java.net/issues/show_bug.cgi?id=157">RFE 157</a>
 *
 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
 * @author James Lemieux
 */
public class ObservableElementList<E> extends TransformedList<E, E> {

    /**
     * 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<E> 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<? super E> elementConnector = null;

    /**
     * <tt>true</tt> 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}. <tt>false</tt>
     * 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<EventListener> 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 <code>ObservableElementList</code> which wraps the given
     * <code>source</code> and uses the given <code>elementConnector</code> to
     * register/unregister change listeners on elements of the
     * <code>source</code>.
     *
     * @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 <code>elementConnector</code> by calling
     *      {@link Connector#setObservableElementList(ObservableElementList)}.
     */
    public ObservableElementList(EventList<E> source, Connector<? super E> 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<E>(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<E> 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 <code>listener</code> will be registered at the specified
     * <code>index</code> and will be added if <code>replace</code> is <tt>true</tt>
     * or will replace any existing listener at the <code>index</code> if
     * <code>replace</code> is <tt>false</tt>.
     *
     * @param index the index of the observed element the <code>listener</code>
     *      is attached to
     * @param listener the {@link EventListener} registered to the observed
     *      element at the given <code>index</code>
     * @param replace <tt>true</tt> indicates the listener should be replaced
     *      at the given index; <tt>false</tt> 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 <code>index</code>.
     *
     * @param index the location of the {@link EventListener} to be returned
     * @return the {@link EventListener} at the given <code>index</code>
     */
    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
     * <code>index</code> from the appropriate listener registry.
     *
     * @param index the index of the {@link EventListener} to be unregistered
     * @return the EventListener that was unregistered or <code>null</code> if
     *      no EventListener existed at the given <code>index</code>
     */
    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
     * <code>listElement</code>.
     *
     * @param listElement the list element to connect change listeners to
     * @return the listener that was connected to the <code>listElement</code>
     *      or <code>null</code> 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 <code>listener</code> from the
     * given <code>listElement</code>.
     *
     * @param listElement the list element to disconnect the
     *      <code>listener</code> 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.
     *
     * <p>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<EventListener>(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
     * <code>null</code> argument.
     *
     * <p><strong><font color="#FF0000">Warning:</font></strong> 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 <code>listElement</code>.
     * This method causes a ListEvent to be fired from this EventList indicating
     * an update occurred at all locations of the given <code>listElement</code>.
     *
     * <p>Note that listElement must be the exact object located within this list
     * (i.e. <code>listElement == get(i) for some i >= 0</code>).
     *
     * <p>This method acquires the write lock for this list before locating the
     * <code>listElement</code> 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<E> {
        /**
         * Start listening for events from the specified <code>element</code>.
         * Alternatively, if the <code>element</code> does not require a
         * listener to be attached to it (e.g. the <code>element</code> is
         * immutable), <code>null</code> 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 <code>element</code>
         *      to be used as a parameter to {@link #uninstallListener(Object, EventListener)}.
         *      <code>null</code> 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 <code>element</code>.
         *
         * @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<? extends E> list);
    }
}