KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > springframework > webflow > engine > Flow


1 /*
2  * Copyright 2002-2006 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.webflow.engine;
17
18 import java.util.Iterator JavaDoc;
19 import java.util.Set JavaDoc;
20
21 import org.apache.commons.logging.Log;
22 import org.apache.commons.logging.LogFactory;
23 import org.springframework.binding.mapping.AttributeMapper;
24 import org.springframework.core.CollectionFactory;
25 import org.springframework.core.style.StylerUtils;
26 import org.springframework.core.style.ToStringCreator;
27 import org.springframework.util.Assert;
28 import org.springframework.util.StringUtils;
29 import org.springframework.webflow.core.collection.MutableAttributeMap;
30 import org.springframework.webflow.definition.FlowDefinition;
31 import org.springframework.webflow.definition.StateDefinition;
32 import org.springframework.webflow.execution.FlowExecutionException;
33 import org.springframework.webflow.execution.RequestContext;
34 import org.springframework.webflow.execution.ViewSelection;
35
36 /**
37  * A single flow definition. A Flow definition is a reusable, self-contained
38  * controller module that provides the blue print for a user dialog or
39  * conversation. Flows typically orchestrate controlled navigations within web
40  * applications to guide users through fulfillment of a business process/goal
41  * that takes place over a series of steps, modeled as states.
42  * <p>
43  * A simple Flow definition could do nothing more than execute an action and
44  * display a view all in one request. A more elaborate Flow definition may be
45  * long-lived and execute across a series of requests, invoking many possible
46  * paths, actions, and subflows.
47  * <p>
48  * Especially in Intranet applications there are often "controlled navigations"
49  * where the user is not free to do what he or she wants but must follow the
50  * guidelines provided by the system to complete a process that is transactional
51  * in nature (the quinessential example would be a 'checkout' flow of a shopping
52  * cart application). This is a typical use case appropriate to model as a flow.
53  * <p>
54  * Structurally a Flow is composed of a set of states. A {@link State} is a
55  * point in a flow where a behavior is executed; for example, showing a view,
56  * executing an action, spawning a subflow, or terminating the flow. Different
57  * types of states execute different behaviors in a polymorphic fashion.
58  * <p>
59  * Each {@link TransitionableState} type has one or more transitions that when
60  * executed move a flow to another state. These transitions define the supported
61  * paths through the flow.
62  * <p>
63  * A state transition is triggered by the occurence of an event. An event is
64  * something that happens the flow should respond to, for example a user input
65  * event like ("submit") or an action execution result event like ("success").
66  * When an event occurs in a state of a Flow that event drives a state
67  * transition that decides what to do next.
68  * <p>
69  * Each Flow has exactly one start state. A start state is simply a marker
70  * noting the state executions of this Flow definition should start in. The
71  * first state added to the flow will become the start state by default.
72  * <p>
73  * Flow definitions may have one or more flow exception handlers. A
74  * {@link FlowExecutionExceptionHandler} can execute custom behavior in response
75  * to a specific exception (or set of exceptions) that occur in a state of one
76  * of this flow's executions.
77  * <p>
78  * Instances of this class are typically built by
79  * {@link org.springframework.webflow.engine.builder.FlowBuilder}
80  * implementations but may also be directly instantiated.
81  * <p>
82  * This class and the rest of the Spring Web Flow (SWF) engine have been designed
83  * with minimal dependencies on other libraries. Spring Web Flow is usable in a
84  * standalone fashion (as well as in the context of other frameworks like Spring
85  * MVC, Struts, or JSF, for example). The engine system is fully usable outside an
86  * HTTP servlet environment, for example in portlets, tests, or standalone
87  * applications. One of the major architectural benefits of Spring Web Flow is
88  * the ability to design reusable, high-level controller modules that may be
89  * executed in <i>any</i> environment.
90  * <p>
91  * Note: flows are singleton definition objects so they should be thread-safe.
92  * You can think a flow definition as analagous somewhat to a Java class,
93  * defining all the behavior of an application module. The core behaviors
94  * {@link #start(RequestControlContext, MutableAttributeMap) start},
95  * {@link #onEvent(RequestControlContext) on event}, and
96  * {@link #end(RequestControlContext, MutableAttributeMap) end} each accept a
97  * {@link RequestContext request context} that allows for this flow to access
98  * execution state in a thread safe manner. A flow execution is what models a
99  * running instance of this flow definition, somewhat analgous to a java object
100  * that is an instance of a class.
101  *
102  * @see org.springframework.webflow.engine.State
103  * @see org.springframework.webflow.engine.TransitionableState
104  * @see org.springframework.webflow.engine.ActionState
105  * @see org.springframework.webflow.engine.ViewState
106  * @see org.springframework.webflow.engine.SubflowState
107  * @see org.springframework.webflow.engine.EndState
108  * @see org.springframework.webflow.engine.DecisionState
109  * @see org.springframework.webflow.engine.Transition
110  * @see org.springframework.webflow.engine.FlowExecutionExceptionHandler
111  *
112  * @author Keith Donald
113  * @author Erwin Vervaet
114  * @author Colin Sampaleanu
115  */

116 public class Flow extends AnnotatedObject implements FlowDefinition {
117
118     /**
119      * Logger, can be used in subclasses.
120      */

121     protected final Log logger = LogFactory.getLog(getClass());
122
123     /**
124      * An assigned flow identifier uniquely identifying this flow among all
125      * other flows.
126      */

127     private String JavaDoc id;
128
129     /**
130      * The set of state definitions for this flow.
131      */

132     private Set JavaDoc states = CollectionFactory.createLinkedSetIfPossible(9);
133
134     /**
135      * The default start state for this flow.
136      */

137     private State startState;
138
139     /**
140      * The set of flow variables created by this flow.
141      */

142     private Set JavaDoc variables = CollectionFactory.createLinkedSetIfPossible(3);
143
144     /**
145      * The mapper to map flow input attributes.
146      */

147     private AttributeMapper inputMapper;
148
149     /**
150      * The list of actions to execute when this flow starts.
151      * <p>
152      * Start actions should execute with care as during startup a flow session
153      * has not yet fully initialized and some properties like its "currentState"
154      * have not yet been set.
155      */

156     private ActionList startActionList = new ActionList();
157
158     /**
159      * The set of global transitions that are shared by all states of this flow.
160      */

161     private TransitionSet globalTransitionSet = new TransitionSet();
162
163     /**
164      * The list of actions to execute when this flow ends.
165      */

166     private ActionList endActionList = new ActionList();
167
168     /**
169      * The mapper to map flow output attributes.
170      */

171     private AttributeMapper outputMapper;
172
173     /**
174      * The set of exception handlers for this flow.
175      */

176     private FlowExecutionExceptionHandlerSet exceptionHandlerSet = new FlowExecutionExceptionHandlerSet();
177
178     /**
179      * The set of inline flows contained by this flow.
180      */

181     private Set JavaDoc inlineFlows = CollectionFactory.createLinkedSetIfPossible(3);
182
183     /**
184      * Construct a new flow definition with the given id. The id should be
185      * unique among all flows.
186      * @param id the flow identifier
187      */

188     public Flow(String JavaDoc id) {
189         setId(id);
190     }
191
192     // implementing FlowDefinition
193

194     public String JavaDoc getId() {
195         return id;
196     }
197
198     public StateDefinition getStartState() {
199         if (startState == null) {
200             throw new IllegalStateException JavaDoc("No start state has been set for this flow ('" + getId()
201                     + "') -- flow builder configuration error?");
202         }
203         return startState;
204     }
205
206     public StateDefinition getState(String JavaDoc stateId) {
207         return getStateInstance(stateId);
208     }
209
210     /**
211      * Set the unique id of this flow.
212      */

213     protected void setId(String JavaDoc id) {
214         Assert.hasText(id, "This flow must have a unique, non-blank identifier");
215         this.id = id;
216     }
217
218     /**
219      * Add given state definition to this flow definition. Marked protected, as
220      * this method is to be called by the (privileged) state definition classes
221      * themselves during state construction as part of a FlowBuilder invocation.
222      * @param state the state to add
223      * @throws IllegalArgumentException when the state cannot be added to the
224      * flow; for instance if another state shares the same id as the one
225      * provided or if given state already belongs to another flow
226      */

227     protected void add(State state) throws IllegalArgumentException JavaDoc {
228         if (this != state.getFlow() && state.getFlow() != null) {
229             throw new IllegalArgumentException JavaDoc("State " + state + " cannot be added to this flow '" + getId()
230                     + "' -- it already belongs to a different flow: '" + state.getFlow().getId() + "'");
231         }
232         if (this.states.contains(state) || this.containsState(state.getId())) {
233             throw new IllegalArgumentException JavaDoc("This flow '" + getId() + "' already contains a state with id '"
234                     + state.getId() + "' -- state ids must be locally unique to the flow definition; "
235                     + "existing state-ids of this flow include: " + StylerUtils.style(getStateIds()));
236         }
237         boolean firstAdd = states.isEmpty();
238         states.add(state);
239         if (firstAdd) {
240             setStartState(state);
241         }
242     }
243
244     /**
245      * Returns the number of states defined in this flow.
246      * @return the state count
247      */

248     public int getStateCount() {
249         return states.size();
250     }
251
252     /**
253      * Is a state with the provided id present in this flow?
254      * @param stateId the state id
255      * @return true if yes, false otherwise
256      */

257     public boolean containsState(String JavaDoc stateId) {
258         Iterator JavaDoc it = states.iterator();
259         while (it.hasNext()) {
260             State state = (State)it.next();
261             if (state.getId().equals(stateId)) {
262                 return true;
263             }
264         }
265         return false;
266     }
267     
268     /**
269      * Set the start state for this flow to the state with the provided
270      * <code>stateId</code>; a state must exist by the provided
271      * <code>stateId</code>.
272      * @param stateId the id of the new start state
273      * @throws IllegalArgumentException when no state exists with the id you
274      * provided
275      */

276     public void setStartState(String JavaDoc stateId) throws IllegalArgumentException JavaDoc {
277         setStartState(getStateInstance(stateId));
278     }
279
280     /**
281      * Set the start state for this flow to the state provided; any state may be
282      * the start state.
283      * @param state the new start state
284      * @throws IllegalArgumentException given state has not been added to this
285      * flow
286      */

287     public void setStartState(State state) throws IllegalArgumentException JavaDoc {
288         if (!states.contains(state)) {
289             throw new IllegalArgumentException JavaDoc("State '" + state + "' is not a state of flow '" + getId() + "'");
290         }
291         startState = state;
292     }
293
294     /**
295      * Return the <code>TransitionableState</code> with given <code>stateId</code>.
296      * @param stateId id of the state to look up
297      * @return the transitionable state
298      * @throws IllegalArgumentException if the identified state cannot be found
299      * @throws ClassCastException when the identified state is not
300      * transitionable
301      */

302     public TransitionableState getTransitionableState(String JavaDoc stateId)
303             throws IllegalArgumentException JavaDoc, ClassCastException JavaDoc {
304         State state = getStateInstance(stateId);
305         if (state != null && !(state instanceof TransitionableState)) {
306             throw new ClassCastException JavaDoc("The state '" + stateId + "' of flow '" + getId() + "' must be transitionable");
307         }
308         return (TransitionableState)state;
309     }
310
311     /**
312      * Lookup the identified state instance of this flow.
313      * @param stateId the state id
314      * @return the state
315      * @throws IllegalArgumentException if the identified state cannot be found
316      */

317     public State getStateInstance(String JavaDoc stateId) throws IllegalArgumentException JavaDoc {
318         if (!StringUtils.hasText(stateId)) {
319             throw new IllegalArgumentException JavaDoc("The specified stateId is invalid: state identifiers must be non-blank");
320         }
321         Iterator JavaDoc it = states.iterator();
322         while (it.hasNext()) {
323             State state = (State)it.next();
324             if (state.getId().equals(stateId)) {
325                 return state;
326             }
327         }
328         throw new IllegalArgumentException JavaDoc("Cannot find state with id '" + stateId + "' in flow '" + getId() + "' -- "
329                 + "Known state ids are '" + StylerUtils.style(getStateIds()) + "'");
330     }
331
332     /**
333      * Convenience accessor that returns an ordered array of the String
334      * <code>ids</code> for the state definitions associated with this flow
335      * definition.
336      * @return the state ids
337      */

338     public String JavaDoc[] getStateIds() {
339         String JavaDoc[] stateIds = new String JavaDoc[getStateCount()];
340         int i = 0;
341         Iterator JavaDoc it = states.iterator();
342         while (it.hasNext()) {
343             stateIds[i++] = ((State)it.next()).getId();
344         }
345         return stateIds;
346     }
347
348     /**
349      * Adds a flow variable.
350      * @param variable the variable
351      */

352     public void addVariable(FlowVariable variable) {
353         variables.add(variable);
354     }
355
356     /**
357      * Adds flow variables.
358      * @param variables the variables
359      */

360     public void addVariables(FlowVariable[] variables) {
361         if (variables == null) {
362             return;
363         }
364         for (int i = 0; i < variables.length; i++) {
365             addVariable(variables[i]);
366         }
367     }
368     
369     /**
370      * Returns the flow variables.
371      */

372     public FlowVariable[] getVariables() {
373         return (FlowVariable[])variables.toArray(new FlowVariable[variables.size()]);
374     }
375
376     /**
377      * Returns the configured flow input mapper, or null if none.
378      * @return the input mapper
379      */

380     public AttributeMapper getInputMapper() {
381         return inputMapper;
382     }
383
384     /**
385      * Sets the mapper to map flow input attributes.
386      * @param inputMapper the input mapper
387      */

388     public void setInputMapper(AttributeMapper inputMapper) {
389         this.inputMapper = inputMapper;
390     }
391
392     /**
393      * Returns the list of actions executed by this flow when an execution of
394      * the flow <i>starts</i>. The returned list is mutable.
395      * @return the start action list
396      */

397     public ActionList getStartActionList() {
398         return startActionList;
399     }
400
401     /**
402      * Returns the list of actions executed by this flow when an execution of
403      * the flow <i>ends</i>. The returned list is mutable.
404      * @return the end action list
405      */

406     public ActionList getEndActionList() {
407         return endActionList;
408     }
409
410     /**
411      * Returns the configured flow output mapper, or null if none.
412      * @return the output mapper
413      */

414     public AttributeMapper getOutputMapper() {
415         return outputMapper;
416     }
417
418     /**
419      * Sets the mapper to map flow output attributes.
420      * @param outputMapper the output mapper
421      */

422     public void setOutputMapper(AttributeMapper outputMapper) {
423         this.outputMapper = outputMapper;
424     }
425
426     /**
427      * Returns the set of exception handlers, allowing manipulation of how
428      * exceptions are handled when thrown during flow execution. Exception
429      * handlers are invoked when an exception occurs at execution time
430      * and can execute custom exception handling logic as well as select an
431      * error view to display. Exception handlers attached at the flow
432      * level have an opportunity to handle exceptions that aren't handled at the
433      * state level.
434      * @return the exception handler set
435      */

436     public FlowExecutionExceptionHandlerSet getExceptionHandlerSet() {
437         return exceptionHandlerSet;
438     }
439
440     /**
441      * Adds an inline flow to this flow.
442      * @param flow the inline flow to add
443      */

444     public void addInlineFlow(Flow flow) {
445         inlineFlows.add(flow);
446     }
447
448     /**
449      * Returns the list of inline flow ids.
450      * @return a string array of inline flow identifiers
451      */

452     public String JavaDoc[] getInlineFlowIds() {
453         String JavaDoc[] flowIds = new String JavaDoc[getInlineFlowCount()];
454         int i = 0;
455         Iterator JavaDoc it = inlineFlows.iterator();
456         while (it.hasNext()) {
457             flowIds[i++] = ((Flow)it.next()).getId();
458         }
459         return flowIds;
460     }
461
462     /**
463      * Returns the list of inline flows.
464      * @return the list of inline flows
465      */

466     public Flow[] getInlineFlows() {
467         return (Flow[])inlineFlows.toArray(new Flow[inlineFlows.size()]);
468     }
469
470     /**
471      * Returns the count of registered inline flows.
472      * @return the count
473      */

474     public int getInlineFlowCount() {
475         return inlineFlows.size();
476     }
477
478     /**
479      * Tests if this flow contains an in-line flow with the specified id.
480      * @param id the inline flow id
481      * @return true if this flow contains a inline flow with that id, false
482      * otherwise
483      */

484     public boolean containsInlineFlow(String JavaDoc id) {
485         return getInlineFlow(id) != null;
486     }
487
488     /**
489      * Returns the inline flow with the provided id, or <code>null</code> if
490      * no such inline flow exists.
491      * @param id the inline flow id
492      * @return the inline flow
493      * @throws IllegalArgumentException when an invalid flow id is provided
494      */

495     public Flow getInlineFlow(String JavaDoc id) throws IllegalArgumentException JavaDoc {
496         if (!StringUtils.hasText(id)) {
497             throw new IllegalArgumentException JavaDoc(
498                     "The specified inline flowId is invalid: flow identifiers must be non-blank");
499         }
500         Iterator JavaDoc it = inlineFlows.iterator();
501         while (it.hasNext()) {
502             Flow flow = (Flow)it.next();
503             if (flow.getId().equals(id)) {
504                 return flow;
505             }
506         }
507         return null;
508     }
509
510     /**
511      * Returns the set of transitions eligible for execution by this flow if no
512      * state-level transition is matched. The returned set is mutable.
513      * @return the global transition set
514      */

515     public TransitionSet getGlobalTransitionSet() {
516         return globalTransitionSet;
517     }
518     
519     // id based equality
520

521     public boolean equals(Object JavaDoc o) {
522         if (!(o instanceof Flow)) {
523             return false;
524         }
525         Flow other = (Flow)o;
526         return id.equals(other.id);
527     }
528
529     public int hashCode() {
530         return id.hashCode();
531     }
532
533     // behavioral code, could be overridden in subclasses
534

535     /**
536      * Start a new session for this flow in its start state. This boils down to
537      * the following:
538      * <ol>
539      * <li>Create (setup) all registered flow variables ({@link #addVariable(FlowVariable)})
540      * in flow scope.</li>
541      * <li>Map provided input data into the flow execution control context.
542      * Typically data will be mapped into flow scope using the registered input
543      * mapper ({@link #setInputMapper(AttributeMapper)}).</li>
544      * <li>Execute all registered start actions ({@link #getStartActionList()}).</li>
545      * <li>Enter the configured start state ({@link #setStartState(State)})</li>
546      * </ol>
547      * @param context the flow execution control context
548      * @param input eligible input into the session
549      * @throws FlowExecutionException when an exception occurs starting the flow
550      */

551     public ViewSelection start(RequestControlContext context, MutableAttributeMap input) throws FlowExecutionException {
552         createVariables(context);
553         if (inputMapper != null) {
554             inputMapper.map(input, context, null);
555         }
556         startActionList.execute(context);
557         return startState.enter(context);
558     }
559
560     /**
561      * Inform this flow definition that an event was signaled in the current
562      * state of an active flow execution. The signaled event is the last event
563      * available in given request context ({@link RequestContext#getLastEvent()}).
564      * @param context the flow execution control context
565      * @return the selected view
566      * @throws FlowExecutionException when an exception occurs processing the
567      * event
568      */

569     public ViewSelection onEvent(RequestControlContext context) throws FlowExecutionException {
570         TransitionableState currentState = getCurrentTransitionableState(context);
571         try {
572             return currentState.onEvent(context);
573         }
574         catch (NoMatchingTransitionException e) {
575             // try the flow level transition set for a match
576
Transition transition = globalTransitionSet.getTransition(context);
577             if (transition != null) {
578                 return transition.execute(currentState, context);
579             }
580             else {
581                 // no matching global transition => let the original exception
582
// propagate
583
throw e;
584             }
585         }
586     }
587
588     /**
589      * Inform this flow definition that an execution session of itself has
590      * ended. As a result, the flow will do the following:
591      * <ol>
592      * <li>Execute all registered end actions ({@link #getEndActionList()}).</li>
593      * <li>Map data available in the flow execution control context into
594      * provided output map using a registered output mapper
595      * ({@link #setOutputMapper(AttributeMapper)}).</li>
596      * </ol>
597      * @param context the flow execution control context
598      * @param output initial output produced by the session that is eligible for
599      * modification by this method
600      * @throws FlowExecutionException when an exception occurs ending this flow
601      */

602     public void end(RequestControlContext context, MutableAttributeMap output) throws FlowExecutionException {
603         endActionList.execute(context);
604         if (outputMapper != null) {
605             outputMapper.map(context, output, null);
606         }
607     }
608
609     /**
610      * Handle an exception that occured during an execution of this flow.
611      * @param exception the exception that occured
612      * @param context the flow execution control context
613      * @return the selected error view, or <code>null</code> if no handler
614      * matched or returned a non-null view selection
615      */

616     public ViewSelection handleException(FlowExecutionException exception, RequestControlContext context)
617             throws FlowExecutionException {
618         return getExceptionHandlerSet().handleException(exception, context);
619     }
620
621     // internal helpers
622

623     /**
624      * Create (setup) all known flow variables in flow scope.
625      */

626     private void createVariables(RequestContext context) {
627         Iterator JavaDoc it = variables.iterator();
628         while (it.hasNext()) {
629             FlowVariable variable = (FlowVariable)it.next();
630             if (logger.isDebugEnabled()) {
631                 logger.debug("Creating " + variable);
632             }
633             variable.create(context);
634         }
635     }
636
637     /**
638      * Returns the current state and makes sure it is transitionable.
639      */

640     private TransitionableState getCurrentTransitionableState(RequestControlContext context) {
641         State currentState = (State)context.getCurrentState();
642         if (!(currentState instanceof TransitionableState)) {
643             throw new IllegalStateException JavaDoc("You can only signal events in transitionable states, and state "
644                     + context.getCurrentState() + " is not transitionable - programmer error");
645         }
646         return (TransitionableState)currentState;
647     }
648
649     public String JavaDoc toString() {
650         return new ToStringCreator(this).append("id", id).append("states", states).append("startState", startState)
651                 .append("variables", variables).append("inputMapper", inputMapper).append("startActionList",
652                         startActionList).append("exceptionHandlerSet", exceptionHandlerSet).append(
653                         "globalTransitionSet", globalTransitionSet).append("endActionList", endActionList).append(
654                         "outputMapper", outputMapper).append("inlineFlows", inlineFlows).toString();
655     }
656 }
Popular Tags