001: /*
002: * Copyright 2004-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.springframework.webflow.engine;
017:
018: import java.util.Iterator;
019: import java.util.Set;
020:
021: import org.apache.commons.logging.Log;
022: import org.apache.commons.logging.LogFactory;
023: import org.springframework.binding.mapping.AttributeMapper;
024: import org.springframework.core.CollectionFactory;
025: import org.springframework.core.style.StylerUtils;
026: import org.springframework.core.style.ToStringCreator;
027: import org.springframework.util.Assert;
028: import org.springframework.util.StringUtils;
029: import org.springframework.webflow.core.collection.MutableAttributeMap;
030: import org.springframework.webflow.definition.FlowDefinition;
031: import org.springframework.webflow.definition.StateDefinition;
032: import org.springframework.webflow.execution.FlowExecutionException;
033: import org.springframework.webflow.execution.RequestContext;
034: import org.springframework.webflow.execution.ViewSelection;
035:
036: /**
037: * A single flow definition. A Flow definition is a reusable, self-contained
038: * controller module that provides the blue print for a user dialog or
039: * conversation. Flows typically orchestrate controlled navigations within web
040: * applications to guide users through fulfillment of a business process/goal
041: * that takes place over a series of steps, modeled as states.
042: * <p>
043: * A simple Flow definition could do nothing more than execute an action and
044: * display a view all in one request. A more elaborate Flow definition may be
045: * long-lived and execute across a series of requests, invoking many possible
046: * paths, actions, and subflows.
047: * <p>
048: * Especially in Intranet applications there are often "controlled navigations"
049: * where the user is not free to do what he or she wants but must follow the
050: * guidelines provided by the system to complete a process that is transactional
051: * in nature (the quinessential example would be a 'checkout' flow of a shopping
052: * cart application). This is a typical use case appropriate to model as a flow.
053: * <p>
054: * Structurally a Flow is composed of a set of states. A {@link State} is a
055: * point in a flow where a behavior is executed; for example, showing a view,
056: * executing an action, spawning a subflow, or terminating the flow. Different
057: * types of states execute different behaviors in a polymorphic fashion.
058: * <p>
059: * Each {@link TransitionableState} type has one or more transitions that when
060: * executed move a flow to another state. These transitions define the supported
061: * paths through the flow.
062: * <p>
063: * A state transition is triggered by the occurence of an event. An event is
064: * something that happens the flow should respond to, for example a user input
065: * event like ("submit") or an action execution result event like ("success").
066: * When an event occurs in a state of a Flow that event drives a state
067: * transition that decides what to do next.
068: * <p>
069: * Each Flow has exactly one start state. A start state is simply a marker
070: * noting the state executions of this Flow definition should start in. The
071: * first state added to the flow will become the start state by default.
072: * <p>
073: * Flow definitions may have one or more flow exception handlers. A
074: * {@link FlowExecutionExceptionHandler} can execute custom behavior in response
075: * to a specific exception (or set of exceptions) that occur in a state of one
076: * of this flow's executions.
077: * <p>
078: * Instances of this class are typically built by
079: * {@link org.springframework.webflow.engine.builder.FlowBuilder}
080: * implementations but may also be directly instantiated.
081: * <p>
082: * This class and the rest of the Spring Web Flow (SWF) engine have been designed
083: * with minimal dependencies on other libraries. Spring Web Flow is usable in a
084: * standalone fashion (as well as in the context of other frameworks like Spring
085: * MVC, Struts, or JSF, for example). The engine system is fully usable outside an
086: * HTTP servlet environment, for example in portlets, tests, or standalone
087: * applications. One of the major architectural benefits of Spring Web Flow is
088: * the ability to design reusable, high-level controller modules that may be
089: * executed in <i>any</i> environment.
090: * <p>
091: * Note: flows are singleton definition objects so they should be thread-safe.
092: * You can think a flow definition as analagous somewhat to a Java class,
093: * defining all the behavior of an application module. The core behaviors
094: * {@link #start(RequestControlContext, MutableAttributeMap) start},
095: * {@link #onEvent(RequestControlContext) on event}, and
096: * {@link #end(RequestControlContext, MutableAttributeMap) end} each accept a
097: * {@link RequestContext request context} that allows for this flow to access
098: * execution state in a thread safe manner. A flow execution is what models a
099: * 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 id;
128:
129: /**
130: * The set of state definitions for this flow.
131: */
132: private Set 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 variables = CollectionFactory
143: .createLinkedSetIfPossible(3);
144:
145: /**
146: * The mapper to map flow input attributes.
147: */
148: private AttributeMapper inputMapper;
149:
150: /**
151: * The list of actions to execute when this flow starts.
152: * <p>
153: * Start actions should execute with care as during startup a flow session
154: * has not yet fully initialized and some properties like its "currentState"
155: * have not yet been set.
156: */
157: private ActionList startActionList = new ActionList();
158:
159: /**
160: * The set of global transitions that are shared by all states of this flow.
161: */
162: private TransitionSet globalTransitionSet = new TransitionSet();
163:
164: /**
165: * The list of actions to execute when this flow ends.
166: */
167: private ActionList endActionList = new ActionList();
168:
169: /**
170: * The mapper to map flow output attributes.
171: */
172: private AttributeMapper outputMapper;
173:
174: /**
175: * The set of exception handlers for this flow.
176: */
177: private FlowExecutionExceptionHandlerSet exceptionHandlerSet = new FlowExecutionExceptionHandlerSet();
178:
179: /**
180: * The set of inline flows contained by this flow.
181: */
182: private Set inlineFlows = CollectionFactory
183: .createLinkedSetIfPossible(3);
184:
185: /**
186: * Construct a new flow definition with the given id. The id should be
187: * unique among all flows.
188: * @param id the flow identifier
189: */
190: public Flow(String id) {
191: setId(id);
192: }
193:
194: // implementing FlowDefinition
195:
196: public String getId() {
197: return id;
198: }
199:
200: public StateDefinition getStartState() {
201: if (startState == null) {
202: throw new IllegalStateException(
203: "No start state has been set for this flow ('"
204: + getId()
205: + "') -- flow builder configuration error?");
206: }
207: return startState;
208: }
209:
210: public StateDefinition getState(String stateId) {
211: return getStateInstance(stateId);
212: }
213:
214: /**
215: * Set the unique id of this flow.
216: */
217: protected void setId(String id) {
218: Assert.hasText(id,
219: "This flow must have a unique, non-blank identifier");
220: this .id = id;
221: }
222:
223: /**
224: * Add given state definition to this flow definition. Marked protected, as
225: * this method is to be called by the (privileged) state definition classes
226: * themselves during state construction as part of a FlowBuilder invocation.
227: * @param state the state to add
228: * @throws IllegalArgumentException when the state cannot be added to the
229: * flow; for instance if another state shares the same id as the one
230: * provided or if given state already belongs to another flow
231: */
232: protected void add(State state) throws IllegalArgumentException {
233: if (this != state.getFlow() && state.getFlow() != null) {
234: throw new IllegalArgumentException("State " + state
235: + " cannot be added to this flow '" + getId()
236: + "' -- it already belongs to a different flow: '"
237: + state.getFlow().getId() + "'");
238: }
239: if (this .states.contains(state)
240: || this .containsState(state.getId())) {
241: throw new IllegalArgumentException(
242: "This flow '"
243: + getId()
244: + "' already contains a state with id '"
245: + state.getId()
246: + "' -- state ids must be locally unique to the flow definition; "
247: + "existing state-ids of this flow include: "
248: + StylerUtils.style(getStateIds()));
249: }
250: boolean firstAdd = states.isEmpty();
251: states.add(state);
252: if (firstAdd) {
253: setStartState(state);
254: }
255: }
256:
257: /**
258: * Returns the number of states defined in this flow.
259: * @return the state count
260: */
261: public int getStateCount() {
262: return states.size();
263: }
264:
265: /**
266: * Is a state with the provided id present in this flow?
267: * @param stateId the state id
268: * @return true if yes, false otherwise
269: */
270: public boolean containsState(String stateId) {
271: Iterator it = states.iterator();
272: while (it.hasNext()) {
273: State state = (State) it.next();
274: if (state.getId().equals(stateId)) {
275: return true;
276: }
277: }
278: return false;
279: }
280:
281: /**
282: * Set the start state for this flow to the state with the provided
283: * <code>stateId</code>; a state must exist by the provided
284: * <code>stateId</code>.
285: * @param stateId the id of the new start state
286: * @throws IllegalArgumentException when no state exists with the id you
287: * provided
288: */
289: public void setStartState(String stateId)
290: throws IllegalArgumentException {
291: setStartState(getStateInstance(stateId));
292: }
293:
294: /**
295: * Set the start state for this flow to the state provided; any state may be
296: * the start state.
297: * @param state the new start state
298: * @throws IllegalArgumentException given state has not been added to this
299: * flow
300: */
301: public void setStartState(State state)
302: throws IllegalArgumentException {
303: if (!states.contains(state)) {
304: throw new IllegalArgumentException("State '" + state
305: + "' is not a state of flow '" + getId() + "'");
306: }
307: startState = state;
308: }
309:
310: /**
311: * Return the <code>TransitionableState</code> with given <code>stateId</code>.
312: * @param stateId id of the state to look up
313: * @return the transitionable state
314: * @throws IllegalArgumentException if the identified state cannot be found
315: * @throws ClassCastException when the identified state is not
316: * transitionable
317: */
318: public TransitionableState getTransitionableState(String stateId)
319: throws IllegalArgumentException, ClassCastException {
320: State state = getStateInstance(stateId);
321: if (state != null && !(state instanceof TransitionableState)) {
322: throw new ClassCastException("The state '" + stateId
323: + "' of flow '" + getId()
324: + "' must be transitionable");
325: }
326: return (TransitionableState) state;
327: }
328:
329: /**
330: * Lookup the identified state instance of this flow.
331: * @param stateId the state id
332: * @return the state
333: * @throws IllegalArgumentException if the identified state cannot be found
334: */
335: public State getStateInstance(String stateId)
336: throws IllegalArgumentException {
337: if (!StringUtils.hasText(stateId)) {
338: throw new IllegalArgumentException(
339: "The specified stateId is invalid: state identifiers must be non-blank");
340: }
341: Iterator it = states.iterator();
342: while (it.hasNext()) {
343: State state = (State) it.next();
344: if (state.getId().equals(stateId)) {
345: return state;
346: }
347: }
348: throw new IllegalArgumentException(
349: "Cannot find state with id '" + stateId + "' in flow '"
350: + getId() + "' -- " + "Known state ids are '"
351: + StylerUtils.style(getStateIds()) + "'");
352: }
353:
354: /**
355: * Convenience accessor that returns an ordered array of the String
356: * <code>ids</code> for the state definitions associated with this flow
357: * definition.
358: * @return the state ids
359: */
360: public String[] getStateIds() {
361: String[] stateIds = new String[getStateCount()];
362: int i = 0;
363: Iterator it = states.iterator();
364: while (it.hasNext()) {
365: stateIds[i++] = ((State) it.next()).getId();
366: }
367: return stateIds;
368: }
369:
370: /**
371: * Adds a flow variable.
372: * @param variable the variable
373: */
374: public void addVariable(FlowVariable variable) {
375: variables.add(variable);
376: }
377:
378: /**
379: * Adds flow variables.
380: * @param variables the variables
381: */
382: public void addVariables(FlowVariable[] variables) {
383: if (variables == null) {
384: return;
385: }
386: for (int i = 0; i < variables.length; i++) {
387: addVariable(variables[i]);
388: }
389: }
390:
391: /**
392: * Returns the flow variables.
393: */
394: public FlowVariable[] getVariables() {
395: return (FlowVariable[]) variables
396: .toArray(new FlowVariable[variables.size()]);
397: }
398:
399: /**
400: * Returns the configured flow input mapper, or null if none.
401: * @return the input mapper
402: */
403: public AttributeMapper getInputMapper() {
404: return inputMapper;
405: }
406:
407: /**
408: * Sets the mapper to map flow input attributes.
409: * @param inputMapper the input mapper
410: */
411: public void setInputMapper(AttributeMapper inputMapper) {
412: this .inputMapper = inputMapper;
413: }
414:
415: /**
416: * Returns the list of actions executed by this flow when an execution of
417: * the flow <i>starts</i>. The returned list is mutable.
418: * @return the start action list
419: */
420: public ActionList getStartActionList() {
421: return startActionList;
422: }
423:
424: /**
425: * Returns the list of actions executed by this flow when an execution of
426: * the flow <i>ends</i>. The returned list is mutable.
427: * @return the end action list
428: */
429: public ActionList getEndActionList() {
430: return endActionList;
431: }
432:
433: /**
434: * Returns the configured flow output mapper, or null if none.
435: * @return the output mapper
436: */
437: public AttributeMapper getOutputMapper() {
438: return outputMapper;
439: }
440:
441: /**
442: * Sets the mapper to map flow output attributes.
443: * @param outputMapper the output mapper
444: */
445: public void setOutputMapper(AttributeMapper outputMapper) {
446: this .outputMapper = outputMapper;
447: }
448:
449: /**
450: * Returns the set of exception handlers, allowing manipulation of how
451: * exceptions are handled when thrown during flow execution. Exception
452: * handlers are invoked when an exception occurs at execution time
453: * and can execute custom exception handling logic as well as select an
454: * error view to display. Exception handlers attached at the flow
455: * level have an opportunity to handle exceptions that aren't handled at the
456: * state level.
457: * @return the exception handler set
458: */
459: public FlowExecutionExceptionHandlerSet getExceptionHandlerSet() {
460: return exceptionHandlerSet;
461: }
462:
463: /**
464: * Adds an inline flow to this flow.
465: * @param flow the inline flow to add
466: */
467: public void addInlineFlow(Flow flow) {
468: inlineFlows.add(flow);
469: }
470:
471: /**
472: * Returns the list of inline flow ids.
473: * @return a string array of inline flow identifiers
474: */
475: public String[] getInlineFlowIds() {
476: String[] flowIds = new String[getInlineFlowCount()];
477: int i = 0;
478: Iterator it = inlineFlows.iterator();
479: while (it.hasNext()) {
480: flowIds[i++] = ((Flow) it.next()).getId();
481: }
482: return flowIds;
483: }
484:
485: /**
486: * Returns the list of inline flows.
487: * @return the list of inline flows
488: */
489: public Flow[] getInlineFlows() {
490: return (Flow[]) inlineFlows
491: .toArray(new Flow[inlineFlows.size()]);
492: }
493:
494: /**
495: * Returns the count of registered inline flows.
496: * @return the count
497: */
498: public int getInlineFlowCount() {
499: return inlineFlows.size();
500: }
501:
502: /**
503: * Tests if this flow contains an in-line flow with the specified id.
504: * @param id the inline flow id
505: * @return true if this flow contains a inline flow with that id, false
506: * otherwise
507: */
508: public boolean containsInlineFlow(String id) {
509: return getInlineFlow(id) != null;
510: }
511:
512: /**
513: * Returns the inline flow with the provided id, or <code>null</code> if
514: * no such inline flow exists.
515: * @param id the inline flow id
516: * @return the inline flow
517: * @throws IllegalArgumentException when an invalid flow id is provided
518: */
519: public Flow getInlineFlow(String id)
520: throws IllegalArgumentException {
521: if (!StringUtils.hasText(id)) {
522: throw new IllegalArgumentException(
523: "The specified inline flowId is invalid: flow identifiers must be non-blank");
524: }
525: Iterator it = inlineFlows.iterator();
526: while (it.hasNext()) {
527: Flow flow = (Flow) it.next();
528: if (flow.getId().equals(id)) {
529: return flow;
530: }
531: }
532: return null;
533: }
534:
535: /**
536: * Returns the set of transitions eligible for execution by this flow if no
537: * state-level transition is matched. The returned set is mutable.
538: * @return the global transition set
539: */
540: public TransitionSet getGlobalTransitionSet() {
541: return globalTransitionSet;
542: }
543:
544: // id based equality
545:
546: public boolean equals(Object o) {
547: if (!(o instanceof Flow)) {
548: return false;
549: }
550: Flow other = (Flow) o;
551: return id.equals(other.id);
552: }
553:
554: public int hashCode() {
555: return id.hashCode();
556: }
557:
558: // behavioral code, could be overridden in subclasses
559:
560: /**
561: * Start a new session for this flow in its start state. This boils down to
562: * the following:
563: * <ol>
564: * <li>Create (setup) all registered flow variables ({@link #addVariable(FlowVariable)})
565: * in flow scope.</li>
566: * <li>Map provided input data into the flow execution control context.
567: * Typically data will be mapped into flow scope using the registered input
568: * mapper ({@link #setInputMapper(AttributeMapper)}).</li>
569: * <li>Execute all registered start actions ({@link #getStartActionList()}).</li>
570: * <li>Enter the configured start state ({@link #setStartState(State)})</li>
571: * </ol>
572: * @param context the flow execution control context
573: * @param input eligible input into the session
574: * @throws FlowExecutionException when an exception occurs starting the flow
575: */
576: public ViewSelection start(RequestControlContext context,
577: MutableAttributeMap input) throws FlowExecutionException {
578: createVariables(context);
579: if (inputMapper != null) {
580: inputMapper.map(input, context, null);
581: }
582: startActionList.execute(context);
583: return startState.enter(context);
584: }
585:
586: /**
587: * Inform this flow definition that an event was signaled in the current
588: * state of an active flow execution. The signaled event is the last event
589: * available in given request context ({@link RequestContext#getLastEvent()}).
590: * @param context the flow execution control context
591: * @return the selected view
592: * @throws FlowExecutionException when an exception occurs processing the
593: * event
594: */
595: public ViewSelection onEvent(RequestControlContext context)
596: throws FlowExecutionException {
597: TransitionableState currentState = getCurrentTransitionableState(context);
598: try {
599: return currentState.onEvent(context);
600: } catch (NoMatchingTransitionException e) {
601: // try the flow level transition set for a match
602: Transition transition = globalTransitionSet
603: .getTransition(context);
604: if (transition != null) {
605: return transition.execute(currentState, context);
606: } else {
607: // no matching global transition => let the original exception
608: // propagate
609: throw e;
610: }
611: }
612: }
613:
614: /**
615: * Inform this flow definition that an execution session of itself has
616: * ended. As a result, the flow will do the following:
617: * <ol>
618: * <li>Execute all registered end actions ({@link #getEndActionList()}).</li>
619: * <li>Map data available in the flow execution control context into
620: * provided output map using a registered output mapper
621: * ({@link #setOutputMapper(AttributeMapper)}).</li>
622: * </ol>
623: * @param context the flow execution control context
624: * @param output initial output produced by the session that is eligible for
625: * modification by this method
626: * @throws FlowExecutionException when an exception occurs ending this flow
627: */
628: public void end(RequestControlContext context,
629: MutableAttributeMap output) throws FlowExecutionException {
630: endActionList.execute(context);
631: if (outputMapper != null) {
632: outputMapper.map(context, output, null);
633: }
634: }
635:
636: /**
637: * Handle an exception that occured during an execution of this flow.
638: * @param exception the exception that occured
639: * @param context the flow execution control context
640: * @return the selected error view, or <code>null</code> if no handler
641: * matched or returned a non-null view selection
642: */
643: public ViewSelection handleException(
644: FlowExecutionException exception,
645: RequestControlContext context)
646: throws FlowExecutionException {
647: return getExceptionHandlerSet().handleException(exception,
648: context);
649: }
650:
651: // internal helpers
652:
653: /**
654: * Create (setup) all known flow variables in flow scope.
655: */
656: private void createVariables(RequestContext context) {
657: Iterator it = variables.iterator();
658: while (it.hasNext()) {
659: FlowVariable variable = (FlowVariable) it.next();
660: if (logger.isDebugEnabled()) {
661: logger.debug("Creating " + variable);
662: }
663: variable.create(context);
664: }
665: }
666:
667: /**
668: * Returns the current state and makes sure it is transitionable.
669: */
670: private TransitionableState getCurrentTransitionableState(
671: RequestControlContext context) {
672: State currentState = (State) context.getCurrentState();
673: if (!(currentState instanceof TransitionableState)) {
674: throw new IllegalStateException(
675: "You can only signal events in transitionable states, and state "
676: + context.getCurrentState()
677: + " is not transitionable - programmer error");
678: }
679: return (TransitionableState) currentState;
680: }
681:
682: public String toString() {
683: return new ToStringCreator(this ).append("id", id).append(
684: "states", states).append("startState", startState)
685: .append("variables", variables).append("inputMapper",
686: inputMapper).append("startActionList",
687: startActionList).append("exceptionHandlerSet",
688: exceptionHandlerSet).append(
689: "globalTransitionSet", globalTransitionSet)
690: .append("endActionList", endActionList).append(
691: "outputMapper", outputMapper).append(
692: "inlineFlows", inlineFlows).toString();
693: }
694: }
|