package org.oddjob.persist;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import org.oddjob.FailedToStopException;
import org.oddjob.Resetable;
import org.oddjob.Stateful;
import org.oddjob.Stoppable;
import org.oddjob.Structural;
import org.oddjob.arooa.deploy.annotations.ArooaComponent;
import org.oddjob.arooa.life.ComponentPersistException;
import org.oddjob.arooa.life.ComponentPersister;
import org.oddjob.framework.BasePrimary;
import org.oddjob.framework.ComponentBoundry;
import org.oddjob.framework.StopWait;
import org.oddjob.images.IconHelper;
import org.oddjob.images.StateIcons;
import org.oddjob.state.IsAnyState;
import org.oddjob.state.IsExecutable;
import org.oddjob.state.IsHardResetable;
import org.oddjob.state.IsSoftResetable;
import org.oddjob.state.IsStoppable;
import org.oddjob.state.ParentState;
import org.oddjob.state.ParentStateChanger;
import org.oddjob.state.ParentStateHandler;
import org.oddjob.state.StandardParentStateConverter;
import org.oddjob.state.State;
import org.oddjob.state.StateChanger;
import org.oddjob.state.StateCondition;
import org.oddjob.state.StateConditions;
import org.oddjob.state.StateEvent;
import org.oddjob.state.StateListener;
import org.oddjob.structural.ChildHelper;
import org.oddjob.structural.StructuralListener;
/**
* @oddjob.description A Job that is capable of taking a snapshot of the
* state of it's child jobs. An {@link org.oddjob.persist.ArchiveBrowserJob}
* can be used to browse an archive created with this job.
*
* @oddjob.example
*
* Create an archive after each scheduled run. The time of the schedule
* is used to identify the archive.
*
* {@oddjob.xml.resource org/oddjob/persist/ArchiveJobTest.xml}
*
* @author rob
*
*/
public class ArchiveJob extends BasePrimary
implements
Runnable, Serializable,
Stoppable, Resetable, Stateful, Structural {
private static final long serialVersionUID = 2010032500L;
/** Handle state. */
private transient volatile ParentStateHandler stateHandler;
/** Used to notify clients of an icon change. */
private transient volatile IconHelper iconHelper;
/** Used to change state. */
private transient volatile ParentStateChanger stateChanger;
/** Track changes to children an notify listeners. */
private transient volatile ChildHelper<Runnable> childHelper;
/**
* @oddjob.property
* @oddjob.description The identifier of the snapshot that will
* be taken when this job runs.
* @oddjob.required Yes.
*/
private volatile Object archiveIdentifier;
/**
* @oddjob.property
* @oddjob.description The name of the acrhive that all snapshots
* will be stored in.
* @oddjob.required Yes.
*/
private volatile String archiveName;
/**
* @oddjob.property
* @oddjob.description The persister to use to store archives.
* @oddjob.required Yes, but will fall back on the current Oddjob persister.
*/
private transient volatile OddjobPersister archiver;
/** Listener that does the archiving. */
private transient volatile PersistingStateListener listener;
/** Stop flag. */
protected transient volatile boolean stop;
/**
* Constructor.
*/
public ArchiveJob() {
completeConstruction();
}
private void completeConstruction() {
stateHandler = new ParentStateHandler(this);
childHelper = new ChildHelper<Runnable>(this);
iconHelper = new IconHelper(this,
StateIcons.iconFor(stateHandler.getState()));
stateChanger = new ParentStateChanger(stateHandler, iconHelper,
new Persistable() {
@Override
public void persist() throws ComponentPersistException {
save();
}
});
}
@Override
protected ParentStateHandler stateHandler() {
return stateHandler;
}
@Override
protected IconHelper iconHelper() {
return iconHelper;
}
protected StateChanger<ParentState> getStateChanger() {
return stateChanger;
}
/**
* Implement the main execute method for a job. This surrounds the
* doExecute method of the sub class and sets state for the job.
*/
public final void run() {
ComponentBoundry.push(loggerName(), this);
try {
if (!stateHandler.waitToWhen(new IsExecutable(), new Runnable() {
public void run() {
getStateChanger().setState(ParentState.EXECUTING);
}
})) {
return;
}
logger().info("Executing.");
try {
configure();
execute();
}
catch (final Throwable e) {
logger().error("Job Exception.", e);
stateHandler.waitToWhen(new IsAnyState(), new Runnable() {
public void run() {
getStateChanger().setStateException(e);
}
});
}
logger().info("Execution finished.");
}
finally {
ComponentBoundry.pop();
}
}
/**
* Listen for state changes. Persist when in finished state.
* Reflect state for this job.
*/
private class PersistingStateListener implements StateListener {
private final Stateful child;
private final ComponentPersister componentPersister;
private volatile StateEvent event;
private volatile boolean reflect;
public PersistingStateListener(Stateful child,
ComponentPersister componentPersister) {
this.child = child;
this.componentPersister = componentPersister;
}
void startReflecting() {
reflect = true;
reflectState();
}
void reflectState() {
if (event.getState().isDestroyed()) {
stopListening(event.getSource());
}
else {
stateHandler.waitToWhen(new IsAnyState(), new Runnable() {
public void run() {
if (event.getState().isException()) {
getStateChanger().setStateException(
event.getException());
}
else {
getStateChanger().setState(
new StandardParentStateConverter(
).toStructuralState(
event.getState()));
}
}
});
}
}
@Override
public void jobStateChange(final StateEvent event) {
ComponentBoundry.push(loggerName(), ArchiveJob.this);
try {
this.event = event;
if (reflect) {
reflectState();
}
if (stop) {
// don't persist when stopping.
return;
}
State state = event.getState();
StateCondition finished = StateConditions.FINISHED;
if (finished.test(state)) {
if (!stateHandler.waitToWhen(new IsAnyState(), new Runnable() {
public void run() {
logger().info("Archiving [" + event.getSource() +
"] as [" + archiveIdentifier +
"] because " + event.getState() + ".");
try {
persist(event.getSource());
}
catch (ComponentPersistException e) {
logger().error("Failed to persist.", e);
getStateChanger().setStateException(e);
}
}
})) {
}
}
} finally {
ComponentBoundry.pop();
}
}
private void persist(Stateful source) throws ComponentPersistException {
ComponentBoundry.push(loggerName(), ArchiveJob.this);
try {
Object silhouette = new SilhouetteFactory().create(
child, ArchiveJob.this.getArooaSession());
componentPersister.persist(archiveIdentifier.toString(),
silhouette, getArooaSession());
}
finally {
ComponentBoundry.pop();
}
}
}
protected void execute() throws Throwable {
OddjobPersister archiver = this.archiver;
if (archiver == null) {
ComponentPersister persister =
getArooaSession().getComponentPersister();
if (persister != null &&
persister instanceof OddjobPersister) {
archiver = ((OddjobPersister) persister);
}
}
if (archiver == null) {
throw new NullPointerException("No Archiver.");
}
if (archiveIdentifier == null) {
throw new NullPointerException("No ArchiveIdentifier.");
}
final ComponentPersister componentPersister =
archiver.persisterFor(archiveName);
if (componentPersister == null) {
throw new NullPointerException("No Persister for [" +
archiveName + "]");
}
Runnable child = childHelper.getChild();
if (child == null) {
return;
}
if (! (child instanceof Stateful)) {
throw new IllegalArgumentException("Child must be stateful to be archived.");
}
if (listener == null) {
listener = new PersistingStateListener((Stateful) child,
componentPersister);
((Stateful) child).addStateListener(listener);
}
child.run();
listener.startReflecting();
}
/**
* Implementation for a typical stop.
* <p>
* This stop implementation doesn't check that the job is
* executing as stop messages must cascade down the hierarchy
* to manually started jobs.
*
* @throws FailedToStopException
*/
public void stop() throws FailedToStopException {
stateHandler.assertAlive();
ComponentBoundry.push(loggerName(), this);
try {
if (!stateHandler.waitToWhen(new IsStoppable(), new Runnable() {
public void run() {
stop = true;
}
})) {
return;
}
logger().info("Stopping.");
iconHelper.changeIcon(IconHelper.STOPPING);
try {
childHelper.stopChildren();
} catch (RuntimeException e) {
iconHelper.changeIcon(IconHelper.EXECUTING);
throw e;
}
synchronized (this) {
notifyAll();
}
new StopWait(this).run();
stopListening((Stateful) childHelper.getChild());
logger().info("Stopped.");
} finally {
ComponentBoundry.pop();
}
}
/**
* Perform a soft reset on the job.
*/
public boolean softReset() {
return commonReset(new IsSoftResetable(), "Soft");
}
/**
* Perform a hard reset on the job.
*/
public boolean hardReset() {
return commonReset(new IsHardResetable(), "Hard");
}
/**
* Provide common reset functionality. Note that the lock is
* not held over propagation to children because some deadlock
* problem was occurring.
*
* @param condition
* @param text
* @return
*/
private boolean commonReset(StateCondition condition, final String text) {
ComponentBoundry.push(loggerName(), this);
try {
if (!stateHandler.waitToWhen(condition, new Runnable() {
public void run() {
logger().debug("Propagating " + text +
" reset to children.");
}
})) {
return false;
}
stopListening((Stateful) childHelper.getChild());
childHelper.hardResetChildren();
return stateHandler.waitToWhen(condition, new Runnable() {
public void run() {
stop = false;
getStateChanger().setState(ParentState.READY);
logger().info(text + " Reset complete.");
}
});
}
finally {
ComponentBoundry.pop();
}
}
/**
* Stop listening to state changes.
*
* @param to The child.
*/
private void stopListening(Stateful to) {
if (to == null) {
return;
}
StateListener listener = this.listener;
this.listener = null;
if (listener != null) {
to.removeStateListener(listener);
logger().debug("Archiving Listener removed from child");
}
}
public Object getArchiveIdentifier() {
return archiveIdentifier;
}
public void setArchiveIdentifier(Object archive) {
this.archiveIdentifier = archive;
}
public String getArchiveName() {
return archiveName;
}
public void setArchiveName(String path) {
this.archiveName = path;
}
public OddjobPersister getArchiver() {
return archiver;
}
public void setArchiver(OddjobPersister archiver) {
this.archiver = archiver;
}
/**
* Add a listener. The listener will immediately receive add
* notifications for all existing children.
*
* @param listener The listener.
*/
public void addStructuralListener(StructuralListener listener) {
stateHandler.assertAlive();
childHelper.addStructuralListener(listener);
}
/**
* Remove a listener.
*
* @param listener The listener.
*/
public void removeStructuralListener(StructuralListener listener) {
childHelper.removeStructuralListener(listener);
}
/**
* @oddjob.property job
* @oddjob.description The child job.
* @oddjob.required No, but pointless if missing.
*
* @param job
*/
@ArooaComponent
public void setJob(Runnable job) {
if (job == null) {
childHelper.removeAllChildren();
}
else {
childHelper.insertChild(0, job);
}
}
/**
* Custom serialisation.
*/
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeObject(getName());
if (loggerName().startsWith(getClass().getName())) {
s.writeObject(null);
}
else {
s.writeObject(loggerName());
}
s.writeObject(stateHandler.lastStateEvent().serializable());
}
/**
* Custom serialisation.
*/
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
String name = (String) s.readObject();
logger((String) s.readObject());
StateEvent.SerializableNoSource savedEvent =
(StateEvent.SerializableNoSource) s.readObject();
completeConstruction();
setName(name);
stateHandler.restoreLastJobStateEvent(savedEvent);
iconHelper.changeIcon(
StateIcons.iconFor(stateHandler.getState()));
}
@Override
protected void onDestroy() {
stateHandler.assertAlive();
super.onDestroy();
ComponentBoundry.push(loggerName(), this);
try {
stateHandler.waitToWhen(new IsAnyState(), new Runnable() {
public void run() {
stop = true;
stopListening((Stateful) childHelper.getChild());
}
});
}
finally {
ComponentBoundry.pop();
}
}
/**
* Internal method to fire state.
*/
protected void fireDestroyedState() {
if (!stateHandler().waitToWhen(new IsAnyState(), new Runnable() {
public void run() {
stateHandler().setState(ParentState.DESTROYED);
stateHandler().fireEvent();
}
})) {
throw new IllegalStateException("[" + ArchiveJob.this + "] Failed set state DESTROYED");
}
logger().debug("[" + this + "] Destroyed.");
}
}