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