package sale; import java.io.*; import users.User; import sale.stdforms.MsgForm; import data.DataBasket; import data.Stock; import data.Catalog; import log.*; /** * A process. Processes are used to manipulate data in {@link DataBasket DataBaskets}, {@link Stock Stocks}, * {@link Catalog Catalogs}, and any other data structures you might want to use. * * <p>Processes are viewed as finite deterministic automata, internally represented by a directed graph. The * nodes of this graph are called {@link Gate Gates}, the edges are {@link Transition Transitions}.</p> * * <p>Processes are persistent, i.e. they are automatically restored when the system comes up again after * having been down for some time.</p> * * <p>A process can be suspended whenever it is at a gate. When a process is asked to * {@link #suspend suspend()} while it is in a transition, it will get suspended only at the next gate. For * this to be feasible, transitions must be rather short, and, in particular, must not comprise any user * interaction.</p> * * @author Steffen Zschaler * @version 2.0 17/08/1999 * @since v2.0 */ public abstract class SaleProcess implements LogContext, Loggable, ProcessErrorCodes, Serializable { /** * The name of this process. Used to identify the process, esp. for debug purposes. * * @serial */ private String m_sName; /** * The context in which this process runs. Used for all user communication, log and * data access. * * @serial */ private ProcessContext m_pcContext; /** * The monitor synchronizing access to the process context. */ private transient Object m_oContextLock; /** * Return the monitor synchronizing access to the process context. * * @override Never */ private Object getContextLock() { if (m_oContextLock == null) { m_oContextLock = new Object(); } return m_oContextLock; } /** * The DataBasket used to implement transactional behavior. * * @serial */ private DataBasket m_dbWorkBasket; /** * The previous log context of the current DataBasket. * * @serial */ private LogContext m_lcOldBasketContext; /** * The monitor synchronizing access to the DataBasket. */ private transient Object m_oBasketLock; /** * Return the monitor synchronizing access to the DataBasket. * * @override Never */ private Object getBasketLock() { if (m_oBasketLock == null) { m_oBasketLock = new Object(); } return m_oBasketLock; } /** * The current gate, if any. * * @serial */ protected Gate m_gCurGate; /** * The current transition, if any. * * @serial */ protected Transition m_tCurTransition; /** * The process' main thread. */ private transient Thread m_trdMain = null; /** * Flag indicating whether the process is currently suspended. * * @serial */ private boolean m_fSuspended = false; /** * Flag indicating whether the process has been resumed. * * @serial */ private boolean m_fResumed = false; /** * Last error condition. * * @serial */ private int m_nErrorCode = 0; /** * Additional information concerning the cause of the last error. * * @serial <strong>Attention:</strong> This may be a common cause of mistake, when objects handed in as * error descriptions are not serializable! */ private Object m_oErrorExtraInfo = null; /** * Count error nesting, so that we do not run into infinite loops. * * @serial */ private int m_nErrorNesting = 0; /** * Create a new SaleProcess with a given name. * * @param sName the name of this process. */ public SaleProcess (String sName) { super(); m_sName = sName; } /** * Return the name of this process. * * @override Never * * @return the name of the process. */ public String getName() { return m_sName; } /** * Attach a ProcessContext to this process. * * @override Never * * @param pcNew the process context to be attached. * * @return the previously attached process context, if any. */ public ProcessContext attach (ProcessContext pcNew) { synchronized (getContextLock()) { ProcessContext pc = m_pcContext; m_pcContext = pcNew; if (isAlive()) { // if the process is currently alive, we have to // unregister the process with the old context... if (pc != null) { pc.processFinished (this); } //...and register it with the new context if (m_pcContext != null) { m_pcContext.processStarted (this); } } return pc; } } /** * Detach and return the current process context. * * @override Never */ public ProcessContext detachContext() { return attach ((ProcessContext) null); } /** * Return the process context attached to this process. * * @override Never */ public ProcessContext getContext() { synchronized (getContextLock()) { return m_pcContext; } } /** * Attach the DataBaskte that is going to be used to implement transactional * behavior for this Process. * * @override Never * * @param dbNew the DataBasket to be attached. * * @return the previously attached DataBasket, if any. */ public DataBasket attach (DataBasket dbNew) { synchronized (getBasketLock()) { DataBasket db = m_dbWorkBasket; if (m_dbWorkBasket != null) { m_dbWorkBasket.setLogContext (m_lcOldBasketContext); } m_dbWorkBasket = dbNew; if (m_dbWorkBasket != null) { m_lcOldBasketContext = m_dbWorkBasket.setLogContext (this); } return db; } } /** * Detach and return the current DataBasket. * * @override Never */ public DataBasket detachBasket() { return attach ((DataBasket) null); } /** * Get the currently attached DataBasket. * * @override Never */ public DataBasket getBasket() { synchronized (getBasketLock()) { return m_dbWorkBasket; } } /** * Return true if this Process can be stopped with a subsequent <code>quit()</code> * command. * * @override Sometimes The default implementation will return <code>!fContextDestroy</code>, so that the * process can only be quitted, if it will be possible to resume it afterwards. * * @param fContextDestroy true, if the quit request was issued due to a destroyal of * the process' context. If false you can assume that it will be possible to restore * and resume the Process after it had been <code>quit</code>ted. * * @return Currently returns <code>!fContextDestroy</code>, so that the process can * only be quitted, if it will be possible to resume it afterwards. */ public boolean canQuit (boolean fContextDestroy) { return !fContextDestroy; } /** * Internal error helper, used to cancel a transition or gate when it calls error(). * * @author Steffen Zschaler * @version 2.0 17/08/1999 * @since v2.0 */ private class ProcessErrorError extends Error { public ProcessErrorError() { super(); } } /** * Raise an error in this process. * * <p>First calls {@link #printErrorInfo} to inform the user of the error condition, * then cancels the process by jumping to the "error" gate.</p> * * <p><strong>Attention:</strong>This method must only be called from within a gate or * a transition of this process. If called from any other environment, unpredictable * behavior will result.</p> * * @override Never * * @param nErrorCode the error code. */ public void error (int nErrorCode) { error (nErrorCode, (Object) null); } /** * Raise an error in this process. * * <p>First calls {@link #printErrorInfo} to inform the user of the error condition, * then cancels the process by jumping to the "error" gate.</p> * * <p><strong>Attention:</strong>This method must only be called from within a gate or * a transition of this process. If called from any other environment, unpredictable * behavior will result.</p> * * @override Never * * @param nErrorCode the error code. * @param oExtraInfo additional information that explains the cause of the error. */ public void error (int nErrorCode, Object oExtraInfo) { m_nErrorCode = nErrorCode; m_oErrorExtraInfo = oExtraInfo; m_nErrorNesting++; //printErrorInfo (nErrorCode, oExtraInfo); --> called at the error gate! throw new ProcessErrorError(); } /** * Raise an error in this process. * * <p>First calls {@link #printErrorInfo} to inform the user of the error condition, * then cancels the process by jumping to the "error" gate.</p> * * <p><strong>Attention:</strong>This method must only be called from within a gate or * a transition of this process. If called from any other environment, unpredictable * behavior will result.</p> * * @override Never * * @param nErrorCode the error code. * @param tExtraInfo the exception that caused the error. */ public void error (int nErrorCode, Throwable tExtraInfo) { java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); java.io.PrintWriter pw = new java.io.PrintWriter (bos); tExtraInfo.printStackTrace (pw); pw.close(); error (nErrorCode, bos.toString()); } /** * Print error information to inform the user of an error condition. * * <p>Calls {@link #getErrorMsg} to resolve the error code into an error message. * All occurences of "%o" in the error message are then replaced by the * extra information's string representation.</p> * * <p>If the context has a {@link ProcessContext#hasUseableDisplay useable display} * a {@link sale.stdforms.MsgForm} is displayed containing the error message. * Otherwise, the error message is printed to the standard error stream.</p> * * <p>This method is never called directly, but rather called by the Framework as * appropriate. Thus, it is assured, that user communication takes place at a gate * only, even in the case of an error.</p> * * @override Never * * @param nErrorCode the error code. * @param oExtraInfo additional information concerning the cause of the error. */ protected void printErrorInfo (int nErrorCode, Object oExtraInfo) { String sErrorMsg = getErrorMsg (nErrorCode); if (oExtraInfo != null) { if (sErrorMsg.indexOf ("%o") >= 0) { int nIndex; while ((nIndex = sErrorMsg.indexOf("%o")) >= 0) { String sTemp = sErrorMsg.substring (0, nIndex) + oExtraInfo; if (nIndex < sErrorMsg.length() - 2) { sTemp += sErrorMsg.substring (nIndex + 2); } sErrorMsg = sTemp; } } else { sErrorMsg += "\nAdditional Information: " + oExtraInfo; } } else { if (sErrorMsg.indexOf ("%o") >= 0) { int nIndex; while ((nIndex = sErrorMsg.indexOf("%o")) >= 0) { String sTemp = sErrorMsg.substring (0, nIndex) + "<>"; if (nIndex < sErrorMsg.length() - 2) { sTemp += sErrorMsg.substring (nIndex + 2); } sErrorMsg = sTemp; } } } if ((m_pcContext != null) && (m_pcContext.hasUseableDisplay (this))) { try { m_pcContext.setFormSheet (this, new MsgForm ("Error", sErrorMsg)); } catch (InterruptedException e) {} } else { System.err.println ("Error in process <" + getName() + ">:\n"); System.err.println (sErrorMsg); } } /** * Return a readable version of the error message. * * @override Sometimes Override this method whenever you define new error codes for a process. * * @param nErrorCode the error code. * * @return a readable version of the error message. */ public String getErrorMsg (int nErrorCode) { switch (nErrorCode) { case ERR_INTERNAL: return "An internal error occured. " + "Please file a bug report to your programmer team.\n" + "Error message (Please provide this message with your bug report):\n\n%o"; case NOT_ENOUGH_ELEMENTS_ERROR: return "Sorry, not enough elements."; case REMOVE_VETO_EXCEPTION: return "Sorry, couldn't delete."; case DUPLICATE_KEY_EXCEPTION: return "Element does already exist."; case DATABASKET_CONFLICT_ERROR: return "Your action stands in conflict with an action of another user."; default: return "Error no. " + nErrorCode + " occured.\nProcess cancelled."; } } /** * Suspend the process. * * <p>This method will suspend the process at the nearest gate. The method will block * until the process was suspended.</p> * * @override Never * * @exception InterruptedException if an interrupt occurs in the calling thread while * waiting for the process to suspend. */ public synchronized void suspend() throws InterruptedException { if (m_fSuspended) { // already done. return; } m_fSuspended = true; Thread trdMain = m_trdMain; if (trdMain != null) { trdMain.interrupt(); trdMain.join(); // wait for main thread to finish // allow InterruptedException, if any, to propagate upwards } } /** * Return true if this process is currently suspended. * * <p><strong>Attention:</strong>This method is not synchronized. This allows you to * call it from within a gate or transition without fear for deadlocks, but there * might be certain rare circumstances where the several <code>isXXX()</code> * methods' return values give an seemingly inconsistent picture.</p> * * @override Never */ public final boolean isSuspended() { return m_fSuspended; } /** * Resume a previously suspended process. * * <p>This method will resume the process at the gate at which it was suspended.</p> * * @override Never */ public synchronized void resume() { m_fResumed = true; start(); } /** * Return true if this process has been resumed from a previous suspended state. * * <p><strong>Attention:</strong>This method is not synchronized. This allows you to * call it from within a gate or transition without fear for deadlocks, but there * might be certain rare circumstances where the several <code>isXXX()</code> * methods' return values give an seemingly inconsistent picture.</p> * * @override Never */ public final boolean isResumed() { return m_fResumed; } /** * Start the process. * * @override Never */ public synchronized void start() { if (m_trdMain == null) { m_fSuspended = false; if (!m_fResumed) { m_gCurGate = getInitialGate(); } m_trdMain = new Thread ("SaleProcess thread: <" + getName() + ">.main()") { public void run() { main(); } }; m_trdMain.start(); } } /** * Return true if this process has been started, but has not yet died. * * <p>In contrast to {@link #isRunning()} <code>isAlive()</code> will also * return true for a process that has been suspended.</p> * * <p><strong>Attention:</strong>This method is not synchronized. This allows you to * call it from within a gate or transition without fear for deadlocks, but there * might be certain rare circumstances where the several <code>isXXX()</code> * methods' return values give an seemingly inconsistent picture.</p> * * @override Never */ public final boolean isAlive() { return isRunning() || isSuspended(); } /** * Return true if this process is currently running. The process is running, if it * has been started, but not yet stopped nor suspended. * * <p><strong>Attention:</strong>This method is not synchronized. This allows you to * call it from within a gate or transition without fear for deadlocks, but there * might be certain rare circumstances where the several <code>isXXX()</code> * methods' return values give an seemingly inconsistent picture.</p> * * @override Never */ public final boolean isRunning() { return (m_trdMain != null); } /** * A thread providing an exception firewall for all subprocesses of a process. */ private class SubProcess extends Thread { /** * The activity to be guarded by the exception firewall. */ private Runnable m_rSubProcess; /** * The parent process. */ private SaleProcess m_pParent; /** * Create a new subprocess. */ public SubProcess (String sName, SaleProcess pParent, Runnable rSubProcess) { super (sName); m_pParent = pParent; m_rSubProcess = rSubProcess; } /** * The exception firewall. */ public void run() { try { m_rSubProcess.run(); } catch (ProcessErrorError pe) { // silently stop the subprocess } catch (ThreadDeath td) { throw td; } catch (Throwable t) { try { m_pParent.error (ERR_INTERNAL, t); } catch (ProcessErrorError pe) { // silently stop the subprocess } } } } /** * The gate that is responsible for printing error messages. */ private class PrintErrorGate implements Gate { private int m_nCode; private Object m_oInfo; private int m_nErrNesting; public PrintErrorGate (int nErrorNesting) { super(); m_nCode = m_nErrorCode; m_oInfo = m_oErrorExtraInfo; m_nErrNesting = nErrorNesting; } public Transition getNextTransition (SaleProcess p, User u) { printErrorInfo (m_nCode, m_oInfo); return new GateChangeTransition (p.getErrorGate (m_nErrNesting)); } } /** * The central control loop of the process. * * @override Never */ private void main() { int nCurErrorNesting = m_nErrorNesting; try { // initialization if (!m_fResumed) { m_pcContext.processStarted (this); } try { onResumeOrStart (m_fResumed); } catch (ProcessErrorError pe) { nCurErrorNesting = m_nErrorNesting; m_gCurGate = new PrintErrorGate (m_nErrorNesting); } // the actual control loop while ((m_gCurGate != null) && (!m_fSuspended)) { // Loop initialization m_tCurTransition = null; // Let the gate figure out the next Transition. // This is interruptible. Thread trdGate = new SubProcess ("SaleProcess thread: <" + getName() + ">.gateHandler", this, new Runnable() { public void run() { try { m_tCurTransition = m_gCurGate.getNextTransition (SaleProcess.this, ((m_pcContext != null) ? (m_pcContext.getCurrentUser (SaleProcess.this)): (null))); if (m_fSuspended) { m_tCurTransition = null; } } catch (InterruptedException e) { util.Debug.print ("Caught interrupt in Gate handler.", -1); m_tCurTransition = null; } } }); trdGate.start(); while (trdGate.isAlive()) { // zzz (see below!) try { trdGate.join(); } catch (InterruptedException e) { util.Debug.print ("Caught interrupt in main process handler.", -1); if (m_fSuspended) { util.Debug.print ("In main process handler: Handing interrupt on to gate handler.", -1); trdGate.interrupt(); // we don't need to wait for trdGate to die here, as we will simply enter the loop at zzz again } } } util.Debug.print ("In main process handler: Gate handler died.", -1); if (m_fSuspended) { // if the process was suspended, break the control loop. break; } if (m_nErrorNesting != nCurErrorNesting) { // an error occurred: jump to "error" gate. nCurErrorNesting = m_nErrorNesting; m_gCurGate = new PrintErrorGate (m_nErrorNesting); continue; } // Leave the gate and do a Transition. // This is non-interruptible, except on error conditions. Thread trdTransition = new SubProcess ("SaleProcess thread: <" + getName() + ">.transitionHandler", this, new Runnable() { public void run() { m_gCurGate = m_tCurTransition.perform (SaleProcess.this, ((m_pcContext != null) ? (m_pcContext.getCurrentUser (SaleProcess.this)): (null))); } }); trdTransition.start(); while (trdTransition.isAlive()) { try { trdTransition.join(); } catch (InterruptedException e) { // In a Transition we don't want to be interrupted, so just go on waiting. // The m_fSuspended flag will be set by the suspend() call. continue; } } if (m_nErrorNesting != nCurErrorNesting) { // an error occurred: jump to "error" gate. nCurErrorNesting = m_nErrorNesting; m_gCurGate = new PrintErrorGate (m_nErrorNesting); continue; } } if (m_fSuspended) { // special cleanup on suspend() calls. try { onSuspended(); } catch (ProcessErrorError pe) { nCurErrorNesting = m_nErrorNesting; m_gCurGate = new PrintErrorGate (m_nErrorNesting); } } } catch (Throwable t) { System.err.println ("Exception occured in process " + getName() + ":\n"); t.printStackTrace(); } finally { try { onFinished(); } catch (ProcessErrorError pe) { if (m_fSuspended) { // on any error only jump to the "error" gate if the process has been suspended // otherwise just forget about the error ! nCurErrorNesting = m_nErrorNesting; m_gCurGate = new PrintErrorGate (m_nErrorNesting); } } catch (ThreadDeath td) {} catch (Throwable t) { System.err.println ("Exception occured in process " + getName() + ", onFinished() sequence:\n"); t.printStackTrace(); } // make sure, this is always done if (!isSuspended()) { m_pcContext.processFinished (this); } m_fResumed = false; m_trdMain = null; } } /** * Hook method called on every start or resume of the process. Should perform any * global process initializiation. * * <p>This method is called in the process' main thread and any uncaught exception * raised in this method will lead to the process being stopped.</p> * * @override Sometimes Override this method if you need special initialization code. * * @param fIsResume true if the process has not been started afresh, but rather has * been resumed. */ protected void onResumeOrStart (boolean fIsResume) {} /** * Hook method called whenever the process was suspended. This method is called * in the process' main thread and any uncaught exception raised in this method * will be reported from that thread.</p> * * <p>This method is called in the process' main thread and any uncaught exception * raised in this method will lead to the process being stopped.</p> * * @override Sometimes Override this method if you need special cleanup code for suspended processes. */ protected void onSuspended() {} /** * Hook method called whenever the process was finished, independently of whether * the process was really finished or just suspended. * * <p>You can find out whether the process was just suspended by calling * {@link #isSuspended isSuspended()}.</p> * * <p>This method is called in the process' main thread and any uncaught exception * raised in this method will be reported in this thread. This method must * <strong>not</strong> call {@link #error error()}, however.</p> * * @override Sometimes Override this method if you need special cleanup code at the end of a process. */ protected void onFinished() {} /** * Quit the process at the nearest gate. The process will jump to the * "quit" gate. * * <p><code>quit()</code> first calls <code>suspend()</code> then sets the current * gate to be the "quit" gate and finally <code>resume()</code>-s the * process.</p> * * @override Never * * @param fWaitQuit if true, quit will block until the process ended. * * @exception InterruptedException if an interrupt occured in the calling thread * while waiting for the process to quit. * * @see #getQuitGate */ public synchronized void quit (boolean fWaitQuit) throws InterruptedException { if (!isAlive()) { return; } try { suspend(); } catch (InterruptedException e) {} m_gCurGate = getQuitGate(); resume(); if (fWaitQuit) { // copy to avoid NullPointerExceptions Thread trdMain = m_trdMain; if (trdMain != null) { // wait for the process to finish. trdMain.join(); } } } /** * A log entry describing a process that was executed. * * <p>The default implementation will only give the name of the process and when it was logged.</p> * * @author Steffen Zschaler * @version 2.0 14/07/1999 * @since v2.0 */ public static class ProcessLogEntry extends LogEntry { /** * The name of the process that this log entry describes. * * @serial */ protected String m_sProcessName; /** * Create a new ProcessLogEntry. */ public ProcessLogEntry (SaleProcess p) { super(); m_sProcessName = p.getName(); } /** * Return the name of the process that this log entry describes. * * @override Never */ public String getProcessName() { return m_sProcessName; } /** * Return descriptive information for this LogEntry. * * @override Always */ public String toString() { return "Process \"" + getProcessName() + "\" logged on " + getLogDate() + "."; } } /** * Return information that describes the process for logging purposes. * * @override Always The default implementation produces a log entry that will simply give the name of the * process and the time when logging happened. * * @see ProcessLogEntry */ public LogEntry getLogData() { return new ProcessLogEntry (this); } /** * Logs the given data to a log file. If the process is in a process context, the data is logged to the * process context's log file. Otherwise, the global log file is used. * * @override Sometimes Override if you want to personalize log entries from the DataBasket. * * @param la the event to be logged. * * @exception LogNoOutputStreamException if no OutputStream has been * specified for the log file. * @exception IOException if an IOException occurs when writing to the * log file. */ public void log (Loggable la) throws LogNoOutputStreamException, IOException { if (getContext() != null) { getContext().log (this, la); } else { Log.getGlobalLog().log (la); } } /** * Return the gate at which the process currently stands or which it just left. * * @override Never */ public Gate getCurrentGate() { return m_gCurGate; } /** * Return the initial gate for this process. * * <p>By the time this method gets called, you can assume that the {@link #getBasket working basket} and the * {@link #getContext process context} have been properly initialized.</p> * * @override Always The process will start at the gate that is returned by this method. Therefore, * in order to do anything sensible, you must override this method. */ protected abstract Gate getInitialGate(); /** * Return the gate to jump to when quitting the process. * * <p>Transitions starting at this gate will usually perform a rollback and will then * jump to the "stop" gate.</p> * * @override Sometimes As a default, returns the "rollback" gate. * * @see #getStopGate * @see #getRollbackGate */ public Gate getQuitGate() { return getRollbackGate(); } /** * Return the gate to jump to when an {@link #error error} occurs. * * <p>Transition starting at this gate can perform any specific error handling and * should then arrive at the "rollback" gate.</p> * * <p>When this method is called, {@link #getCurrentGate} will still deliver the * last valid gate.</p> * * @override Sometimes As a default returns the "rollback" gate, unless nErrorNesting is greater * than 1, in which case <code>null</code> is returned to indicate the end of the process. * * @param nErrorNesting a value that indicates nested errors. This value increases with * every new error, so that values greater than 1 indicate errors that occured while * other errors where handled. * * @return As a default returns the "rollback" gate, unless nErrorNesting is * greater than 1, in which case <code>null</code> is returned to indicate the end of * the process. */ protected Gate getErrorGate (int nErrorNesting) { if (nErrorNesting <= 1) { return getRollbackGate(); } else { return null; } } /** * Return the gate to jump to when performing a rollback. * * <p>Transitions starting from this gate must roll back any data structures the process used.</p> * * @override Sometimes As a default returns a gate with a transition that will roll back the DataBasket * attached to the process and will eventually jump to the {@link #getLogGate "log" gate}. * * @see #getStopGate */ public Gate getRollbackGate() { return new Gate() { public Transition getNextTransition (SaleProcess p, User u) { return new Transition() { public Gate perform (SaleProcess p, User u) { DataBasket db = p.getBasket(); if (db != null) { db.rollback(); } return p.getLogGate(); } }; } }; } /** * Return the gate to jump to when performing a commit. * * <p>Transitions starting from this gate must commit any data structures the process used.</p> * * @override Sometimes As a default returns a gate with a transition that will commit the DataBasket * attached to the process and will eventually jump to the {@link #getLogGate "log" gate}. * * @see #getStopGate */ public Gate getCommitGate() { return new Gate() { public Transition getNextTransition (SaleProcess p, User u) { return new Transition() { public Gate perform (SaleProcess p, User u) { DataBasket db = p.getBasket(); if (db != null) { db.commit(); } return p.getLogGate(); } }; } }; } /** * Return the gate that the process must jump to if it wishes to be logged before finishing. * * <p>Transitions from this gate should {@link Log#log log} the process into a log file of their choice and * then proceed to the {@link #getStopGate "stop" gate}.</p> * * @override Sometimes As a default returns a gate with a transition that will log the process using the * process context's {@link ProcessContext#log log()} method.</p> */ public Gate getLogGate() { return new Gate() { public Transition getNextTransition (SaleProcess p, User u) { return new Transition() { public Gate perform (SaleProcess p, User u) { try { p.log (p); } catch (java.io.IOException ioe) { throw new Error ("Exception occurred while logging process: " + ioe); } return p.getStopGate(); } }; } }; } /** * Return the last gate that this process should be at. * * <p> Transitions from this gate must return <code>null</code> instead of a next gate.</p> * * @override Sometimes As a default just return <code>null</code>, indicating no further processing is to be * performed.</p> */ public Gate getStopGate() { return null; } /** * A LogEntryFilter that will accept only such {@link LogEntry LogEntries} that stem from a process. */ public static final LogEntryFilter LOGENTRYFILTER_PROCESSES_ONLY = new LogEntryFilter() { public boolean accept (LogEntry le) { return (le instanceof ProcessLogEntry); } }; }