/*******************************************************************************
* Copyright (c) 2015, 2016 Pivotal, Inc.
* 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springsource.ide.eclipse.commons.livexp.core;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet;
import com.google.common.collect.ImmutableSet;
/**
* Similar to a 'live variable' but represents a set of values that can be listened
* to. At the moment only coarse grained change events are produced. I.e. there
* is just one event for "the Set has changed".
* <p>
* To allow more efficient incemental processing, clients may be interested in
* just knowing about individual elements getting added / removed.
* This is not yet supported.
* <p>
* Note: this class is meant to eventually replace LiveSet in commons which should
* then become deprecated and, eventually, removed.
*/
public class LiveSetVariable<T> extends ObservableSet<T> {
/**
* To be able to efficiently check that backing collection has changed.
* This assumes the backing collection is owned by the instance and it isn't
* mutated externally.
*/
private boolean dirty = false;
private Set<T> backingCollection;
public LiveSetVariable() {
this(AsyncMode.SYNC);
}
/**
* Instantiate a LiveSet that uses a HashSet as a backing collection.
*/
public LiveSetVariable(AsyncMode async) {
this(new HashSet<T>(), async);
}
public LiveSetVariable(Set<T> backingCollection) {
this(backingCollection, AsyncMode.SYNC);
}
/**
* Instantiate a LiveSet with a specific backing collection. It is assumed that
* the backing collection henceforth is owned by the LiveSet. Client code should
* not retain references to the backing collection and should only modify the
* collection via liveset operations.
*/
public LiveSetVariable(Set<T> backingCollection, AsyncMode async) {
super(ImmutableSet.copyOf(backingCollection), AsyncMode.SYNC, async);
//Note the 'refresh' is done synchronously as its the most logical from user point of
// view. Notably: otherwise when replacing values and immediately thereafter calling 'getValues'
// you are not guaranteed to actually get the values you just inserted back.
this.backingCollection = backingCollection;
}
@Override
protected void syncRefresh() {
//TODO: if we override 'refresh' as well we can avoid scheduling a job if not dirty.
// Or alternately we can add isDirty() method to the protocol of AsyncLiveExpression
// and buid this optimization into AsyncLiveExpression itself.
//We override refresh method so we can avoid doing set comparison by making
// use of a dirty flag instead.
synchronized (this) {
if (!dirty) return; //bail out fast and don't copy the collection needlessly
value = compute();
}
//Note... we are being careful here to put the 'changed' call outside synch block.
// only keep locks for short time while maniping the collection / dirty state.
// but notify listeners without holding on to the lock while listeneres are
// doing their thing (which could be anything... and lead to deadlocks otherwise!)
changed();
}
@Override
protected synchronized ImmutableSet<T> compute() {
return ImmutableSet.copyOf(backingCollection);
}
public void add(T name) {
synchronized (this) {
if (backingCollection.contains(name)) {
//Nothing to do!
return;
} else {
backingCollection.add(name);
dirty = true;
}
}
//Carefull... this leads to 'change' call, so must have released monitor before calling!
refresh();
}
public void remove(T name) {
synchronized (this) {
if (!backingCollection.contains(name)) {
//Nothing to do!
return;
} else {
backingCollection.remove(name);
dirty = true;
}
}
refresh();
}
/**
* Batch-add a number of elements to the set. Only at most one change event will
* be fired no matter how many elements where actually added.
*/
public void addAll(T[] elements) {
synchronized (this) {
for (T e : elements) {
dirty = backingCollection.add(e) || dirty;
}
}
refresh();
}
public void addAll(Collection<T> elements) {
synchronized (this) {
for (T e : elements) {
dirty = backingCollection.add(e) || dirty;
}
}
refresh();
}
/**
* Replaces current elements with newElements. This is more efficient than
* using individual add/remove calls as it only generates at most one 'changed'
* event at the end of applying all the changes (if any).
*/
public void replaceAll(Collection<T> newElements) {
synchronized (this) {
//Remove any old elements that should no longer be there
Iterator<T> iter = backingCollection.iterator();
while (iter.hasNext()) {
T oldElement = iter.next();
if (!newElements.contains(oldElement)) {
iter.remove();
dirty = true;
}
}
//Add any missing new Elements
for (T newElement : newElements) {
dirty = backingCollection.add(newElement) || dirty;
}
}
refresh(); //refreshes (if dirty)
}
}