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