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("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 */ 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 }