/* * (c) Rob Gordon 2005 */ package org.oddjob.scheduling; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import javax.inject.Inject; import org.oddjob.Stateful; import org.oddjob.arooa.deploy.annotations.ArooaAttribute; import org.oddjob.arooa.deploy.annotations.ArooaComponent; import org.oddjob.arooa.deploy.annotations.ArooaHidden; import org.oddjob.arooa.utils.DateHelper; import org.oddjob.framework.ComponentBoundry; import org.oddjob.framework.JobDestroyedException; import org.oddjob.scheduling.state.TimerState; import org.oddjob.state.AnyActiveStateOp; import org.oddjob.state.IsAnyState; import org.oddjob.state.IsStoppable; import org.oddjob.state.StateCondition; import org.oddjob.state.StateConditions; import org.oddjob.state.StateEvent; import org.oddjob.state.StateListener; import org.oddjob.state.StateOperator; /** * @oddjob.description A trigger runs it's job when the job being triggered * on enters the state specified. * <p> * Once the trigger's job runs the trigger * will reflect the state of the it's job. The trigger will continue to * reflect it's job's state until it is reset. * <p>Subsequent state changes in * the triggering job are ignored until the trigger is reset and re-run. * <p> * If the triggering job is destroyed, because it is deleted or on a remote * server the trigger will enter an exception state. * <p> * * @oddjob.example * * A simple trigger. * * {@oddjob.xml.resource org/oddjob/scheduling/TriggerSimple.xml} * * @oddjob.example * * A trigger that runs once two other jobs have completed. * * {@oddjob.xml.resource org/oddjob/scheduling/TriggerExample.xml} * * @oddjob.example * * Cancelling a trigger. * * {@oddjob.xml.resource org/oddjob/scheduling/TriggerCancelExample.xml} * * @oddjob.example * * Examples Elsewhere. * <ul> * <li>The scheduling example (<code>examples/scheduling/dailyftp.xml</code>) * uses a trigger to send an email if one of the FTP transfers fails.</li> * </ul> * * * @author Rob Gordon. */ public class Trigger extends ScheduleBase { private static final long serialVersionUID = 2009031000L; /** * @oddjob.property * @oddjob.description The job the trigger will trigger on. * @oddjob.required Yes. */ private transient Stateful on; /** * @oddjob.property * @oddjob.description The state condition which will cause the trigger * to fire. See the Oddjob User guide for a full list of state * conditions. * @oddjob.required No, defaults to COMPLETE. */ private StateCondition state = StateConditions.COMPLETE; /** * @oddjob.property * @oddjob.description A state condition that will cause the trigger * to cancel. * @oddjob.required No, defaults to not cancelling. */ private StateCondition cancelWhen; /** The last time on the event that caused the trigger. */ private Date lastTime; /** * @oddjob.property * @oddjob.description Fire trigger on new events only. If set the time on * the event will be compared with the last that this trigger received and * only a new event will cause the trigger to fire. * @oddjob.required No, defaults to false. */ private boolean newOnly; /** The scheduler to schedule on. */ private transient ExecutorService executors; /** The schedule id. */ private transient Future<?> future; private transient StateListener listener; @ArooaHidden @Inject public void setExecutorService(ExecutorService executor) { this.executors = executor; } @Override protected StateOperator getStateOp() { return new AnyActiveStateOp(); } @Override protected void begin() { if (on == null) { throw new NullPointerException("Nothing to trigger on."); } if (executors == null) { throw new NullPointerException("No ExecutorService."); } listener = new TriggerStateListener(); on.addStateListener(listener); logger().info("Waiting for [" + on + "] to have state [" + state + "]"); } @Override protected void onStop() { Future<?> future = null; synchronized (this) { future = this.future; this.future = null; } if (future != null) { future.cancel(false); } removeListener(); } @Override protected void postStop() { childStateReflector.start(); } /** * Remove the state listener from the job we're triggering on. */ private void removeListener() { StateListener listener = null; synchronized (this) { listener = this.listener; this.listener = null; } if (listener != null) { on.removeStateListener(listener); } } /** * @oddjob.property job * @oddjob.description The job to run when the trigger fires. * @oddjob.required Yes. */ @ArooaComponent public synchronized void setJob(Runnable job) { if (job == null) { childHelper.removeChildAt(0); } else { childHelper.insertChild(0, job); } } public StateCondition getState() { return state; } @ArooaAttribute public void setState(StateCondition state) { this.state = state; } public StateCondition getCancelWhen() { return cancelWhen; } @ArooaAttribute public void setCancelWhen(StateCondition cancelWhen) { this.cancelWhen = cancelWhen; } public boolean isNewOnly() { return newOnly; } public void setNewOnly(boolean newEventOnly) { this.newOnly = newEventOnly; } public Stateful getOn() { return on; } @ArooaAttribute public void setOn(Stateful triggerOn) { this.on = triggerOn; } /** * Wrap the job. This is the Runnable that is submitted. */ class Execution implements Runnable { public void run() { ComponentBoundry.push(loggerName(), Trigger.this); try { logger().info("Executing child."); // check job state here because it guarantees all other // state listeners have been notified. on.lastStateEvent(); stateHandler.waitToWhen(new IsAnyState(), new Runnable() { @Override public void run() { getStateChanger().setState(TimerState.ACTIVE); } }); Runnable job = childHelper.getChild(); if (job == null) { logger().warn("Nothing to run. Job is null!"); } else { // Note reset isn't necessary because this is a // single execution only. try { job.run(); save(); } catch (Throwable t) { logger().error("Failed running triggered job.", t); } } childStateReflector.start(); } finally { ComponentBoundry.pop(); } } } class TriggerStateListener implements StateListener { @Override public synchronized void jobStateChange(StateEvent event) { logger().debug("Trigger on [" + on + "] has state [" + event.getState() + "] at [" + DateHelper.formatDateTime(event.getTime()) + "]"); if (event.getState().isDestroyed()) { stateHandler().waitToWhen(new IsStoppable(), new Runnable() { @Override public void run() { getStateChanger().setStateException( new JobDestroyedException(on)); } }); on = null; } if (!state.test(event.getState()) && (cancelWhen == null || !cancelWhen.test(event.getState())) ) { logger().debug("Not a trigger state, returning."); return; } // don't fire if event time hasn't changed. if (newOnly && event.getTime().equals(lastTime)) { logger().info("Already had event for time [" + DateHelper.formatDateTime(lastTime) + "], not triggering."); return; } lastTime = event.getTime(); // We won't fire again until run again. removeListener(); if (state.test(event.getState())) { logger().debug("Submitting [" + childHelper.getChild() + "] for immediate execution."); future = executors.submit(new Execution()); } else { stateHandler.waitToWhen(new IsAnyState(), new Runnable() { public void run() { getStateChanger().setState(TimerState.COMPLETE); } }); } }; } }