/*******************************************************************************
* 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.databinding.observable.sideeffect;
import java.util.function.Consumer;
import java.util.function.Supplier;
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.value.ComputedValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.internal.databinding.observable.sideeffect.SideEffect;
/**
* An {@link ISideEffect} allows you to run code whenever one or more
* observables change. An {@link ISideEffect} is a lot like a listener except
* that it doesn't need to be attached to anything. Instead, it reacts
* automatically to changes in tracked getters that are invoked by the listener.
* <p>
* Observables form a directed graph of dependencies. Classes like
* {@link WritableValue} form the inputs to the graph (nodes which have only
* outputs), classes like {@link ComputedValue} form the interior nodes (they
* receive inputs from observables and produce an output which is used by other
* observables), and {@link ISideEffect} is used for the leaf nodes (nodes which
* receive inputs but produce no output).
* <p>
* Side-effects have a life-cycle which passes through a number of states:
* <ul>
* <li>Paused: The side-effect will listen for changes but will not react to
* them. If any change occurs while the side-effect is paused, it will react
* when and if the side-effect is resumed. Some side-effects are paused
* immediately on construction. This is useful, for example, for creating a
* side-effect in an object's constructor which should not begin running until a
* later time. When using a side-effect to update a control or a view, it is
* common to pause the side-effect when the view is hidden and resume the
* side-effect when the view becomes visible.</li>
* <li>Resumed: The side-effect will listen for changes and react to them
* asynchronously. Side-effects may be paused and resumed any number of times.
* </li>
* <li>Disposed: The side-effect will not listen to or react to changes. It will
* also remove any strong references to its dependencies. Once a side-effect
* enters the disposed state it remains in that state until it is garbage
* collected.</li>
* </ul>
*
* Example usage:
*
* <pre>
* IObservableValue<String> firstName = ...
* IObservableValue<String> lastName = ...
* IObservableValue<Boolean> showFullNamePreference = ...
* Label userName = ...
*
* ISideEffect sideEffect = ISideEffect.create(() -> {
* String name = showFullNamePreference.get()
* ? (firstName.get() + " " + lastName.get())
* : firstName.get();
* userName.setText("Your name is " + name);
* });
* </pre>
* <p>
* The above example uses an {@link ISideEffect} to fill in a label with a
* user's name. It will react automatically to changes in the username and the
* showFullNamePreference.
* <p>
* The same thing could be accomplished by attaching listeners to all three
* observables, but there are several advantages to using {@link ISideEffect}
* over listeners.
* <ul>
* <li>The {@link ISideEffect} can self-optimize based on branches in the run
* method. It will remove listeners from any {@link IObservable} which wasn't
* used on the most recent run. In the above example, there is no need to listen
* to the lastName field when showFullNamePreference is false.
* <li>The {@link ISideEffect} will batch changes together and run
* asynchronously. If firstName and lastName change at the same time, the
* {@link ISideEffect} will only run once.
* <li>Since the {@link ISideEffect} doesn't need to be explicitly attached to
* the observables it affects, it is impossible for it to get out of sync with
* the underlying data.
* </ul>
* <p>
* Please be aware of a common anti-pattern. Don't create new observables inside
* an {@link ISideEffect} unless you remember them for future runs. Creating new
* observables inside an {@link ISideEffect} can easily create infinite loops.
*
* <pre>
* // Bad: May create an infinite loop, since each AvatarObservable instance may
* // fire an asynchronous event after creation
* void createControls() {
* ISideEffect sideEffect = ISideEffect.create(() -> {
* IObservableValue<Image> myAvatar = new AvatarObservable();
*
* myIcon.setImage(myAvatar.getValue());
* });
* }
*
* // Good: The AvatarObservable instance is remembered between invocations of
* // the side-effect.
* void createControls() {
* final IObservableValue<Image> myAvatar = new AvatarObservable();
* ISideEffect sideEffect = ISideEffect.create(() -> {
* myIcon.setImage(myAvatar.getValue());
* });
* }
* </pre>
*
* @since 1.6
* @noimplement This interface is not intended to be implemented by clients.
*/
public interface ISideEffect {
/**
* Disposes the side-effect, detaching all listeners and deallocating all
* memory used by the side-effect. The side-effect will not execute again
* after this method is invoked.
* <p>
* This method may be invoked more than once.
*/
void dispose();
/**
* Returns true if this side-effect has been disposed. A disposed
* side-effect will never execute again or retain any strong references to
* the observables it uses. A side-effect which has not been disposed has
* some possibility of executing again in the future and of retaining strong
* references to observables.
*
* @return true if this side-effect has been disposed.
*/
boolean isDisposed();
/**
* Increments the count of the number of times the {@link ISideEffect} has
* been paused. If the side-effect has been paused a greater number of times
* than it has been resumed, it enters the paused state.
* <p>
* When a {@link ISideEffect} is paused, this prevents it from running again
* until it is resumed. Note that the side-effect will continue listening to
* its dependencies while it is paused. If a dependency changes while the
* {@link ISideEffect} is paused, the {@link ISideEffect} will run again
* after it is resumed.
* <p>
* A side-effect may be paused and resumed any number of times. You should
* use pause instead of dispose if there is a chance you may want to resume
* the SideEffect later.
*/
void pause();
/**
* Increments the count of the number of times the {@link ISideEffect} has
* been resumed. If the side-effect has been resumed an equal number of
* times than it has been paused, it leaves the paused state and enters the
* resumed state. It is an error to resume {@link ISideEffect} more often
* than it has been paused.
* <p>
* When a {@link ISideEffect} is resumed, it starts reacting to changes in
* tracked getters invoked by its runnable. It will continue to react to
* changes until it is either paused or disposed. If the {@link ISideEffect}
* is dirty, it will be run at the earliest opportunity after this method
* returns.
*/
void resume();
/**
* Increments the count of the number of times the {@link ISideEffect} has
* been resumed. If the side-effect has been resumed an equal or greater
* number of times than it has been paused, it leaves the paused state and
* enters the resumed state.
* <p>
* When a {@link ISideEffect} is resumed, it starts reacting to changes in
* TrackedGetters invoked by its runnable. It will continue to react to
* changes until it is either paused or disposed. If the {@link ISideEffect}
* is dirty, it will be run synchronously.
* <p>
* This is a convenience method which is fully equivalent to calling
* {@link #resume} followed by {@link #runIfDirty}, but slightly faster.
*/
void resumeAndRunIfDirty();
/**
* Causes the side effect to run synchronously if and only if it is
* currently dirty (that is, if one of its dependencies has changed since
* the last time it ran). Does nothing if the {@link ISideEffect} is
* currently paused.
*/
void runIfDirty();
/**
* Adds a listener that will be invoked when this {@link ISideEffect}
* instance is disposed. The listener will not be invoked if the receiver
* has already been disposed at the time when the listener is attached.
*
* @param disposalConsumer
* a consumer which will be notified once this
* {@link ISideEffect} is disposed.
*/
void addDisposeListener(Consumer<ISideEffect> disposalConsumer);
/**
* Removes a dispose listener from this {@link ISideEffect} instance. Has no
* effect if no such listener was previously attached.
*
* @param disposalConsumer
* a consumer which is supposed to be removed from the dispose
* listener list.
*/
void removeDisposeListener(Consumer<ISideEffect> disposalConsumer);
/**
* Creates a new {@link ISideEffect} on the default {@link Realm} but does
* not run it immediately. Callers are responsible for invoking
* {@link #resume()} or {@link #resumeAndRunIfDirty()} when they want the
* runnable to begin executing.
*
* @param runnable
* the runnable to execute. Must be idempotent.
* @return a newly-created {@link ISideEffect} which has not yet been
* activated. Callers are responsible for calling {@link #dispose()}
* on the result when it is no longer needed.
*/
static ISideEffect createPaused(Runnable runnable) {
return new SideEffect(runnable);
}
/**
* Creates a new {@link ISideEffect} on the given Realm but does not
* activate it immediately. Callers are responsible for invoking
* {@link #resume()} when they want the runnable to begin executing.
*
* @param realm
* the realm to execute
* @param runnable
* the runnable to execute. Must be idempotent.
* @return a newly-created {@link ISideEffect} which has not yet been
* activated. Callers are responsible for calling {@link #dispose()}
* on the result when it is no longer needed.
*/
static ISideEffect createPaused(Realm realm, Runnable runnable) {
return new SideEffect(realm, runnable);
}
/**
* Runs the given runnable once synchronously. The runnable will then run
* again after any tracked getter invoked by the runnable changes. It will
* continue doing so until the returned {@link ISideEffect} is disposed. The
* returned {@link ISideEffect} is associated with the default realm. The
* caller must dispose the returned {@link ISideEffect} when they are done
* with it.
*
* @param runnable
* an idempotent runnable which will be executed once
* synchronously then additional times after any tracked getter
* it uses changes state
* @return an {@link ISideEffect} interface that may be used to stop the
* side-effect from running. The {@link Runnable} will not be
* executed anymore after the dispose method is invoked.
*/
static ISideEffect create(Runnable runnable) {
IObservable[] dependencies = ObservableTracker.runAndMonitor(runnable, null, null);
if (dependencies.length == 0) {
return SideEffect.NULL_SIDE_EFFECT;
}
return new SideEffect(runnable, dependencies);
}
/**
* Runs the supplier and passes its result to the consumer. The same thing
* will happen again after any tracked getter invoked by the supplier
* changes. It will continue to do so until the given {@link ISideEffect} is
* disposed. The returned {@link ISideEffect} is associated with the default
* realm. The caller must dispose the returned {@link ISideEffect} when they
* are done with it.
* <p>
* The ISideEffect will initially be in the resumed state.
* <p>
* The first invocation of this method will be synchronous. This version is
* slightly more efficient than {@link #createResumed(Supplier, Consumer)} and
* should be preferred if synchronous execution is acceptable.
*
* @param supplier
* a supplier which will compute a value and be monitored for
* changes in tracked getters. It should be side-effect-free.
* @param consumer
* a consumer which will receive the value. It should be
* idempotent. It will not be monitored for tracked getters.
*
* @return an {@link ISideEffect} interface that may be used to stop the
* side-effect from running. The {@link Runnable} will not be
* executed anymore after the dispose method is invoked.
*/
static <T> ISideEffect create(Supplier<T> supplier, Consumer<T> consumer) {
return ISideEffect.create(SideEffect.makeRunnable(supplier, consumer));
}
/**
* Runs the supplier and passes its result to the consumer. The same thing
* will happen again after any tracked getter invoked by the supplier
* changes. It will continue to do so until the given {@link ISideEffect} is
* disposed. The returned {@link ISideEffect} is associated with the default
* realm. The caller must dispose the returned {@link ISideEffect} when they
* are done with it.
* <p>
* The ISideEffect will initially be in the resumed state.
* <p>
* The first invocation of this method will be asynchronous. This is useful,
* for example, when constructing an {@link ISideEffect} in a constructor
* since it ensures that the constructor will run to completion before the
* first invocation of the {@link ISideEffect}. However, this extra safety
* comes with a small performance cost over
* {@link #create(Supplier, Consumer)}.
*
* @param supplier
* a supplier which will compute a value and be monitored for
* changes in tracked getters. It should be side-effect-free.
* @param consumer
* a consumer which will receive the value. It should be
* idempotent. It will not be monitored for tracked getters.
*
* @return an {@link ISideEffect} interface that may be used to stop the
* side-effect from running. The {@link Runnable} will not be
* executed anymore after the dispose method is invoked.
*/
static <T> ISideEffect createResumed(Supplier<T> supplier, Consumer<T> consumer) {
ISideEffect result = ISideEffect.createPaused(SideEffect.makeRunnable(supplier, consumer));
result.resume();
return result;
}
/**
* Runs the given supplier until it returns a non-null result. The first
* time it returns a non-null result, that result will be passed to the
* consumer and the ISideEffect will dispose itself. As long as the supplier
* returns null, any tracked getters it invokes will be monitored for
* changes. If they change, the supplier will be run again at some point in
* the future.
* <p>
* The resulting ISideEffect will be dirty and resumed, so the first
* invocation of the supplier will be asynchronous. If the caller needs it
* to be invoked synchronously, they can call {@link #runIfDirty()}
* <p>
* Unlike {@link #create(Supplier, Consumer)}, the consumer does not need to
* be idempotent.
* <p>
* This method is used for gathering asynchronous data before opening an
* editor, saving to disk, opening a dialog box, or doing some other
* operation which should only be performed once.
* <p>
* Consider the following example, which displays the content of a text file
* in a message box without doing any file I/O on the UI thread.
* <p>
*
* <pre>
* IObservableValue<String> loadFileAsString(String filename) {
* // Uses another thread to load the given filename. The resulting observable returns
* // null if the file is not yet loaded or contains the file contents if the file is
* // fully loaded
* // ...
* }
*
* void showFileContents(Shell parentShell, String filename) {
* IObservableValue<String> webPageContent = loadFileAsString(filename);
* ISideEffect.runOnce(webPageContent::getValue, (content) -> {
* MessageDialog.openInformation(parentShell, "Your file contains", content);
* })
* }
* </pre>
*
* @param supplier
* supplier which returns null if the side-effect should continue
* to wait or returns a non-null value to be passed to the
* consumer if it is time to invoke the consumer
* @param consumer
* a (possibly non-idempotent) consumer which will receive the
* first non-null result returned by the supplier.
* @return a side-effect which can be used to control this operation. If it
* is disposed before the consumer is invoked, the consumer will
* never be invoked. It will not invoke the supplier if it is
* paused.
*/
static <T> ISideEffect consumeOnceAsync(Supplier<T> supplier, Consumer<T> consumer) {
final ISideEffect[] result = new ISideEffect[1];
Runnable theRunnable = () -> {
T value = supplier.get();
if (value != null) {
ObservableTracker.setIgnore(true);
try {
consumer.accept(value);
} finally {
ObservableTracker.setIgnore(false);
}
result[0].dispose();
}
};
result[0] = ISideEffect.createPaused(theRunnable);
result[0].resume();
return result[0];
}
}