| /******************************************************************************* |
| * Copyright (c) 2007 Oracle. All rights reserved. |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v1.0, which accompanies this distribution |
| * and is available at http://www.eclipse.org/legal/epl-v10.html. |
| * |
| * Contributors: |
| * Oracle - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.jpt.utility.internal.model.value; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.EventObject; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.ListIterator; |
| |
| import org.eclipse.jpt.utility.internal.Counter; |
| import org.eclipse.jpt.utility.internal.model.Model; |
| import org.eclipse.jpt.utility.internal.model.event.ListChangeEvent; |
| |
| /** |
| * Abstract list value model that provides behavior for wrapping a list value |
| * model (or collection value model) and listening for changes to aspects of the |
| * *items* held by the list (or collection). Changes to the actual list |
| * (or collection) are also monitored. |
| * |
| * This is useful if you have a collection of items that can be modified by adding |
| * or removing items or the items themselves might change in a fashion that |
| * might change the collection's external appearance. |
| * |
| * Subclasses need to override two methods: |
| * |
| * listenToItem(Model) |
| * begin listening to the appropriate aspect of the specified item and call |
| * #itemAspectChanged(Object) whenever the aspect changes |
| * |
| * stopListeningToItem(Model) |
| * stop listening to the appropriate aspect of the specified item |
| */ |
| public abstract class ItemAspectListValueModelAdapter |
| extends ListValueModelWrapper |
| { |
| |
| /** |
| * Maintain a counter for each of the items in the |
| * wrapped list holder we are listening to. |
| */ |
| protected final IdentityHashMap counters; |
| |
| |
| // ********** constructors ********** |
| |
| /** |
| * Constructor - the list holder is required. |
| */ |
| protected ItemAspectListValueModelAdapter(ListValueModel listHolder) { |
| super(listHolder); |
| this.counters = new IdentityHashMap(); |
| } |
| |
| /** |
| * Constructor - the collection holder is required. |
| */ |
| protected ItemAspectListValueModelAdapter(CollectionValueModel collectionHolder) { |
| this(new CollectionListValueModelAdapter(collectionHolder)); |
| } |
| |
| |
| // ********** ListValueModel implementation ********** |
| |
| public ListIterator values() { |
| return this.listHolder.values(); |
| } |
| |
| public void add(int index, Object item) { |
| this.listHolder.add(index, item); |
| } |
| |
| public void addAll(int index, List items) { |
| this.listHolder.addAll(index, items); |
| } |
| |
| public Object remove(int index) { |
| return this.listHolder.remove(index); |
| } |
| |
| public List remove(int index, int length) { |
| return this.listHolder.remove(index, length); |
| } |
| |
| public Object replace(int index, Object item) { |
| return this.listHolder.replace(index, item); |
| } |
| |
| public List replaceAll(int index, List items) { |
| return this.listHolder.replaceAll(index, items); |
| } |
| |
| public Object get(int index) { |
| return this.listHolder.get(index); |
| } |
| |
| public int size() { |
| return this.listHolder.size(); |
| } |
| |
| |
| // ********** behavior ********** |
| |
| /** |
| * Start listening to the list holder and the items in the list. |
| */ |
| @Override |
| protected void engageModel() { |
| super.engageModel(); |
| this.engageAllItems(); |
| } |
| |
| protected void engageAllItems() { |
| this.engageItems((ListIterator) this.listHolder.values()); |
| } |
| |
| protected void engageItems(Iterator stream) { |
| while (stream.hasNext()) { |
| this.engageItem(stream.next()); |
| } |
| } |
| |
| protected void engageItem(Object item) { |
| // listen to an item only once |
| Counter counter = (Counter) this.counters.get(item); |
| if (counter == null) { |
| counter = new Counter(); |
| this.counters.put(item, counter); |
| this.startListeningToItem((Model) item); |
| } |
| counter.increment(); |
| } |
| |
| /** |
| * Start listening to the specified item. |
| */ |
| protected abstract void startListeningToItem(Model item); |
| |
| /** |
| * Stop listening to the list holder and the items in the list. |
| */ |
| @Override |
| protected void disengageModel() { |
| this.disengageAllItems(); |
| super.disengageModel(); |
| } |
| |
| protected void disengageAllItems() { |
| this.disengageItems((ListIterator) this.listHolder.values()); |
| } |
| |
| protected void disengageItems(Iterator stream) { |
| while (stream.hasNext()) { |
| this.disengageItem(stream.next()); |
| } |
| } |
| |
| protected void disengageItem(Object item) { |
| // stop listening to an item only once |
| Counter counter = (Counter) this.counters.get(item); |
| if (counter == null) { |
| // something is wrong if this happens... ~bjv |
| throw new IllegalStateException("missing counter: " + item); |
| } |
| if (counter.decrement() == 0) { |
| this.counters.remove(item); |
| this.stopListeningToItem((Model) item); |
| } |
| } |
| |
| /** |
| * Stop listening to the specified item. |
| */ |
| protected abstract void stopListeningToItem(Model item); |
| |
| |
| // ********** list change support ********** |
| |
| /** |
| * Items were added to the wrapped list holder. |
| * Forward the event and begin listening to the added items. |
| */ |
| @Override |
| protected void itemsAdded(ListChangeEvent e) { |
| // re-fire event with the wrapper as the source |
| this.fireItemsAdded(e.cloneWithSource(this, LIST_VALUES)); |
| this.engageItems(e.items()); |
| } |
| |
| /** |
| * Items were removed from the wrapped list holder. |
| * Stop listening to the removed items and forward the event. |
| */ |
| @Override |
| protected void itemsRemoved(ListChangeEvent e) { |
| this.disengageItems(e.items()); |
| // re-fire event with the wrapper as the source |
| this.fireItemsRemoved(e.cloneWithSource(this, LIST_VALUES)); |
| } |
| |
| /** |
| * Items were replaced in the wrapped list holder. |
| * Stop listening to the removed items, forward the event, |
| * and begin listening to the added items. |
| */ |
| @Override |
| protected void itemsReplaced(ListChangeEvent e) { |
| this.disengageItems(e.replacedItems()); |
| // re-fire event with the wrapper as the source |
| this.fireItemsReplaced(e.cloneWithSource(this, LIST_VALUES)); |
| this.engageItems(e.items()); |
| } |
| |
| /** |
| * Items were moved in the wrapped list holder. |
| * No need to change any listeners; just forward the event. |
| */ |
| @Override |
| protected void itemsMoved(ListChangeEvent e) { |
| // re-fire event with the wrapper as the source |
| this.fireItemsMoved(e.cloneWithSource(this, LIST_VALUES)); |
| } |
| |
| /** |
| * The wrapped list holder was cleared. |
| * Stop listening to the removed items and forward the event. |
| */ |
| @Override |
| protected void listCleared(ListChangeEvent e) { |
| // we should only need to disengage each item once... |
| // make a copy to prevent a ConcurrentModificationException |
| Collection keys = new ArrayList(this.counters.keySet()); |
| this.disengageItems(keys.iterator()); |
| this.counters.clear(); |
| // re-fire event with the wrapper as the source |
| this.fireListCleared(LIST_VALUES); |
| } |
| |
| /** |
| * The wrapped list holder has changed in some dramatic fashion. |
| * Reconfigure our listeners and forward the event. |
| */ |
| @Override |
| protected void listChanged(ListChangeEvent e) { |
| // we should only need to disengage each item once... |
| // make a copy to prevent a ConcurrentModificationException |
| Collection keys = new ArrayList(this.counters.keySet()); |
| this.disengageItems(keys.iterator()); |
| this.counters.clear(); |
| // re-fire event with the wrapper as the source |
| this.fireListChanged(LIST_VALUES); |
| this.engageAllItems(); |
| } |
| |
| |
| // ********** item change support ********** |
| |
| /** |
| * The specified item has a bound property that has changed. |
| * Notify listeners of the change. |
| */ |
| protected void itemAspectChanged(EventObject e) { |
| Object item = e.getSource(); |
| int index = this.lastIdentityIndexOf(item); |
| while (index != -1) { |
| this.itemAspectChanged(index, item); |
| index = this.lastIdentityIndexOf(item, index); |
| } |
| } |
| |
| /** |
| * The specified item has a bound property that has changed. |
| * Notify listeners of the change. |
| */ |
| protected void itemAspectChanged(int index, Object item) { |
| this.fireItemReplaced(LIST_VALUES, index, item, item); // hmmm... |
| } |
| |
| /** |
| * Return the last index of the specified item, using object |
| * identity instead of equality. |
| */ |
| protected int lastIdentityIndexOf(Object o) { |
| return this.lastIdentityIndexOf(o, this.listHolder.size()); |
| } |
| |
| /** |
| * Return the last index of the specified item, starting just before the |
| * the specified endpoint, and using object identity instead of equality. |
| */ |
| protected int lastIdentityIndexOf(Object o, int end) { |
| for (int i = end; i-- > 0; ) { |
| if (this.listHolder.get(i) == o) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| } |