package org.codefx.libfx.concurrent.when;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
/**
* Executes an action when an {@link ObservableValue}'s value fulfills a certain condition.
* <p>
* The action will not be executed before {@link #executeWhen()} is called. The action is only executed once. If it was
* not yet executed, this can be prevented by calling {@link #cancel()}.
* <p>
* This class guarantees that regardless of the way different threads interact with the {@code ObservableValue} the
* action will be executed...
* <ul>
* <li>... if the value held when {@code executeWhen()} returns passes the condition
* <li>... if a new value passes the condition (either during {@code executeWhen()} or after it returns)
* <li>... at most once
* </ul>
* If the observable is manipulated by several threads, this class does not guarantee that the first value to pass the
* condition is the one handed to the action. Depending on the interaction of those threads it might be the initial
* value (the one tested during {@code executeWhen()}) or one of several which were set by those threads.
* <p>
* Use {@link ExecuteWhen} to build an instance of this class.
*
* @param <T>
* the type the observed {@link ObservableValue}'s wraps
*/
public class ExecuteOnceWhen<T> {
/*
* If no other threads were involved the class would be simple. It would suffice to check the observable's current
* value. If it passes the condition, execute the action; otherwise attach a listener which processes each new value
* in the same way.
*/
/*
* But since other threads are allowed to interfere, this could fail. If a correct value is set between the check
* and attaching the listener, this value would not be processed and the action would not be executed. To prevent
* this the listener is added first and only then is the current value checked and the action possibly executed.
*/
/*
* Now, if another thread sets the correct value after the listener was added but before the current value is
* processed, the action would be executed twice. To prevent this from happening an atomic boolean is used. It will
* contain true when the action can still be executed. When a value (either the initial or a new one) fulfills the
* condition, it is checked (and set to false). If it contained true, the action will be executed.
*/
// #begin FIELDS
/**
* The {@link ObservableValue} upon whose value the action's execution depends.
*/
private final ObservableValue<T> observable;
/**
* The condition the {@link #observable}'s value must fulfill for {@link #action} to be executed.
*/
private final Predicate<? super T> condition;
/**
* The action which will be executed.
*/
private final Consumer<? super T> action;
/**
* Indicates whether {@link #action} might still be executed at some point in the future. Is used to prevent the
* listener and the initial check (see {@link #executeWhen()}) to both execute the action.
*/
private final AtomicBoolean willExecute;
/**
* The listener which executes {@link #action} and sets {@link #willExecute} accordingly.
*/
private final ChangeListener<T> listenerWhichExecutesAction;
/**
* Indicates whether {@link #executeWhen()} was already called. If so, it can not be called again.
*/
private final AtomicBoolean executeWhenWasAlreadyCalled;
// #end FIELDS
/**
* Creates a new instance from the specified arguments.
* <p>
* Note that for the action to be executed, {@link #executeWhen()} needs to be called.
*
* @param observable
* the {@link ObservableValue} upon whose value the action's execution depends
* @param condition
* the condition the {@link #observable}'s value must fulfill for {@link #action} to be executed
* @param action
* the action which will be executed
*/
ExecuteOnceWhen(ObservableValue<T> observable, Predicate<? super T> condition, Consumer<? super T> action) {
assert observable != null : "The argument 'observable' must not be null.";
assert condition != null : "The argument 'condition' must not be null.";
assert action != null : "The argument 'action' must not be null.";
this.observable = observable;
this.condition = condition;
this.action = action;
listenerWhichExecutesAction = (obs, oldValue, newValue) -> tryExecuteAction(newValue);
executeWhenWasAlreadyCalled = new AtomicBoolean(false);
willExecute = new AtomicBoolean(true);
}
// #begin METHODS
/**
* Executes the action (once) when the observable's value passes the condition.
* <p>
* This is a one way function that must only be called once. Calling it again throws an
* {@link IllegalStateException}.
* <p>
* Call {@link #cancel()} to prevent future execution.
*
* @throws IllegalStateException
* if this method is called more than once
*/
public void executeWhen() throws IllegalStateException {
boolean wasAlreadyCalled = executeWhenWasAlreadyCalled.getAndSet(true);
if (wasAlreadyCalled)
throw new IllegalStateException("The method 'executeWhen' can only be called once.");
observable.addListener(listenerWhichExecutesAction);
tryExecuteAction(observable.getValue());
}
/**
* Executes {@link #action} if the specified value fulfills the {@link #condition} and the action was not yet
* executed. The latter is indicated by the {@link #willExecute}, which will also be updated.
*
* @param currentValue
* the {@link #observable}'s current value
*/
private void tryExecuteAction(T currentValue) {
boolean valueFailsGateway = !condition.test(currentValue);
if (valueFailsGateway)
return;
boolean actionCanBeExecuted = willExecute.getAndSet(false);
if (actionCanBeExecuted) {
action.accept(currentValue);
// the action was just executed and will not be executed again so the listener is not needed anymore
observable.removeListener(listenerWhichExecutesAction);
}
}
/**
* Cancels the future execution of the action. If {@link #executeWhen()} was not yet called or the action was
* already executed, this is a no-op.
*/
public void cancel() {
willExecute.set(false);
observable.removeListener(listenerWhichExecutesAction);
}
// #end METHODS
}