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