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 executed every time the value
* passes the condition. If this can happen in parallel in several threads, the action must be thread-safe as no further
* synchronization is provided by this class. Further execution 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>... once during {@code executeWhen()} if either the observable's initial value or one it was changed to passes
* the condition
* <li>... every time a new value passes the condition after {@code executeWhen()} returns
* </ul>
* If the observable is manipulated by several threads during {@code executeWhen()}, 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 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 ExecuteAlwaysWhen<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. Also, a listener which processes each new value in the
* same way has to be added.
*/
/*
* 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. Actually, if the value is switched fast enough by different
* threads, each could be in the middle of executing the listener when the check of the initial value finally
* proceeds. This would lead to multiple desired executions plus the undesired one of the initial check. (Note that
* this problem can only occur until the initial value is processed. After that only the listener can execute the
* action and there is only one so no funny stuff can happen.) That is the reason why the contract states that a
* correct value will only be processed once during executeWhen().
*/
/*
* To achieve this, two atomic booleans are used. The first, 'executeAlways', is false after construction and will
* be set to true at the end of 'executeWhen'. When it is true, all values will be processed. Until then only one
* execution is allowed. This is monitored using 'alreadyExecuted', which is initially false and will be set to true
* on the first execution of the action.
*/
// #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;
/**
* The listener which executes {@link #action} and sets {@link #alreadyExecuted} accordingly.
*/
private final ChangeListener<? super T> listenerWhichExecutesAction;
/**
* Indicates whether {@link #executeWhen()} was already called. If so, it can not be called again.
*/
private final AtomicBoolean executeWhenWasAlreadyCalled;
/**
* Indicates whether {@link #action} is executed each time the value passes the {@link #condition}.
*/
private final AtomicBoolean executeAlways;
/**
* Indicates whether {@link #action} was already executed once.
*/
private final AtomicBoolean alreadyExecuted;
// #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
*/
ExecuteAlwaysWhen(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);
executeAlways = new AtomicBoolean(false);
executeWhenWasAlreadyCalled = new AtomicBoolean(false);
alreadyExecuted = new AtomicBoolean(false);
}
// #begin METHODS
/**
* Executes the action (every time) 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 further 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' must only be called once.");
observable.addListener(listenerWhichExecutesAction);
tryExecuteAction(observable.getValue());
executeAlways.set(true);
}
/**
* Executes {@link #action} if the specified value fulfills the {@link #condition}. Sets {@link #alreadyExecuted} to
* indicate that the action was executed at least once.
* <p>
* Called by {@link #listenerWhichExecutesAction} every time {@link #observable} changes its value.
*
* @param currentValue
* the {@link #observable}'s current value
*/
private void tryExecuteAction(T currentValue) {
boolean valueFailsGateway = !condition.test(currentValue);
if (valueFailsGateway)
return;
boolean canNotExecuteNow = !canExecuteNow();
if (canNotExecuteNow)
return;
action.accept(currentValue);
}
/**
* Indicates whether the {@link #action} can be executed now by checking {@link #executeAlways} and
* {@link #alreadyExecuted}.
* <p>
* Potentially changes the state of {@code alreadyExecuted} so it must only be called if {@link #action} is really
* executed afterwards (i.e. the value should already have passed the {@link #condition}).
*
* @return true if the {@link #action} can be executed now
*/
private boolean canExecuteNow() {
if (executeAlways.get()) {
alreadyExecuted.set(true);
return true;
}
// in this case the action must only be executed once, so make sure that didn't happen yet
boolean alreadyExecutedOnce = alreadyExecuted.getAndSet(true);
boolean canExecuteNow = !alreadyExecutedOnce;
return canExecuteNow;
}
/**
* 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() {
observable.removeListener(listenerWhichExecutesAction);
}
// #end METHODS
}