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