blob: 54a5a001870817f95cd7403877c5241e5abcbcf2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2015 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.common.utility.internal.model.value;
import java.util.ArrayList;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jpt.common.utility.internal.StringBuilderTools;
import org.eclipse.jpt.common.utility.internal.collection.CollectionTools;
import org.eclipse.jpt.common.utility.internal.collection.NullList;
import org.eclipse.jpt.common.utility.internal.iterable.IterableTools;
import org.eclipse.jpt.common.utility.internal.transformer.TransformerTools;
import org.eclipse.jpt.common.utility.model.event.CollectionAddEvent;
import org.eclipse.jpt.common.utility.model.event.CollectionChangeEvent;
import org.eclipse.jpt.common.utility.model.event.CollectionClearEvent;
import org.eclipse.jpt.common.utility.model.event.CollectionEvent;
import org.eclipse.jpt.common.utility.model.event.CollectionRemoveEvent;
import org.eclipse.jpt.common.utility.model.listener.CollectionChangeAdapter;
import org.eclipse.jpt.common.utility.model.listener.CollectionChangeListener;
import org.eclipse.jpt.common.utility.model.value.CollectionValueModel;
import org.eclipse.jpt.common.utility.model.value.ListValueModel;
import org.eclipse.jpt.common.utility.transformer.Transformer;
/**
* A <code>CompositeCollectionValueModel</code> wraps another
* {@link CollectionValueModel} and uses a {@link Transformer}
* to convert each item in the wrapped collection to yet another
* {@link CollectionValueModel}. This composite collection contains
* the combined items from all these component collections.
* <p>
* <strong>NB:</strong> The wrapped collection must be an "identity set" that does not
* contain the same item twice or this class will throw an exception.
* <p>
* Terminology:<ul>
* <li><em>sources</em> - the items in the wrapped collection value model; these
* are converted into component CVMs by the transformer
* <li><em>component CVMs</em> - the component collection value models that are combined
* by this composite collection value model
* <li><em>items</em> - the items held by the component CVMs
* </ul>
*
* @param <E1> the type of items held by the wrapped collection model;
* each of these is transformed (by the {@link #transformer}) into a
* collection model of <code>E2</code>s
* @param <E2> the type of items held by the composite collection model
*/
public class CompositeCollectionValueModel<E1, E2>
extends CollectionValueModelWrapper<E1>
implements CollectionValueModel<E2>
{
/**
* This is the (optional) user-supplied object that transforms
* the items in the wrapped collection to collection value models.
*/
private final Transformer<E1, ? extends CollectionValueModel<? extends E2>> transformer;
/**
* Cache of the component collection value models that
* were generated by the transformer; keyed by the item
* in the wrapped collection that was passed to the transformer.
*/
private final IdentityHashMap<E1, CollectionValueModel<? extends E2>> componentCVMs =
new IdentityHashMap<E1, CollectionValueModel<? extends E2>>();
/**
* Cache of the collections corresponding to the component
* collection value models above; keyed by the component
* collection value models.
* Use {@link ArrayList}s so we can use {@link ArrayList}-specific methods
* (e.g. {@link ArrayList#clone()} and {@link ArrayList#ensureCapacity(int)}).
*/
private final IdentityHashMap<CollectionValueModel<? extends E2>, ArrayList<E2>> collections =
new IdentityHashMap<CollectionValueModel<? extends E2>, ArrayList<E2>>();
/** Listener that listens to all the component collection value models. */
private final CollectionChangeListener componentCVMListener;
/** Cache the size of the composite collection. */
private int size;
// ********** constructors **********
/**
* Construct a collection value model with the specified wrapped
* list value model. The specified list model already contains other
* collection value models.
*/
public static <E1 extends CollectionValueModel<? extends E2>, E2> CompositeCollectionValueModel<E1, E2> forModels(ListValueModel<E1> listModel) {
return forModels(new ListCollectionValueModelAdapter<E1>(listModel));
}
/**
* Construct a collection value model with the specified wrapped collection.
*/
public static <E1 extends CollectionValueModel<? extends E2>, E2> CompositeCollectionValueModel<E1, E2> forModels(Collection<E1> collection) {
return forModels(new StaticCollectionValueModel<E1>(collection));
}
/**
* Construct a collection value model with the specified wrapped collection.
*/
public static <E1 extends CollectionValueModel<? extends E2>, E2> CompositeCollectionValueModel<E1, E2> forModels(E1... collection) {
return forModels(new StaticCollectionValueModel<E1>(collection));
}
/**
* Construct a collection value model with the specified wrapped
* collection value model. The specified collection
* model already contains other collection value models.
*/
public static <E1 extends CollectionValueModel<? extends E2>, E2> CompositeCollectionValueModel<E1, E2> forModels(CollectionValueModel<E1> collectionModel) {
return new CompositeCollectionValueModel<E1, E2>(collectionModel, TransformerTools.<E1>passThruTransformer());
}
/**
* Construct a collection value model with the specified wrapped
* list value model and transformer.
*/
public CompositeCollectionValueModel(ListValueModel<? extends E1> listModel, Transformer<E1, ? extends CollectionValueModel<? extends E2>> transformer) {
this(new ListCollectionValueModelAdapter<E1>(listModel), transformer);
}
/**
* Construct a collection value model with the specified, unchanging, wrapped
* collection and transformer.
*/
public CompositeCollectionValueModel(Collection<? extends E1> collection, Transformer<E1, ? extends CollectionValueModel<? extends E2>> transformer) {
this(new StaticCollectionValueModel<E1>(collection), transformer);
}
/**
* Construct a collection value model with the specified, unchanging, wrapped
* collection and transformer.
*/
public CompositeCollectionValueModel(E1[] collection, Transformer<E1, ? extends CollectionValueModel<? extends E2>> transformer) {
this(new StaticCollectionValueModel<E1>(collection), transformer);
}
/**
* Construct a collection value model with the specified wrapped
* collection value model and transformer.
*/
public CompositeCollectionValueModel(CollectionValueModel<? extends E1> collectionModel, Transformer<E1, ? extends CollectionValueModel<? extends E2>> transformer) {
super(collectionModel);
if (transformer == null) {
throw new NullPointerException();
}
this.transformer = transformer;
this.componentCVMListener = this.buildComponentListener();
this.size = 0;
}
// ********** initialization **********
protected CollectionChangeListener buildComponentListener() {
return new ComponentListener();
}
protected class ComponentListener
extends CollectionChangeAdapter
{
@Override
public void itemsAdded(CollectionAddEvent event) {
CompositeCollectionValueModel.this.componentItemsAdded(event);
}
@Override
public void itemsRemoved(CollectionRemoveEvent event) {
CompositeCollectionValueModel.this.componentItemsRemoved(event);
}
@Override
public void collectionCleared(CollectionClearEvent event) {
CompositeCollectionValueModel.this.componentCollectionCleared(event);
}
@Override
public void collectionChanged(CollectionChangeEvent event) {
CompositeCollectionValueModel.this.componentCollectionChanged(event);
}
}
// ********** CollectionValueModel implementation **********
public Iterator<E2> iterator() {
return this.buildCompositeIterable().iterator();
}
protected Iterable<E2> buildCompositeIterable() {
return IterableTools.concatenate(this.collections.values());
}
public int size() {
return this.size;
}
// ********** CollectionValueModelWrapper overrides/implementation **********
@Override
protected void engageModel() {
super.engageModel();
// sync our cache *after* we start listening to the wrapped collection,
// since its value might change when a listener is added
this.addAllComponentSources();
}
/**
* Transform all the sources to collection value models
* and add their items to our cache, with no event notification.
*/
protected void addAllComponentSources() {
for (E1 source : this.collectionModel) {
this.addComponentSource(source, NullList.<E2>instance());
}
}
@Override
protected void disengageModel() {
super.disengageModel();
// stop listening to the components...
for (CollectionValueModel<? extends E2> componentCVM : this.componentCVMs.values()) {
componentCVM.removeCollectionChangeListener(VALUES, this.componentCVMListener);
}
// ...and clear the cache
this.componentCVMs.clear();
this.collections.clear();
this.size = 0;
}
/**
* Some component sources were added;
* add their corresponding items to our cache.
*/
@Override
protected void itemsAdded(CollectionAddEvent event) {
ArrayList<E2> addedItems = new ArrayList<E2>();
for (E1 item : this.getItems(event)) {
this.addComponentSource(item, addedItems);
}
this.fireItemsAdded(VALUES, addedItems);
}
/**
* Transform the specified source to a collection value model
* and add its items to our cache and the "collecting parameter".
*/
protected void addComponentSource(E1 source, List<E2> addedItems) {
CollectionValueModel<? extends E2> componentCVM = this.transformer.transform(source);
if (this.componentCVMs.put(source, componentCVM) != null) {
throw new IllegalStateException("duplicate component: " + source); //$NON-NLS-1$
}
componentCVM.addCollectionChangeListener(VALUES, this.componentCVMListener);
ArrayList<E2> componentCollection = new ArrayList<E2>(componentCVM.size());
if (this.collections.put(componentCVM, componentCollection) != null) {
throw new IllegalStateException("duplicate collection: " + source); //$NON-NLS-1$
}
this.addComponentItems(componentCVM, componentCollection);
addedItems.addAll(componentCollection);
}
/**
* Add the items in the specified component CVM to the specified component
* collection.
*/
protected void addComponentItems(CollectionValueModel<? extends E2> componentCVM, ArrayList<E2> componentCollection) {
int itemsSize = componentCVM.size();
this.size += itemsSize;
componentCollection.ensureCapacity(componentCollection.size() + itemsSize);
CollectionTools.addAll(componentCollection, componentCVM);
}
/**
* Some component sources were removed;
* remove their corresponding items from our cache.
*/
@Override
protected void itemsRemoved(CollectionRemoveEvent event) {
ArrayList<E2> removedItems = new ArrayList<E2>();
for (E1 item : this.getItems(event)) {
this.removeComponentSource(item, removedItems);
}
this.fireItemsRemoved(VALUES, removedItems);
}
/**
* Remove the items corresponding to the specified source
* from our cache.
*/
protected void removeComponentSource(E1 source, List<E2> removedItems) {
CollectionValueModel<? extends E2> componentCVM = this.componentCVMs.remove(source);
if (componentCVM == null) {
throw new IllegalStateException("missing component: " + source); //$NON-NLS-1$
}
componentCVM.removeCollectionChangeListener(VALUES, this.componentCVMListener);
ArrayList<E2> componentCollection = this.collections.remove(componentCVM);
if (componentCollection == null) {
throw new IllegalStateException("missing collection: " + source); //$NON-NLS-1$
}
removedItems.addAll(componentCollection);
this.removeComponentItems(componentCollection);
}
/**
* Update our size and collection cache.
*/
protected void removeComponentItems(ArrayList<E2> componentCollection) {
this.size -= componentCollection.size();
componentCollection.clear();
}
/**
* The component sources cleared;
* clear our cache.
*/
@Override
protected void collectionCleared(CollectionClearEvent event) {
this.removeAllComponentSources();
this.fireCollectionCleared(VALUES);
}
protected void removeAllComponentSources() {
// copy the keys so we don't eat our own tail
ArrayList<E1> copy = new ArrayList<E1>(this.componentCVMs.keySet());
for (E1 source : copy) {
this.removeComponentSource(source, NullList.<E2>instance());
}
}
/**
* The component sources changed;
* rebuild our cache.
*/
@Override
protected void collectionChanged(CollectionChangeEvent event) {
this.removeAllComponentSources();
this.addAllComponentSources();
this.fireCollectionChanged(VALUES, CollectionTools.hashBag(this.iterator()));
}
// ********** internal methods **********
/**
* One of the component collections had items added;
* synchronize our caches.
*/
protected void componentItemsAdded(CollectionAddEvent event) {
int itemsSize = event.getItemsSize();
this.size += itemsSize;
ArrayList<E2> componentCollection = this.collections.get(this.componentCVM(event));
componentCollection.ensureCapacity(componentCollection.size() + itemsSize);
this.addItemsToCollection(this.getComponentItems(event), componentCollection, VALUES);
}
/**
* One of the component collections had items removed;
* synchronize our caches.
*/
protected void componentItemsRemoved(CollectionRemoveEvent event) {
this.size -= event.getItemsSize();
ArrayList<E2> componentCollection = this.collections.get(this.componentCVM(event));
this.removeItemsFromCollection(this.getComponentItems(event), componentCollection, VALUES);
}
/**
* One of the component collections was cleared;
* synchronize our caches by clearing out the appropriate
* collection.
*/
protected void componentCollectionCleared(CollectionClearEvent event) {
ArrayList<E2> componentCollection = this.collections.get(this.componentCVM(event));
ArrayList<E2> removedItems = new ArrayList<E2>(componentCollection);
this.removeComponentItems(componentCollection);
this.fireItemsRemoved(VALUES, removedItems);
}
/**
* One of the component collections changed;
* synchronize our caches by clearing out the appropriate
* collection and then rebuilding it.
*/
protected void componentCollectionChanged(CollectionChangeEvent event) {
CollectionValueModel<E2> componentCVM = this.componentCVM(event);
ArrayList<E2> componentCollection = this.collections.get(componentCVM);
this.removeComponentItems(componentCollection);
this.addComponentItems(componentCVM, componentCollection);
this.fireCollectionChanged(VALUES, CollectionTools.hashBag(this.iterator()));
}
// minimize scope of suppressed warnings
@SuppressWarnings("unchecked")
protected Iterable<E2> getComponentItems(CollectionAddEvent event) {
return (Iterable<E2>) event.getItems();
}
// minimize scope of suppressed warnings
@SuppressWarnings("unchecked")
protected Iterable<E2> getComponentItems(CollectionRemoveEvent event) {
return (Iterable<E2>) event.getItems();
}
// minimize scope of suppressed warnings
@SuppressWarnings("unchecked")
protected CollectionValueModel<E2> componentCVM(CollectionEvent event) {
return (CollectionValueModel<E2>) event.getSource();
}
@Override
public void toString(StringBuilder sb) {
StringBuilderTools.append(sb, this);
}
}