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