blob: 71f17d0025a0b02a897701f1e9c2950e87092e08 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2017 Matthew Hall and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Matthew Hall - initial API and implementation (bug 237703)
* Matthew Hall - bug 274081
* Abel Hegedus - bug 414297
* Stefan Xenos <sxenos@gmail.com> - Bug 335792
*******************************************************************************/
package org.eclipse.core.databinding.observable.set;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.IObservable;
import org.eclipse.core.databinding.observable.IStaleListener;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.StaleEvent;
import org.eclipse.core.databinding.observable.value.IObservableValue;
/**
* A lazily calculated set that automatically computes and registers listeners
* on its dependencies as long as all of its dependencies are
* {@link IObservable} objects. Any change to one of the observable dependencies
* causes the set to be recomputed.
* <p>
* This class is thread safe. All state accessing methods must be invoked from
* the {@link Realm#isCurrent() current realm}. Methods for adding and removing
* listeners may be invoked from any thread.
* </p>
* <p>
* Example: compute the set of all primes greater than 1 and less than the value
* of an {@link IObservableValue} &lt; {@link Integer} &gt;.
* </p>
*
* <pre>
* final IObservableValue max = WritableValue.withValueType(Integer.TYPE);
* max.setValue(Integer.valueOf(0));
* IObservableSet primes = new ComputedSet() {
* protected Set calculate() {
* int maxVal = ((Integer) max.getValue()).intValue();
*
* Set result = new HashSet();
* outer: for (int i = 2; i &lt; maxVal; i++) {
* for (Iterator it = result.iterator(); it.hasNext();) {
* Integer knownPrime = (Integer) it.next();
* if (i % knownPrime.intValue() == 0)
* continue outer;
* }
* result.add(Integer.valueOf(i));
* }
* return result;
* }
* };
*
* System.out.println(primes); // =&gt; &quot;[]&quot;
*
* max.setValue(Integer.valueOf(20));
* System.out.println(primes); // =&gt; &quot;[2, 3, 5, 7, 11, 13, 17, 19]&quot;
* </pre>
*
* @param <E>
* the type of the elements in this set
*
* @since 1.2
*/
public abstract class ComputedSet<E> extends AbstractObservableSet<E> {
private Set<E> cachedSet = new HashSet<>();
private boolean dirty = true;
private boolean stale = false;
private IObservable[] dependencies = new IObservable[0];
/**
* Factory method to create {@link ComputedSet} objects in an easy manner.
* <p>
* The created list has a null {@link IObservableSet#getElementType}.
*
* @param supplier {@link Supplier}, which is tracked using
* {@link ObservableTracker} to find out observables it uses, in
* the same manner as {@link #calculate}.
* @return {@link ComputedSet} whose elements are computed using the given
* {@link Supplier}.
* @since 1.12
*/
public static <E> IObservableSet<E> create(Supplier<Set<E>> supplier) {
Objects.requireNonNull(supplier);
return new ComputedSet<E>() {
@Override
protected Set<E> calculate() {
return supplier.get();
}
};
}
/**
* Creates a computed set in the default realm and with an unknown (null)
* element type.
*/
public ComputedSet() {
this(Realm.getDefault(), null);
}
/**
* Creates a computed set in the default realm and with the given element
* type.
*
* @param elementType
* the element type, may be <code>null</code> to indicate unknown
* element type
*/
public ComputedSet(Object elementType) {
this(Realm.getDefault(), elementType);
}
/**
* Creates a computed set in given realm and with an unknown (null) element
* type.
*
* @param realm
* the realm
*
*/
public ComputedSet(Realm realm) {
this(realm, null);
}
/**
* Creates a computed set in the given realm and with the given element
* type.
*
* @param realm
* the realm
* @param elementType
* the element type, may be <code>null</code> to indicate unknown
* element type
*/
public ComputedSet(Realm realm, Object elementType) {
super(realm);
this.elementType = elementType;
}
/**
* Inner class that implements interfaces that we don't want to expose as
* public API. Each interface could have been implemented using a separate
* anonymous class, but we combine them here to reduce the memory overhead
* and number of classes.
*
* <p>
* The Runnable calls calculate and stores the result in cachedSet.
* </p>
*
* <p>
* The IChangeListener stores each observable in the dependencies list. This
* is registered as the listener when calling ObservableTracker, to detect
* every observable that is used by computeValue.
* </p>
*
* <p>
* The IChangeListener is attached to every dependency.
* </p>
*
*/
private class PrivateInterface implements Runnable, IChangeListener,
IStaleListener {
@Override
public void run() {
cachedSet = calculate();
if (cachedSet == null)
cachedSet = Collections.EMPTY_SET;
}
@Override
public void handleStale(StaleEvent event) {
if (!dirty)
makeStale();
}
@Override
public void handleChange(ChangeEvent event) {
makeDirty();
}
}
private PrivateInterface privateInterface = new PrivateInterface();
private Object elementType;
protected int doGetSize() {
return doGetSet().size();
}
private final Set<E> getSet() {
getterCalled();
return doGetSet();
}
@Override
protected Set<E> getWrappedSet() {
return doGetSet();
}
final Set<E> doGetSet() {
if (dirty) {
// This line will do the following:
// - Run the calculate method
// - While doing so, add any observable that is touched to the
// dependencies list
IObservable[] newDependencies = ObservableTracker.runAndMonitor(
privateInterface, privateInterface, null);
// If any dependencies are stale, a stale event will be fired here
// even if we were already stale before recomputing. This is in case
// clients assume that a set change is indicative of non-staleness.
stale = false;
for (IObservable newDependency : newDependencies) {
if (newDependency.isStale()) {
makeStale();
break;
}
}
if (!stale) {
for (IObservable newDependency : newDependencies) {
newDependency.addStaleListener(privateInterface);
}
}
dependencies = newDependencies;
dirty = false;
}
return cachedSet;
}
/**
* Subclasses must override this method to calculate the set contents. Any
* dependencies used to calculate the set must be {@link IObservable}, and
* implementers must use one of the interface methods tagged TrackedGetter
* for ComputedSet to recognize it as a dependency.
*
* @return the object's set.
*/
protected abstract Set<E> calculate();
private void makeDirty() {
if (!dirty) {
dirty = true;
// copy the old set
// bug 414297: moved before makeStale(), as cachedSet may be
// overwritten
// in makeStale() if a listener calls isStale()
final Set<E> oldSet = new HashSet<>(cachedSet);
makeStale();
stopListening();
// Fire the "dirty" event. This implementation recomputes the new
// set lazily.
fireSetChange(new SetDiff<E>() {
SetDiff<E> delegate;
private SetDiff<E> getDelegate() {
if (delegate == null)
delegate = Diffs.computeSetDiff(oldSet, getSet());
return delegate;
}
@Override
public Set<E> getAdditions() {
return getDelegate().getAdditions();
}
@Override
public Set<E> getRemovals() {
return getDelegate().getRemovals();
}
});
}
}
private void stopListening() {
if (dependencies != null) {
for (IObservable observable : dependencies) {
observable.removeChangeListener(privateInterface);
observable.removeStaleListener(privateInterface);
}
dependencies = null;
}
}
private void makeStale() {
if (!stale) {
stale = true;
fireStale();
}
}
@Override
public boolean isStale() {
// recalculate set if dirty, to ensure staleness is correct.
getSet();
return stale;
}
@Override
public Object getElementType() {
return elementType;
}
@Override
public synchronized void addChangeListener(IChangeListener listener) {
super.addChangeListener(listener);
// If somebody is listening, we need to make sure we attach our own
// listeners
computeSetForListeners();
}
@Override
public synchronized void addSetChangeListener(
ISetChangeListener<? super E> listener) {
super.addSetChangeListener(listener);
// If somebody is listening, we need to make sure we attach our own
// listeners
computeSetForListeners();
}
private void computeSetForListeners() {
// Some clients just add a listener and expect to get notified even if
// they never called getValue(), so we have to call getValue() ourselves
// here to be sure. Need to be careful about realms though, this method
// can be called outside of our realm.
// See also bug 198211. If a client calls this outside of our realm,
// they may receive change notifications before the runnable below has
// been executed. It is their job to figure out what to do with those
// notifications.
getRealm().exec(() -> {
if (dependencies == null) {
// We are not currently listening.
// But someone is listening for changes. Call getValue()
// to make sure we start listening to the observables we
// depend on.
getSet();
}
});
}
@Override
public synchronized void dispose() {
stopListening();
super.dispose();
}
}