001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.workflow;
021:
022: import java.security.Principal;
023: import java.util.*;
024:
025: import com.ecyrd.jspwiki.WikiException;
026: import com.ecyrd.jspwiki.event.WikiEventListener;
027: import com.ecyrd.jspwiki.event.WikiEventManager;
028: import com.ecyrd.jspwiki.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 com.ecyrd.jspwiki.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(Object)}.</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("workflow.myworkflow", 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("Admin");
167: * 5 Step decision = new SimpleDecision(this, "decision.AdminDecision", 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: * @author Andrew Jaquith
199: */
200: public class Workflow {
201: /** Time value: the start or end time has not been set. */
202: public static final Date TIME_NOT_SET = new Date(0);
203:
204: /** ID value: the workflow ID has not been set. */
205: public static final int ID_NOT_SET = 0;
206:
207: /** State value: Workflow completed all Steps without errors. */
208: public static final int COMPLETED = 50;
209:
210: /** State value: Workflow aborted before completion. */
211: public static final int ABORTED = 40;
212:
213: /**
214: * State value: Workflow paused, typically because a Step returned an
215: * Outcome that doesn't signify "completion."
216: */
217: public static final int WAITING = 30;
218:
219: /** State value: Workflow started, and is running. */
220: public static final int RUNNING = -1;
221:
222: /** State value: Workflow instantiated, but not started. */
223: public static final int CREATED = -2;
224:
225: /** Lazily-initialized attribute map. */
226: private Map m_attributes;
227:
228: /** The initial Step for this Workflow. */
229: private Step m_firstStep;
230:
231: /** Flag indicating whether the Workflow has started yet. */
232: private boolean m_started;
233:
234: private final LinkedList m_history;
235:
236: private int m_id;
237:
238: private final String m_key;
239:
240: private final Principal m_owner;
241:
242: private final List m_messageArgs;
243:
244: private int m_state;
245:
246: private Step m_currentStep;
247:
248: private WorkflowManager m_manager;
249:
250: /**
251: * Constructs a new Workflow object with a supplied message key, owner
252: * Principal, and undefined unique identifier {@link #ID_NOT_SET}. Once
253: * instantiated the Workflow is considered to be in the {@link #CREATED}
254: * state; a caller must explicitly invoke the {@link #start()} method to
255: * begin processing.
256: *
257: * @param messageKey
258: * the message key used to construct a localized workflow name,
259: * such as <code>workflow.saveWikiPage</code>
260: * @param owner
261: * the Principal who owns the Workflow. Typically, this is the
262: * user who created and submitted it
263: */
264: public Workflow(String messageKey, Principal owner) {
265: super ();
266: m_attributes = null;
267: m_currentStep = null;
268: m_history = new LinkedList();
269: m_id = ID_NOT_SET;
270: m_key = messageKey;
271: m_manager = null;
272: m_messageArgs = new ArrayList();
273: m_owner = owner;
274: m_started = false;
275: m_state = CREATED;
276: }
277:
278: /**
279: * Aborts the Workflow by setting the current Step's Outcome to
280: * {@link Outcome#STEP_ABORT}, and the Workflow's overall state to
281: * {@link #ABORTED}. It also appends the aborted Step into the workflow
282: * history, and sets the current step to <code>null</code>. If the Step
283: * is a Decision, it is removed from the DecisionQueue. This method
284: * can be called at any point in the lifecycle prior to completion, but it
285: * cannot be called twice. It finishes by calling the {@link #cleanup()}
286: * method to flush retained objects. If the Workflow had been previously
287: * aborted, this method throws an IllegalStateException.
288: */
289: public final synchronized void abort() {
290: // Check corner cases: previous abort or completion
291: if (m_state == ABORTED) {
292: throw new IllegalStateException(
293: "The workflow has already been aborted.");
294: }
295: if (m_state == COMPLETED) {
296: throw new IllegalStateException(
297: "The workflow has already completed.");
298: }
299:
300: if (m_currentStep != null) {
301: if (m_manager != null && m_currentStep instanceof Decision) {
302: Decision d = (Decision) m_currentStep;
303: m_manager.getDecisionQueue().remove(d);
304: }
305: m_currentStep.setOutcome(Outcome.STEP_ABORT);
306: m_history.addLast(m_currentStep);
307: }
308: m_state = ABORTED;
309: fireEvent(WorkflowEvent.ABORTED);
310: cleanup();
311: }
312:
313: /**
314: * Appends a message argument object to the array returned by
315: * {@link #getMessageArguments()}. The object <em>must</em> be an type
316: * used by the {@link java.text.MessageFormat}: String, Date, or Number
317: * (BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short).
318: * If the object is not of type String, Number or Date, this method throws
319: * an IllegalArgumentException.
320: * @param obj the object to add
321: */
322: public final void addMessageArgument(Object obj) {
323: if (obj instanceof String || obj instanceof Date
324: || obj instanceof Number) {
325: m_messageArgs.add(obj);
326: return;
327: }
328: throw new IllegalArgumentException(
329: "Message arguments must be of type String, Date or Number.");
330: }
331:
332: /**
333: * Returns the actor Principal responsible for the current Step. If there is
334: * no current Step, this method returns <code>null</code>.
335: *
336: * @return the current actor
337: */
338: public final synchronized Principal getCurrentActor() {
339: if (m_currentStep == null) {
340: return null;
341: }
342: return m_currentStep.getActor();
343: }
344:
345: /**
346: * Returns the workflow state: {@link #CREATED}, {@link #RUNNING},
347: * {@link #WAITING}, {@link #COMPLETED} or {@link #ABORTED}.
348: *
349: * @return the workflow state
350: */
351: public final int getCurrentState() {
352: return m_state;
353: }
354:
355: /**
356: * Returns the current Step, or <code>null</code> if the workflow has not
357: * started or already completed.
358: *
359: * @return the current step
360: */
361: public final Step getCurrentStep() {
362: return m_currentStep;
363: }
364:
365: /**
366: * Retrieves a named Object associated with this Workflow. If the Workflow
367: * has completed or aborted, this method always returns <code>null</code>.
368: *
369: * @param attr
370: * the name of the attribute
371: * @return the value
372: */
373: public final synchronized Object getAttribute(String attr) {
374: if (m_attributes == null) {
375: return null;
376: }
377: return m_attributes.get(attr);
378: }
379:
380: /**
381: * The end time for this Workflow, expressed as a system time number. This
382: * value is equal to the end-time value returned by the final Step's
383: * {@link Step#getEndTime()} method, if the workflow has completed.
384: * Otherwise, this method returns {@link #TIME_NOT_SET}.
385: *
386: * @return the end time
387: */
388: public final Date getEndTime() {
389: if (isCompleted()) {
390: Step last = (Step) m_history.getLast();
391: if (last != null) {
392: return last.getEndTime();
393: }
394: }
395: return TIME_NOT_SET;
396: }
397:
398: /**
399: * Returns the unique identifier for this Workflow. If not set, this method
400: * returns ID_NOT_SET ({@value #ID_NOT_SET}).
401: *
402: * @return the unique identifier
403: */
404: public final synchronized int getId() {
405: return m_id;
406: }
407:
408: /**
409: * <p>
410: * Returns an array of message arguments, used by
411: * {@link java.text.MessageFormat} to create localized messages. The first
412: * two array elements will always be these:
413: * </p>
414: * <ul>
415: * <li>String representing the name of the workflow owner (<em>i.e.</em>,{@link #getOwner()})</li>
416: * <li>String representing the name of the current actor (<em>i.e.</em>,{@link #getCurrentActor()}).
417: * If the current step is <code>null</code> because the workflow hasn't started or has already
418: * finished, the value of this argument will be a dash character (<code>-</code>)</li>
419: * </ul>
420: * <p>
421: * Workflow and Step subclasses are free to append items to this collection
422: * with {@link #addMessageArgument(Object)}.
423: * </p>
424: *
425: * @return the array of message arguments
426: */
427: public final Object[] getMessageArguments() {
428: List args = new ArrayList();
429: args.add(m_owner.getName());
430: Principal actor = getCurrentActor();
431: args.add(actor == null ? "-" : actor.getName());
432: args.addAll(m_messageArgs);
433: return args.toArray(new Object[args.size()]);
434: }
435:
436: /**
437: * Returns an i18n message key for the name of this workflow; for example,
438: * <code>workflow.saveWikiPage</code>.
439: *
440: * @return the name
441: */
442: public final String getMessageKey() {
443: return m_key;
444: }
445:
446: /**
447: * The owner Principal on whose behalf this Workflow is being executed; that
448: * is, the user who created the workflow.
449: *
450: * @return the name of the Principal who owns this workflow
451: */
452: public final Principal getOwner() {
453: return m_owner;
454: }
455:
456: /**
457: * The start time for this Workflow, expressed as a system time number. This
458: * value is equal to the start-time value returned by the first Step's
459: * {@link Step#getStartTime()} method, if the workflow has started already.
460: * Otherwise, this method returns {@link #TIME_NOT_SET}.
461: *
462: * @return the start time
463: */
464: public final Date getStartTime() {
465: return isStarted() ? m_firstStep.getStartTime() : TIME_NOT_SET;
466: }
467:
468: /**
469: * Returns the WorkflowManager that contains this Workflow.
470: *
471: * @return the workflow manager
472: */
473: public final synchronized WorkflowManager getWorkflowManager() {
474: return m_manager;
475: }
476:
477: /**
478: * Returns a Step history for this Workflow as a List, chronologically, from the
479: * first Step to the currently executing one. The first step is the first
480: * item in the array. If the Workflow has not started, this method returns a
481: * zero-length array.
482: *
483: * @return an array of Steps representing those that have executed, or are
484: * currently executing
485: */
486: public final List getHistory() {
487: return Collections.unmodifiableList(m_history);
488: }
489:
490: /**
491: * Returns <code>true</code> if the workflow had been previously aborted.
492: *
493: * @return the result
494: */
495: public final boolean isAborted() {
496: return m_state == ABORTED;
497: }
498:
499: /**
500: * Determines whether this Workflow is completed; that is, if it has no
501: * additional Steps to perform. If the last Step in the workflow is
502: * finished, this method will return <code>true</code>.
503: *
504: * @return <code>true</code> if the workflow has been started but has no
505: * more steps to perform; <code>false</code> if not.
506: */
507: public final synchronized boolean isCompleted() {
508: // If current step is null, then we're done
509: return m_started && m_state == COMPLETED;
510: }
511:
512: /**
513: * Determines whether this Workflow has started; that is, its
514: * {@link #start()} method has been executed.
515: *
516: * @return <code>true</code> if the workflow has been started;
517: * <code>false</code> if not.
518: */
519: public final boolean isStarted() {
520: return m_started;
521: }
522:
523: /**
524: * Convenience method that returns the predecessor of the current Step. This
525: * method simply examines the Workflow history and returns the
526: * second-to-last Step.
527: *
528: * @return the predecessor, or <code>null</code> if the first Step is
529: * currently executing
530: */
531: public final Step getPreviousStep() {
532: return previousStep(m_currentStep);
533: }
534:
535: /**
536: * Restarts the Workflow from the {@link #WAITING} state and puts it into
537: * the {@link #RUNNING} state again. If the Workflow had not previously been
538: * paused, this method throws an IllegalStateException. If any of the
539: * Steps in this Workflow throw a WikiException, the Workflow will abort
540: * and propagate the exception to callers.
541: * @throws WikiException if the current task's {@link Task#execute()} method
542: * throws an exception
543: */
544: public final synchronized void restart() throws WikiException {
545: if (m_state != WAITING) {
546: throw new IllegalStateException(
547: "Workflow is not paused; cannot restart.");
548: }
549: m_state = RUNNING;
550: fireEvent(WorkflowEvent.RUNNING);
551:
552: // Process current step
553: try {
554: processCurrentStep();
555: } catch (WikiException e) {
556: abort();
557: throw e;
558: }
559: }
560:
561: /**
562: * Temporarily associates an Object with this Workflow, as a named attribute, for the
563: * duration of workflow execution. The passed Object can be anything required by
564: * an executing Step. Note that when the workflow completes or aborts, all
565: * attributes will be cleared.
566: *
567: * @param attr
568: * the attribute name
569: * @param obj
570: * the value
571: */
572: public final synchronized void setAttribute(String attr, Object obj) {
573: if (m_attributes == null) {
574: m_attributes = new HashMap();
575: }
576: m_attributes.put(attr, obj);
577: }
578:
579: /**
580: * Sets the first Step for this Workflow, which will be executed immediately
581: * after the {@link #start()} method executes. Note than the Step is not
582: * marked as the "current" step or added to the Workflow history until the
583: * {@link #start()} method is called.
584: *
585: * @param step
586: * the first step for the workflow
587: */
588: public final synchronized void setFirstStep(Step step) {
589: m_firstStep = step;
590: }
591:
592: /**
593: * Sets the unique identifier for this Workflow.
594: *
595: * @param id
596: * the unique identifier
597: */
598: public final synchronized void setId(int id) {
599: this .m_id = id;
600: }
601:
602: /**
603: * Sets the WorkflowManager that contains this Workflow.
604: *
605: * @param manager
606: * the workflow manager
607: */
608: public final synchronized void setWorkflowManager(
609: WorkflowManager manager) {
610: m_manager = manager;
611: addWikiEventListener(manager);
612: }
613:
614: /**
615: * Starts the Workflow and sets the state to {@link #RUNNING}. If the
616: * Workflow has already been started (or previously aborted), this method
617: * returns an {@linkplain IllegalStateException}. If any of the
618: * Steps in this Workflow throw a WikiException, the Workflow will abort
619: * and propagate the exception to callers.
620: * @throws WikiException if the current Step's {@link Step#start()}
621: * method throws an exception of any kind
622: */
623: public final synchronized void start() throws WikiException {
624: if (m_state == ABORTED) {
625: throw new IllegalStateException(
626: "Workflow cannot be started; it has already been aborted.");
627: }
628: if (m_started) {
629: throw new IllegalStateException(
630: "Workflow has already started.");
631: }
632: m_started = true;
633: m_state = RUNNING;
634: fireEvent(WorkflowEvent.RUNNING);
635:
636: // Mark the first step as the current one & add to history
637: m_currentStep = m_firstStep;
638: m_history.add(m_currentStep);
639:
640: // Process current step
641: try {
642: processCurrentStep();
643: } catch (WikiException e) {
644: abort();
645: throw e;
646: }
647: }
648:
649: /**
650: * Sets the Workflow in the {@link #WAITING} state. If the Workflow is not
651: * running or has already been paused, this method throws an
652: * IllegalStateException. Once paused, the Workflow can be un-paused by
653: * executing the {@link #restart()} method.
654: */
655: public final synchronized void waitstate() {
656: if (m_state != RUNNING) {
657: throw new IllegalStateException(
658: "Workflow is not running; cannot pause.");
659: }
660: m_state = WAITING;
661: fireEvent(WorkflowEvent.WAITING);
662: }
663:
664: /**
665: * Clears the attribute map and sets the current step field to
666: * <code>null</code>.
667: */
668: protected void cleanup() {
669: m_currentStep = null;
670: m_attributes = null;
671: }
672:
673: /**
674: * Protected helper method that changes the Workflow's state to
675: * {@link #COMPLETED} and sets the current Step to <code>null</code>. It
676: * calls the {@link #cleanup()} method to flush retained objects.
677: * This method will no-op if it has previously been called.
678: */
679: protected final synchronized void complete() {
680: if (!isCompleted()) {
681: m_state = COMPLETED;
682: fireEvent(WorkflowEvent.COMPLETED);
683: cleanup();
684: }
685: }
686:
687: /**
688: * Protected method that returns the predecessor for a supplied Step.
689: *
690: * @param step
691: * the Step for which the predecessor is requested
692: * @return its predecessor, or <code>null</code> if the first Step was
693: * supplied.
694: */
695: protected final Step previousStep(Step step) {
696: int index = m_history.indexOf(step);
697: return index < 1 ? null : (Step) m_history.get(index - 1);
698: }
699:
700: /**
701: * Protected method that processes the current Step by calling
702: * {@link Step#execute()}. If the <code>execute</code> throws an
703: * exception, this method will propagate the exception immediately
704: * to callers without aborting.
705: * @throws WikiException if the current Step's {@link Step#start()}
706: * method throws an exception of any kind
707: */
708: protected final void processCurrentStep() throws WikiException {
709: while (m_currentStep != null) {
710:
711: // Start and execute the current step
712: if (!m_currentStep.isStarted()) {
713: m_currentStep.start();
714: }
715: try {
716: Outcome result = m_currentStep.execute();
717: if (Outcome.STEP_ABORT.equals(result)) {
718: abort();
719: break;
720: }
721:
722: if (!m_currentStep.isCompleted()) {
723: m_currentStep.setOutcome(result);
724: }
725: } catch (WikiException e) {
726: throw e;
727: }
728:
729: // Get the execution Outcome; if not complete, pause workflow and
730: // exit
731: Outcome outcome = m_currentStep.getOutcome();
732: if (!outcome.isCompletion()) {
733: waitstate();
734: break;
735: }
736:
737: // Get the next Step; if null, we're done
738: Step nextStep = m_currentStep.getSuccessor(outcome);
739: if (nextStep == null) {
740: complete();
741: break;
742: }
743:
744: // Add the next step to Workflow history, and mark as current
745: m_history.add(nextStep);
746: m_currentStep = nextStep;
747: }
748:
749: }
750:
751: // events processing .......................................................
752:
753: /**
754: * Registers a WikiEventListener with this instance. This is a convenience
755: * method.
756: *
757: * @param listener
758: * the event listener
759: */
760: public final synchronized void addWikiEventListener(
761: WikiEventListener listener) {
762: WikiEventManager.addWikiEventListener(this , listener);
763: }
764:
765: /**
766: * Un-registers a WikiEventListener with this instance. This is a
767: * convenience method.
768: *
769: * @param listener
770: * the event listener
771: */
772: public final synchronized void removeWikiEventListener(
773: WikiEventListener listener) {
774: WikiEventManager.removeWikiEventListener(this , listener);
775: }
776:
777: /**
778: * Fires a WorkflowEvent of the provided type to all registered listeners.
779: *
780: * @see com.ecyrd.jspwiki.event.WorkflowEvent
781: * @param type
782: * the event type to be fired
783: */
784: protected final void fireEvent(int type) {
785: if (WikiEventManager.isListening(this )) {
786: WikiEventManager.fireEvent(this , new WorkflowEvent(this,
787: type));
788: }
789: }
790:
791: }
|