package org.oddjob.state;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.log4j.Logger;
import org.oddjob.Stateful;
import org.oddjob.Structural;
import org.oddjob.structural.StructuralEvent;
import org.oddjob.structural.StructuralListener;
/**
* Track, and aggregate the states of child jobs. Aggregation is
* achieved using the given {@link StateOperator}.
*
* @author rob
*
*/
public class StructuralStateHelper implements Stateful {
private static final Logger logger = Logger.getLogger(
StructuralStateHelper.class);
/** The structural we're helping. */
private final Structural structural;
/** Used to capture state into. */
private final ParentStateHandler stateHandler =
new ParentStateHandler(this);
/** The states of the children. *
* References are required because the position of the children
* can move. The state listener uses a reference instead
* of an index to insert state.
*/
private final List<AtomicReference<State>> states =
new ArrayList<AtomicReference<State>>();
/** The listeners listening to the children. */
private final List<StateListener> listeners =
new ArrayList<StateListener>();
/** The {@link StateOperator}. */
private volatile StateOperator stateOperator;
/**
* Listens to a single child's state.
*/
class ChildStateListener implements StateListener {
private final AtomicReference<State> holder;
public ChildStateListener(AtomicReference<State> holder) {
this.holder = holder;
}
@Override
public void jobStateChange(final StateEvent event) {
stateHandler.callLocked(new Callable<Void>() {
@Override
public Void call() throws Exception {
State previous = holder.getAndSet(event.getState());
// Don't check when listener initially added as this happens
// in when child added.
if (previous != null) {
checkStates();
}
return null;
}
});
};
@Override
public String toString() {
return getClass().getName() + " for [" + structural + "]";
}
};
/**
* Create a new instance that will track state changes in the children
* of the given {@link Structural}. States of the children will be
* combined using the given {@link StateOperator}.
*
* @param structural The structural. Must not be null.
* @param operator The operator that combines child state. Must not be
* null.
*/
public StructuralStateHelper(Structural structural, StateOperator operator) {
if (structural == null) {
throw new NullPointerException("No Structural.");
}
if (operator == null) {
throw new NullPointerException("No State Operator.");
}
this.structural = structural;
setStateOperator(operator);
// Add a listener that tracks child changes.
structural.addStructuralListener(
new StructuralListener() {
@Override
public void childAdded(final StructuralEvent event) {
stateHandler.waitToWhen(new IsAnyState(), new Runnable() {
@Override
public void run() {
int index = event.getIndex();
Object child = event.getChild();
AtomicReference<State> stateHolder =
new AtomicReference<State>();
ChildStateListener listener =
new ChildStateListener(stateHolder);
if (child instanceof Stateful) {
((Stateful) child).addStateListener(listener);
}
else {
stateHolder.set(ParentState.COMPLETE);
}
listeners.add(index, listener);
states.add(index, stateHolder);
checkStates();
}
});
}
@Override
public void childRemoved(final StructuralEvent event) {
stateHandler.waitToWhen(new IsAnyState(), new Runnable() {
@Override
public void run() {
int index = event.getIndex();
Object child = event.getChild();
StateListener listener = listeners.remove(index);
if (child instanceof Stateful) {
((Stateful) child).removeStateListener(listener);
}
states.remove(index);
checkStates();
}
});
}
});
}
public void addStateListener(StateListener listener) {
stateHandler.addStateListener(listener);
}
public void removeStateListener(StateListener listener) {
stateHandler.removeStateListener(listener);
}
private void checkStates() {
stateHandler.waitToWhen(new IsAnyState(), new Runnable() {
public void run() {
State[] stateArgs = new State[states.size()];
int i = 0;
for (AtomicReference<State> holder : states) {
stateArgs[i++] = holder.get();
}
ParentState state = stateOperator.evaluate(stateArgs);
// don't fire a new state if it is the same as the last.
if (state == stateHandler.getState()) {
return;
}
stateHandler.setState(state);
stateHandler.fireEvent();
}
});
}
@Override
public StateEvent lastStateEvent() {
return stateHandler.lastStateEvent();
}
public State[] getChildStates() {
return stateHandler.callLocked(new Callable<State[]>() {
@Override
public State[] call() throws Exception {
State[] array = new State[states.size()];
int i = 0;
for (AtomicReference<State> holder : states) {
array[i++] = holder.get();
}
return array;
}
});
}
public StateOperator getStateOperator() {
return stateOperator;
}
/**
* Change the State Operator to be used to combine child state.
*
* @param stateOperator The State Operator. Must not be null.
*/
public void setStateOperator(StateOperator stateOperator) {
if (stateOperator == null) {
throw new NullPointerException("State Operator must not be null.");
}
if (stateOperator.equals(this.stateOperator)) {
return;
}
if (this.stateOperator != null) {
logger.info("Changing State Operator from " + this.stateOperator +
" to " + stateOperator);
}
this.stateOperator = stateOperator;
checkStates();
}
}