    001/*
    002    Licensed to the Apache Software Foundation (ASF) under one
    003    or more contributor license agreements.  See the NOTICE file
    004    distributed with this work for additional information
    005    regarding copyright ownership.  The ASF licenses this file
    006    to you under the Apache License, Version 2.0 (the
    007    "License"); you may not use this file except in compliance
    008    with the License.  You may obtain a copy of the License at
    009
    010       http://www.apache.org/licenses/LICENSE-2.0
    011
    012    Unless required by applicable law or agreed to in writing,
    013    software distributed under the License is distributed on an
    014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015    KIND, either express or implied.  See the License for the
    016    specific language governing permissions and limitations
    017    under the License.
    018 */
    019package org.apache.wiki.workflow;
    020
    021import org.apache.wiki.api.core.Context;
    022import org.apache.wiki.api.exceptions.WikiException;
    023import org.apache.wiki.event.WikiEventEmitter;
    024import org.apache.wiki.event.WorkflowEvent;
    025
    026import java.io.Serializable;
    027import java.security.Principal;
    028import java.util.*;
    029import java.util.concurrent.ConcurrentHashMap;
    030import java.util.concurrent.atomic.AtomicInteger;
    031
    032
    033/**
    034 * <p>
    035 * Sequence of {@link Step} objects linked together. Workflows are always initialized with a message key that denotes the name of the
    036 * Workflow, and a Principal that represents its owner.
    037 * </p>
    038 * <h2>Workflow lifecycle</h2>
    039 * A Workflow's state (obtained by {@link #getCurrentState()}) will be one of the following:
    040 * </p>
    041 * <ul>
    042 * <li><strong>{@link #CREATED}</strong>: after the Workflow has been instantiated, but before it has been started using the {@link #start(Context)}
    043 * method.</li>
    044 * <li><strong>{@link #RUNNING}</strong>: after the Workflow has been started using the {@link #start(Context)} method, but before it has
    045 * finished processing all Steps. Note that a Workflow can only be started once; attempting to start it again results in an
    046 * IllegalStateException. Callers can place the Workflow into the WAITING state by calling {@link #waitstate()}.</li>
    047 * <li><strong>{@link #WAITING}</strong>: when the Workflow has temporarily paused, for example because of a pending Decision. Once the
    048 * responsible actor decides what to do, the caller can change the Workflow back to the RUNNING state by calling the {@link #restart(Context)}
    049 * method (this is done automatically by the Decision class, for instance, when the {@link Decision#decide(Outcome, Context)} method is invoked)</li>
    050 * <li><strong>{@link #COMPLETED}</strong>: after the Workflow has finished processing all Steps, without errors.</li>
    051 * <li><strong>{@link #ABORTED}</strong>: if a Step has elected to abort the Workflow.</li>
    052 * </ul>
    053 * <h2>Steps and processing algorithm</h2>
    054 * <p>
    055 * Workflow Step objects can be of type {@link Decision}, {@link Task} or other Step subclasses. Decisions require user input, while Tasks
    056 * do not. See the {@link Step} class for more details.
    057 * </p>
    058 * <p>
    059 * After instantiating a new Workflow (but before telling it to {@link #start(Context)}), calling classes should specify the first Step by
    060 * executing the {@link #setFirstStep(Step)} method. Additional Steps can be chained by invoking the first step's
    061 * {@link Step#addSuccessor(Outcome, Step)} method.
    062 * </p>
    063 * <p>
    064 * When a Workflow's <code>start</code> method is invoked, the Workflow retrieves the first Step and processes it. This Step, and subsequent
    065 * ones, are processed as follows:
    066 * </p>
    067 * <p>
    068 * <ul>
    069 * <li>The Step's {@link Step#start()} method executes, which sets the start time.</li>
    070 * <li>The Step's {@link Step#execute(Context)} method is called to begin processing, which will return an Outcome to indicate completion,
    071 * continuation or errors:</li>
    072 * <ul>
    073 * <li>{@link Outcome#STEP_COMPLETE} indicates that the execution method ran without errors, and that the Step should be considered
    074 * "completed."</li>
    075 * <li>{@link Outcome#STEP_CONTINUE} indicates that the execution method ran without errors, but that the Step is not "complete" and should
    076 * be put into the WAITING state.</li>
    077 * <li>{@link Outcome#STEP_ABORT} indicates that the execution method encountered errors, and should abort the Step <em>and</em> the
    078 * Workflow as a whole. When this happens, the Workflow will set the current Step's Outcome to {@link Outcome#STEP_ABORT} and invoke the
    079 * Workflow's {@link #abort(Context)} method. The Step's processing errors, if any, can be retrieved by {@link Step#getErrors()}.</li>
    080 * </ul>
    081 * <li>The Outcome of the <code>execute</code> method also affects what happens next. Depending on the result (and assuming the Step did
    082 * not abort), the Workflow will either move on to the next Step or put the Workflow into the {@link Workflow#WAITING} state:</li>
    083 * <ul>
    084 * <li>If the Outcome denoted "completion" (<em>i.e.</em>, its {@link Step#isCompleted()} method returns <code>true</code>) then the Step
    085 * is considered complete; the Workflow looks up the next Step by calling the current Step's {@link Step#getSuccessor(Outcome)} method. If
    086 * <code>successor()</code> returns a non-<code>null</code> Step, the return value is marked as the current Step and added to the Workflow's
    087 * Step history. If <code>successor()</code> returns <code>null</code>, then the Workflow has no more Steps and it enters the
    088 * {@link #COMPLETED} state.</li>
    089 * <li>If the Outcome did not denote "completion" (<em>i.e.</em>, its {@link Step#isCompleted()} method returns <code>false</code>), then
    090 * the Step still has further work to do. The Workflow enters the {@link #WAITING} state and stops further processing until a caller
    091 * restarts it.</li>
    092 * </ul>
    093 * </ul>
    094 * </p>
    095 * <p>
    096 * The currently executing Step can be obtained by {@link #getCurrentStep()}. The actor for the current Step is returned by
    097 * {@link #getCurrentActor()}.
    098 * </p>
    099 * <p>
    100 * To provide flexibility for specific implementations, the Workflow class provides two additional features that enable Workflow
    101 * participants (<em>i.e.</em>, Workflow subclasses and Step/Task/Decision subclasses) to share context and state information. These two
    102 * features are <em>named attributes</em> and <em>message arguments</em>:
    103 * </p>
    104 * <ul>
    105 * <li><strong>Named attributes</strong> are simple key-value pairs that Workflow participants can get or set. Keys are Strings; values
    106 * can be any Object. Named attributes are set with {@link #setAttribute(String, Serializable)} and retrieved with {@link #getAttribute(String)}.</li>
    107 * <li><strong>Message arguments</strong> are used in combination with JSPWiki's {@link org.apache.wiki.i18n.InternationalizationManager} to
    108 * create language-independent user interface messages. The message argument array is retrieved via {@link #getMessageArguments()}; the
    109 * first two array elements will always be these: a String representing work flow owner's name, and a String representing the current
    110 * actor's name. Workflow participants can add to this array by invoking {@link #addMessageArgument(Serializable)}.</li>
    111 * </ul>
    112 * <h2>Example</h2>
    113 * <p>
    114 * Workflow Steps can be very powerful when linked together. JSPWiki provides two abstract subclasses classes that you can use to build
    115 * your own Workflows: Tasks and Decisions. As noted, Tasks are Steps that execute without user intervention, while Decisions require
    116 * actors (<em>aka</em> Principals) to take action. Decisions and Tasks can be mixed freely to produce some highly elaborate branching
    117 * structures.
    118 * </p>
    119 * <p>
    120 * Here is a simple case. For example, suppose you would like to create a Workflow that (a) executes a initialization Task, (b) pauses to
    121 * obtain an approval Decision from a user in the Admin group, and if approved, (c) executes a "finish" Task. Here's sample code that
    122 * illustrates how to do it:
    123 * </p>
    124 *
    125 * <pre>
    126 *    // Create workflow; owner is current user
    127 * 1  Workflow workflow = new Workflow( &quot; workflow.myworkflow &quot;, context.getCurrentUser() );
    128 *
    129 *    // Create custom initialization task
    130 * 2  Step initTask = new InitTask( this );
    131 *
    132 *    // Create finish task
    133 * 3  Step finishTask = new FinishTask( this );
    134 *
    135 *    // Create an intermediate decision step
    136 * 4  Principal actor = new GroupPrincipal( &quot;Admin&quot; );
    137 * 5  Step decision = new SimpleDecision( this, &quot;decision.AdminDecision&quot;, actor );
    138 *
    139 *    // Hook the steps together
    140 * 6  initTask.addSuccessor( Outcome.STEP_COMPLETE, decision );
    141 * 7  decision.addSuccessor( Outcome.DECISION_APPROVE, finishTask );
    142 *
    143 *    // Set workflow's first step
    144 * 8  workflow.setFirstStep( initTask );
    145 * </pre>
    146 *
    147 * <p>
    148 * Some comments on the source code:
    149 * </p>
    150 * <ul>
    151 * <li>Line 1 instantiates the workflow with a sample message key and designated owner Principal, in this case the current wiki user</li>
    152 * <li>Lines 2 and 3 instantiate the custom Task subclasses, which contain the business logic</li>
    153 * <li>Line 4 creates the relevant GroupPrincipal for the <code>Admin</code> group, who will be the actor in the Decision step</li>
    154 * <li>Line 5 creates the Decision step, passing the Workflow, sample message key, and actor in the constructor</li>
    155 * <li>Line 6 specifies that if the InitTask's Outcome signifies "normal completion" (STEP_COMPLETE), the SimpleDecision step should be
    156 * invoked next</li>
    157 * <li>Line 7 specifies that if the actor (anyone possessing the <code>Admin</code> GroupPrincipal) selects DECISION_APPROVE, the FinishTask
    158 * step should be invoked</li>
    159 * <li>Line 8 adds the InitTask (and all of its successor Steps, nicely wired together) to the workflow</li>
    160 * </ul>
    161 */
    162public class Workflow implements Serializable {
    163
    164    private static final long serialVersionUID = 5228149040690660032L;
    165
    166    private static final AtomicInteger idsCounter = new AtomicInteger( 1 );
    167
    168    /** ID value: the workflow ID has not been set. */
    169    public static final int ID_NOT_SET = 0;
    170
    171    /** State value: Workflow completed all Steps without errors. */
    172    public static final int COMPLETED = 50;
    173
    174    /** State value: Workflow aborted before completion. */
    175    public static final int ABORTED = 40;
    176
    177    /** State value: Workflow paused, typically because a Step returned an Outcome that doesn't signify "completion." */
    178    public static final int WAITING = 30;
    179
    180    /** State value: Workflow started, and is running. */
    181    public static final int RUNNING = -1;
    182
    183    /** State value: Workflow instantiated, but not started. */
    184    public static final int CREATED = -2;
    185
    186    /** attribute map. */
    187    private Map< String, Serializable > m_attributes;
    188
    189    /** The initial Step for this Workflow. */
    190    private Step m_firstStep;
    191
    192    /** Flag indicating whether the Workflow has started yet. */
    193    private boolean m_started;
    194
    195    private final LinkedList< Step > m_history;
    196
    197    private int m_id;
    198
    199    private final String m_key;
    200
    201    private final Principal m_owner;
    202
    203    private final List<Serializable> m_messageArgs;
    204
    205    private int m_state;
    206
    207    private Step m_currentStep;
    208
    209    /**
    210     * Constructs a new Workflow object with a supplied message key, owner Principal, and undefined unique identifier {@link #ID_NOT_SET}.
    211     * Once instantiated the Workflow is considered to be in the {@link #CREATED} state; a caller must explicitly invoke the
    212     * {@link #start(Context)} method to begin processing.
    213     *
    214     * @param messageKey the message key used to construct a localized workflow name, such as <code>workflow.saveWikiPage</code>
    215     * @param owner the Principal who owns the Workflow. Typically, this is the user who created and submitted it
    216     */
    217    public Workflow( final String messageKey, final Principal owner ) {
    218        m_attributes = new ConcurrentHashMap<>();
    219        m_currentStep = null;
    220        m_history = new LinkedList<>();
    221        m_id = idsCounter.getAndIncrement();
    222        m_key = messageKey;
    223        m_messageArgs = new ArrayList<>();
    224        m_owner = owner;
    225        m_started = false;
    226        m_state = CREATED;
    227        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.CREATED );
    228    }
    229
    230    /**
    231     * Aborts the Workflow by setting the current Step's Outcome to {@link Outcome#STEP_ABORT}, and the Workflow's overall state to
    232     * {@link #ABORTED}. It also appends the aborted Step into the workflow history, and sets the current step to <code>null</code>.
    233     * If the Step is a Decision, it is removed from the DecisionQueue. This method can be called at any point in the lifecycle prior
    234     * to completion, but it cannot be called twice. It finishes by calling the {@link #cleanup()} method to flush retained objects.
    235     * If the Workflow had been previously aborted, this method throws an IllegalStateException.
    236     */
    237    public final synchronized void abort( final Context context ) {
    238        // Check corner cases: previous abort or completion
    239        if( m_state == ABORTED ) {
    240            throw new IllegalStateException( "The workflow has already been aborted." );
    241        }
    242        if( m_state == COMPLETED ) {
    243            throw new IllegalStateException( "The workflow has already completed." );
    244        }
    245
    246        if( m_currentStep != null ) {
    247            if( m_currentStep instanceof Decision ) {
    248                WikiEventEmitter.fireWorkflowEvent( m_currentStep, WorkflowEvent.DQ_REMOVAL, context );
    249            }
    250            m_currentStep.setOutcome( Outcome.STEP_ABORT );
    251            m_history.addLast( m_currentStep );
    252        }
    253        m_state = ABORTED;
    254        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.ABORTED );
    255        cleanup();
    256    }
    257
    258    /**
    259     * Appends a message argument object to the array returned by {@link #getMessageArguments()}. The object <em>must</em> be an type
    260     * used by the {@link java.text.MessageFormat}: String, Date, or Number (BigDecimal, BigInteger, Byte, Double, Float, Integer, Long,
    261     * Short). If the object is not of type String, Number or Date, this method throws an IllegalArgumentException.
    262     *
    263     * @param obj the object to add
    264     */
    265    public final void addMessageArgument( final Serializable obj ) {
    266        if( obj instanceof String || obj instanceof Date || obj instanceof Number ) {
    267            m_messageArgs.add( obj );
    268            return;
    269        }
    270        throw new IllegalArgumentException( "Message arguments must be of type String, Date or Number." );
    271    }
    272
    273    /**
    274     * Returns the actor Principal responsible for the current Step. If there is
    275     * no current Step, this method returns <code>null</code>.
    276     *
    277     * @return the current actor
    278     */
    279    public final synchronized Principal getCurrentActor() {
    280        if( m_currentStep == null ) {
    281            return null;
    282        }
    283        return m_currentStep.getActor();
    284    }
    285
    286    /**
    287     * Returns the workflow state: {@link #CREATED}, {@link #RUNNING}, {@link #WAITING}, {@link #COMPLETED} or {@link #ABORTED}.
    288     *
    289     * @return the workflow state
    290     */
    291    public final int getCurrentState()
    292    {
    293        return m_state;
    294    }
    295
    296    /**
    297     * Returns the current Step, or <code>null</code> if the workflow has not started or already completed.
    298     *
    299     * @return the current step
    300     */
    301    public final Step getCurrentStep()
    302    {
    303        return m_currentStep;
    304    }
    305
    306    /**
    307     * Retrieves a named Object associated with this Workflow. If the Workflow has completed or aborted, this method always returns
    308     * <code>null</code>.
    309     *
    310     * @param attr the name of the attribute
    311     * @return the value
    312     */
    313    public final Object getAttribute( final String attr ) {
    314        return m_attributes.get( attr );
    315    }
    316
    317    /**
    318     * Retrieves workflow's attributes.
    319     *
    320     * @return workflow's attributes.
    321     */
    322    public final Map< String, Serializable > getAttributes() {
    323        return m_attributes;
    324    }
    325
    326    /**
    327     * The end time for this Workflow, expressed as a system time number. This value is equal to the end-time value returned by the final
    328     * Step's {@link Step#getEndTime()} method, if the workflow has completed. Otherwise, this method returns {@link Step#TIME_NOT_SET}.
    329     *
    330     * @return the end time
    331     */
    332    public final Date getEndTime() {
    333        if( isCompleted() ) {
    334            final Step last = m_history.getLast();
    335            if( last != null ) {
    336                return last.getEndTime();
    337            }
    338        }
    339        return Step.TIME_NOT_SET;
    340    }
    341
    342    /**
    343     * Returns the unique identifier for this Workflow. If not set, this method returns ID_NOT_SET ({@value #ID_NOT_SET}).
    344     *
    345     * @return the unique identifier
    346     */
    347    public final synchronized int getId()
    348    {
    349        return m_id;
    350    }
    351
    352    /**
    353     * <p>
    354     * Returns an array of message arguments, used by {@link java.text.MessageFormat} to create localized messages. The first
    355     * two array elements will always be these:
    356     * </p>
    357     * <ul>
    358     * <li>String representing the name of the workflow owner (<em>i.e.</em>,{@link #getOwner()})</li>
    359     * <li>String representing the name of the current actor (<em>i.e.</em>,{@link #getCurrentActor()}).
    360     * If the current step is <code>null</code> because the workflow hasn't started or has already
    361     * finished, the value of this argument will be a dash character (<code>-</code>)</li>
    362     * </ul>
    363     * <p>
    364     * Workflow and Step subclasses are free to append items to this collection with {@link #addMessageArgument(Serializable)}.
    365     * </p>
    366     *
    367     * @return the array of message arguments
    368     */
    369    public final Serializable[] getMessageArguments() {
    370        final List< Serializable > args = new ArrayList<>();
    371        args.add( m_owner.getName() );
    372        final Principal actor = getCurrentActor();
    373        args.add( actor == null ? "-" : actor.getName() );
    374        args.addAll( m_messageArgs );
    375        return args.toArray( new Serializable[0] );
    376    }
    377
    378    /**
    379     * Returns an i18n message key for the name of this workflow; for example,
    380     * <code>workflow.saveWikiPage</code>.
    381     *
    382     * @return the name
    383     */
    384    public final String getMessageKey()
    385    {
    386        return m_key;
    387    }
    388
    389    /**
    390     * The owner Principal on whose behalf this Workflow is being executed; that is, the user who created the workflow.
    391     *
    392     * @return the name of the Principal who owns this workflow
    393     */
    394    public final Principal getOwner()
    395    {
    396        return m_owner;
    397    }
    398
    399    /**
    400     * The start time for this Workflow, expressed as a system time number. This value is equal to the start-time value returned by the
    401     * first Step's {@link Step#getStartTime()} method, if the workflow has started already. Otherwise, this method returns
    402     * {@link Step#TIME_NOT_SET}.
    403     *
    404     * @return the start time
    405     */
    406    public final Date getStartTime()
    407    {
    408        return isStarted() ? m_firstStep.getStartTime() : Step.TIME_NOT_SET;
    409    }
    410
    411    /**
    412     * Returns a Step history for this Workflow as a List, chronologically, from the first Step to the currently executing one. The first
    413     * step is the first item in the array. If the Workflow has not started, this method returns a zero-length array.
    414     *
    415     * @return an array of Steps representing those that have executed, or are currently executing
    416     */
    417    public final List< Step > getHistory()
    418    {
    419        return Collections.unmodifiableList( m_history );
    420    }
    421
    422    /**
    423     * Returns <code>true</code> if the workflow had been previously aborted.
    424     *
    425     * @return the result
    426     */
    427    public final boolean isAborted()
    428    {
    429        return m_state == ABORTED;
    430    }
    431
    432    /**
    433     * Determines whether this Workflow is completed; that is, if it has no additional Steps to perform. If the last Step in the workflow is
    434     * finished, this method will return <code>true</code>.
    435     *
    436     * @return <code>true</code> if the workflow has been started but has no more steps to perform; <code>false</code> if not.
    437     */
    438    public final synchronized boolean isCompleted() {
    439        // If current step is null, then we're done
    440        return m_started && m_state == COMPLETED;
    441    }
    442
    443    /**
    444     * Determines whether this Workflow has started; that is, its {@link #start(Context)} method has been executed.
    445     *
    446     * @return <code>true</code> if the workflow has been started; <code>false</code> if not.
    447     */
    448    public final boolean isStarted()
    449    {
    450        return m_started;
    451    }
    452
    453    /**
    454     * Convenience method that returns the predecessor of the current Step. This method simply examines the Workflow history and returns the
    455     * second-to-last Step.
    456     *
    457     * @return the predecessor, or <code>null</code> if the first Step is currently executing
    458     */
    459    public final Step getPreviousStep()
    460    {
    461        return previousStep( m_currentStep );
    462    }
    463
    464    /**
    465     * Restarts the Workflow from the {@link #WAITING} state and puts it into the {@link #RUNNING} state again. If the Workflow had not
    466     * previously been paused, this method throws an IllegalStateException. If any of the Steps in this Workflow throw a WikiException,
    467     * the Workflow will abort and propagate the exception to callers.
    468     *
    469     * @param context current wiki context
    470     * @throws WikiException if the current task's {@link Task#execute( Context )} method throws an exception
    471     */
    472    public final synchronized void restart( final Context context ) throws WikiException {
    473        if( m_state != WAITING ) {
    474            throw new IllegalStateException( "Workflow is not paused; cannot restart." );
    475        }
    476        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.STARTED );
    477        m_state = RUNNING;
    478        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.RUNNING );
    479
    480        // Process current step
    481        try {
    482            processCurrentStep( context );
    483        } catch( final WikiException e ) {
    484            abort( context );
    485            throw e;
    486        }
    487    }
    488
    489    /**
    490     * Temporarily associates an object with this Workflow, as a named attribute, for the duration of workflow execution. The passed
    491     * object can be anything required by an executing Step, although it <em>should</em> be serializable. Note that when the workflow
    492     * completes or aborts, all attributes will be cleared.
    493     *
    494     * @param attr the attribute name
    495     * @param obj  the value
    496     */
    497    public final void setAttribute( final String attr, final Serializable obj ) {
    498        m_attributes.put( attr, obj );
    499    }
    500
    501    /**
    502     * Sets the first Step for this Workflow, which will be executed immediately
    503     * after the {@link #start( Context )} method executes. Note than the Step is not
    504     * marked as the "current" step or added to the Workflow history until the
    505     * {@link #start( Context )} method is called.
    506     *
    507     * @param step the first step for the workflow
    508     */
    509    public final synchronized void setFirstStep( final Step step )
    510    {
    511        m_firstStep = step;
    512    }
    513
    514    /**
    515     * Sets the unique identifier for this Workflow.
    516     *
    517     * @param id the unique identifier
    518     */
    519    public final synchronized void setId( final int id )
    520    {
    521        this.m_id = id;
    522    }
    523
    524    /**
    525     * Starts the Workflow and sets the state to {@link #RUNNING}. If the Workflow has already been started (or previously aborted), this
    526     * method returns an {@linkplain IllegalStateException}. If any of the Steps in this Workflow throw a WikiException, the Workflow will
    527     * abort and propagate the exception to callers.
    528     *
    529     * @param context current wiki context.
    530     * @throws WikiException if the current Step's {@link Step#start()} method throws an exception of any kind
    531     */
    532    public final synchronized void start( final Context context ) throws WikiException {
    533        if( m_state == ABORTED ) {
    534            throw new IllegalStateException( "Workflow cannot be started; it has already been aborted." );
    535        }
    536        if( m_started ) {
    537            throw new IllegalStateException( "Workflow has already started." );
    538        }
    539        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.STARTED );
    540        m_started = true;
    541        m_state = RUNNING;
    542
    543        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.RUNNING );
    544        // Mark the first step as the current one & add to history
    545        m_currentStep = m_firstStep;
    546        m_history.add( m_currentStep );
    547
    548        // Process current step
    549        try {
    550            processCurrentStep( context );
    551        } catch( final WikiException e ) {
    552            abort( context );
    553            throw e;
    554        }
    555    }
    556
    557    /**
    558     * Sets the Workflow in the {@link #WAITING} state. If the Workflow is not running or has already been paused, this method throws an
    559     * IllegalStateException. Once paused, the Workflow can be un-paused by executing the {@link #restart(Context)} method.
    560     */
    561    public final synchronized void waitstate() {
    562        if ( m_state != RUNNING ) {
    563            throw new IllegalStateException( "Workflow is not running; cannot pause." );
    564        }
    565        m_state = WAITING;
    566        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.WAITING );
    567    }
    568
    569    /**
    570     * Clears the attribute map and sets the current step field to <code>null</code>.
    571     */
    572    protected void cleanup() {
    573        m_currentStep = null;
    574        m_attributes = null;
    575    }
    576
    577    /**
    578     * Protected helper method that changes the Workflow's state to {@link #COMPLETED} and sets the current Step to <code>null</code>. It
    579     * calls the {@link #cleanup()} method to flush retained objects. This method will no-op if it has previously been called.
    580     */
    581    protected final synchronized void complete() {
    582        if( !isCompleted() ) {
    583            m_state = COMPLETED;
    584            WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.COMPLETED );
    585            cleanup();
    586        }
    587    }
    588
    589    /**
    590     * Protected method that returns the predecessor for a supplied Step.
    591     *
    592     * @param step the Step for which the predecessor is requested
    593     * @return its predecessor, or <code>null</code> if the first Step was supplied.
    594     */
    595    protected final Step previousStep( final Step step ) {
    596        final int index = m_history.indexOf( step );
    597        return index < 1 ? null : m_history.get( index - 1 );
    598    }
    599
    600    /**
    601     * Protected method that processes the current Step by calling {@link Step#execute( Context )}. If the <code>execute</code> throws an
    602     * exception, this method will propagate the exception immediately to callers without aborting.
    603     *
    604     * @throws WikiException if the current Step's {@link Step#start()} method throws an exception of any kind
    605     */
    606    protected final void processCurrentStep( final Context context ) throws WikiException {
    607        while ( m_currentStep != null ) {
    608            // Start and execute the current step
    609            if( !m_currentStep.isStarted() ) {
    610                m_currentStep.start();
    611            }
    612            final Outcome result = m_currentStep.execute( context );
    613            if( Outcome.STEP_ABORT.equals( result ) ) {
    614                abort( context );
    615                break;
    616            }
    617
    618            if( !m_currentStep.isCompleted() ) {
    619                m_currentStep.setOutcome( result );
    620            }
    621
    622            // Get the execution Outcome; if not complete, pause workflow and exit
    623            final Outcome outcome = m_currentStep.getOutcome();
    624            if ( !outcome.isCompletion() ) {
    625                waitstate();
    626                break;
    627            }
    628
    629            // Get the next Step; if null, we're done
    630            final Step nextStep = m_currentStep.getSuccessor( outcome );
    631            if ( nextStep == null ) {
    632                complete();
    633                break;
    634            }
    635
    636            // Add the next step to Workflow history, and mark as current
    637            m_history.add( nextStep );
    638            m_currentStep = nextStep;
    639        }
    640
    641    }
    642
    643}