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     */
019    package org.apache.wiki.workflow;
020    
021    import java.io.Serializable;
022    import java.security.Principal;
023    import java.util.*;
024    
025    import org.apache.wiki.api.exceptions.WikiException;
026    import org.apache.wiki.event.WikiEventListener;
027    import org.apache.wiki.event.WikiEventManager;
028    import 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     */
199    public 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    }