/******************************************************************************* * Copyright (c) 2015 Google, Inc. and others. * 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: * Stefan Xenos (Google) - initial API and implementation ******************************************************************************/ package org.eclipse.core.internal.databinding.observable.sideeffect; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; import org.eclipse.core.databinding.observable.ChangeEvent; import org.eclipse.core.databinding.observable.IChangeListener; import org.eclipse.core.databinding.observable.IObservable; import org.eclipse.core.databinding.observable.ObservableTracker; import org.eclipse.core.databinding.observable.Realm; import org.eclipse.core.databinding.observable.sideeffect.ISideEffect; import org.eclipse.core.runtime.Assert; /** * Concrete implementation of the {@link ISideEffect} interface. * * @since 1.6 */ public final class SideEffect implements ISideEffect { /** * Holds a singleton side-effect which does nothing. */ public static final ISideEffect NULL_SIDE_EFFECT = new ISideEffect() { @Override public void dispose() { } @Override public void pause() { } @Override public void resume() { } @Override public void resumeAndRunIfDirty() { } @Override public void runIfDirty() { } @Override public boolean isDisposed() { // This side-effect never executes nor retains any references and // callers may rely upon this, so we should return true. return true; } @Override public void addDisposeListener(Consumer<ISideEffect> disposalConsumer) { } @Override public void removeDisposeListener(Consumer<ISideEffect> disposalConsumer) { } }; /** * True if we've been dirtied since the last time we executed * {@link #runnable}. A side-effect becomes dirtied if: * <ul> * <li>{@link #markDirty()} is called * <li>one of its dependencies changes * <li>it was newly created without executing the runnable * </ul> */ private boolean dirty; /** * True if PrivateInterface is currently enqueued in a call to * realm.asyncExec */ private boolean asyncScheduled; private int pauseDepth; private Runnable runnable; /** * Dependencies which we are currently listening for change events from */ private IObservable[] dependencies; private Realm realm; private PrivateInterface privateInterface = new PrivateInterface(); /** * List of dispose listeners. Null if empty */ private List<Consumer<ISideEffect>> disposeListeners; /** * Creates a SideEffect in the paused state that wraps the given runnable on * the default Realm. * * @param runnable * the runnable to execute. */ public SideEffect(Runnable runnable) { this(Realm.getDefault(), runnable); } /** * Creates a SideEffect in the given realm that wraps the given runnable. * * @param realm * the realm to use for this SideEffect. * @param runnable * the runnable to execute. */ public SideEffect(Realm realm, Runnable runnable) { this.runnable = runnable; this.realm = realm; this.dirty = true; this.pauseDepth = 1; } /** * Creates a SideEffect with the given initial set of dependencies in the * default realm that wraps the given runnable. * * @param runnable * the runnable to wrap * @param dependencies * the initial set of dependencies */ public SideEffect(Runnable runnable, IObservable... dependencies) { this.dependencies = dependencies; this.runnable = runnable; this.dirty = false; this.pauseDepth = 0; this.realm = Realm.getDefault(); for (IObservable next : dependencies) { next.addChangeListener(privateInterface); } } @Override public void resume() { checkRealm(); pauseDepth--; if (pauseDepth < 0) { throw new IllegalStateException( "The resume() method was called more times than pause()."); //$NON-NLS-1$ } else if (dirty && pauseDepth == 0) { scheduleUpdate(); } } @Override public void pause() { checkRealm(); pauseDepth++; if (dirty && pauseDepth == 1) { // No need to continue listening if we're already dirtied, since // we'll just end up running again after we're resumed stopListening(); dependencies = null; } } @Override public void resumeAndRunIfDirty() { checkRealm(); pauseDepth--; update(); } private void update() { if (dirty && pauseDepth <= 0) { dirty = false; // Hold a reference to the old dependencies to prevent them from // being garbage collected until we've computed the new set. In the // event that a dependency is lazily created, this prevents it from // being destroyed and immediately recreated. // Stop listening for dependency changes. stopListening(); // 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(runnable, null, null); // If the side-effect disposed itself, exit without attaching any // listeners. if (isDisposed()) { return; } for (IObservable next : newDependencies) { next.addChangeListener(privateInterface); } dependencies = newDependencies; } } @Override public void dispose() { checkRealm(); if (isDisposed()) { return; } pauseDepth = 0; stopListening(); dependencies = null; runnable = null; if (disposeListeners != null) { List<Consumer<ISideEffect>> oldListeners = disposeListeners; disposeListeners = null; oldListeners.forEach(dc -> dc.accept(SideEffect.this)); } } @Override public boolean isDisposed() { return runnable == null; } /** * Add an disposal consumer for this {@link ISideEffect} instance. * * @param disposalConsumer * a consumer which will be notified once this * {@link ISideEffect} is disposed. */ @Override public void addDisposeListener(Consumer<ISideEffect> disposalConsumer) { checkRealm(); if (isDisposed()) { return; } if (this.disposeListeners == null) { this.disposeListeners = new ArrayList<>(); } this.disposeListeners.add(disposalConsumer); } /** * Remove an disposal consumer for this {@link ISideEffect} instance. * * @param disposalConsumer * a consumer which is supposed to be removed from the dispose * listener list. */ @Override public void removeDisposeListener(Consumer<ISideEffect> disposalConsumer) { checkRealm(); if (this.disposeListeners == null) { return; } this.disposeListeners.remove(disposalConsumer); } @Override public void runIfDirty() { checkRealm(); update(); } private void stopListening() { if (dependencies != null) { for (IObservable observable : dependencies) { observable.removeChangeListener(privateInterface); } } } private void markDirtyInternal() { if (!dirty) { dirty = true; if (pauseDepth <= 0) { scheduleUpdate(); } else { stopListening(); dependencies = null; } } } private void scheduleUpdate() { if (this.asyncScheduled) { return; } this.asyncScheduled = true; realm.asyncExec(privateInterface); } private void checkRealm() { Assert.isTrue(realm.isCurrent(), "This operation must be run within the observable's realm"); //$NON-NLS-1$ } /** * Creates a runnable which will execute the given supplier and pass the * result to the given consumer while suppressing all tracked getters from * the consumer. * * @param supplier * supplier to execute * @param consumer * a consumer that will receive the value and in which tracked * getters will be suppressed. * @return a newly constructed runnable */ public static <T> Runnable makeRunnable(Supplier<T> supplier, Consumer<T> consumer) { return () -> { T value = supplier.get(); ObservableTracker.setIgnore(true); try { consumer.accept(value); } finally { ObservableTracker.setIgnore(false); } }; } private class PrivateInterface implements IChangeListener, Runnable { @Override public void handleChange(ChangeEvent event) { markDirtyInternal(); } @Override public void run() { if (isDisposed()) { return; } asyncScheduled = false; update(); } } }