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.impl;
017:
018: import java.io.Externalizable;
019: import java.io.IOException;
020: import java.io.ObjectInput;
021: import java.io.ObjectOutput;
022: import java.util.LinkedList;
023: import java.util.ListIterator;
024:
025: import org.apache.commons.logging.Log;
026: import org.apache.commons.logging.LogFactory;
027: import org.springframework.core.style.ToStringCreator;
028: import org.springframework.util.Assert;
029: import org.springframework.webflow.context.ExternalContext;
030: import org.springframework.webflow.core.collection.AttributeMap;
031: import org.springframework.webflow.core.collection.CollectionUtils;
032: import org.springframework.webflow.core.collection.LocalAttributeMap;
033: import org.springframework.webflow.core.collection.MutableAttributeMap;
034: import org.springframework.webflow.definition.FlowDefinition;
035: import org.springframework.webflow.engine.Flow;
036: import org.springframework.webflow.engine.RequestControlContext;
037: import org.springframework.webflow.engine.State;
038: import org.springframework.webflow.engine.ViewState;
039: import org.springframework.webflow.execution.Event;
040: import org.springframework.webflow.execution.FlowExecution;
041: import org.springframework.webflow.execution.FlowExecutionException;
042: import org.springframework.webflow.execution.FlowExecutionListener;
043: import org.springframework.webflow.execution.FlowSession;
044: import org.springframework.webflow.execution.FlowSessionStatus;
045: import org.springframework.webflow.execution.ViewSelection;
046:
047: /**
048: * Default implementation of FlowExecution that uses a stack-based data
049: * structure to manage spawned flow sessions. This class is closely coupled with
050: * package-private <code>FlowSessionImpl</code> and
051: * <code>RequestControlContextImpl</code>. The three classes work together to
052: * form a complete flow execution implementation based on a finite state
053: * machine.
054: * <p>
055: * This implementation of FlowExecution is serializable so it can be safely
056: * stored in an HTTP session or other persistent store such as a file, database,
057: * or client-side form field. Once deserialized, the
058: * {@link FlowExecutionImplStateRestorer} strategy is expected to be used to
059: * restore the execution to a usable state.
060: *
061: * @see FlowExecutionImplFactory
062: * @see FlowExecutionImplStateRestorer
063: *
064: * @author Keith Donald
065: * @author Erwin Vervaet
066: * @author Ben Hale
067: */
068: public class FlowExecutionImpl implements FlowExecution, Externalizable {
069:
070: private static final Log logger = LogFactory
071: .getLog(FlowExecutionImpl.class);
072:
073: /**
074: * The execution's root flow; the top level flow that acts as the starting
075: * point for this flow execution.
076: * <p>
077: * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}.
078: */
079: private transient Flow flow;
080:
081: /**
082: * The stack of active, currently executing flow sessions. As subflows are
083: * spawned, they are pushed onto the stack. As they end, they are popped off
084: * the stack.
085: */
086: private LinkedList flowSessions;
087:
088: /**
089: * A thread-safe listener list, holding listeners monitoring the lifecycle
090: * of this flow execution.
091: * <p>
092: * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}.
093: */
094: private transient FlowExecutionListeners listeners;
095:
096: /**
097: * A data structure for attributes shared by all flow sessions.
098: * <p>
099: * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}.
100: */
101: private transient MutableAttributeMap conversationScope;
102:
103: /**
104: * A data structure for runtime system execution attributes.
105: * <p>
106: * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}.
107: */
108: private transient AttributeMap attributes;
109:
110: /**
111: * Set so the transient {@link #flow} field can be restored by the
112: * {@link FlowExecutionImplStateRestorer}.
113: */
114: private String flowId;
115:
116: /**
117: * Default constructor required for externalizable serialization. Should NOT
118: * be called programmatically.
119: */
120: public FlowExecutionImpl() {
121: }
122:
123: /**
124: * Create a new flow execution executing the provided flow. This constructor
125: * is mainly used for testing.
126: * @param flow the root flow of this flow execution
127: */
128: public FlowExecutionImpl(Flow flow) {
129: this (flow, new FlowExecutionListener[0], null);
130: }
131:
132: /**
133: * Create a new flow execution executing the provided flow.
134: * @param flow the root flow of this flow execution
135: * @param listeners the listeners interested in flow execution lifecycle
136: * events
137: * @param attributes flow execution system attributes
138: */
139: public FlowExecutionImpl(Flow flow,
140: FlowExecutionListener[] listeners, AttributeMap attributes) {
141: setFlow(flow);
142: this .flowSessions = new LinkedList();
143: this .listeners = new FlowExecutionListeners(listeners);
144: this .attributes = (attributes != null ? attributes
145: : CollectionUtils.EMPTY_ATTRIBUTE_MAP);
146: this .conversationScope = new LocalAttributeMap();
147: if (logger.isDebugEnabled()) {
148: logger.debug("Created new execution of flow '"
149: + flow.getId() + "'");
150: }
151: }
152:
153: public String getCaption() {
154: return "execution of '" + flowId + "'";
155: }
156:
157: // implementing FlowExecutionContext
158:
159: public FlowDefinition getDefinition() {
160: return flow;
161: }
162:
163: public boolean isActive() {
164: return !flowSessions.isEmpty();
165: }
166:
167: public FlowSession getActiveSession() {
168: return getActiveSessionInternal();
169: }
170:
171: public MutableAttributeMap getConversationScope() {
172: return conversationScope;
173: }
174:
175: public AttributeMap getAttributes() {
176: return attributes;
177: }
178:
179: // methods implementing FlowExecution
180:
181: public ViewSelection start(MutableAttributeMap input,
182: ExternalContext externalContext)
183: throws FlowExecutionException {
184: Assert
185: .state(!isActive(),
186: "This flow is already executing -- you cannot call 'start()' more than once");
187: if (logger.isDebugEnabled()) {
188: logger.debug("Starting execution with input '" + input
189: + "'");
190: }
191: RequestControlContext context = createControlContext(externalContext);
192: getListeners().fireRequestSubmitted(context);
193: try {
194: try {
195: // launch a flow session for the root flow
196: ViewSelection selectedView = context.start(flow, input);
197: return pause(context, selectedView);
198: } catch (FlowExecutionException e) {
199: return pause(context, handleException(e, context));
200: } catch (Exception e) {
201: return pause(context, handleException(wrapException(e),
202: context));
203: }
204: } finally {
205: getListeners().fireRequestProcessed(context);
206: }
207: }
208:
209: public ViewSelection signalEvent(String eventId,
210: ExternalContext externalContext)
211: throws FlowExecutionException {
212: assertActive();
213: if (logger.isDebugEnabled()) {
214: logger.debug("Resuming execution on user event '" + eventId
215: + "'");
216: }
217: RequestControlContext context = createControlContext(externalContext);
218: context.getFlashScope().clear();
219: getListeners().fireRequestSubmitted(context);
220: try {
221: try {
222: resume(context);
223: Event event = new Event(externalContext, eventId,
224: externalContext.getRequestParameterMap()
225: .asAttributeMap());
226: ViewSelection selectedView = context.signalEvent(event);
227: return pause(context, selectedView);
228: } catch (FlowExecutionException e) {
229: return pause(context, handleException(e, context));
230: } catch (Exception e) {
231: return pause(context, handleException(wrapException(e),
232: context));
233: }
234: } finally {
235: getListeners().fireRequestProcessed(context);
236: }
237: }
238:
239: public ViewSelection refresh(ExternalContext externalContext)
240: throws FlowExecutionException {
241: assertActive();
242: if (logger.isDebugEnabled()) {
243: logger.debug("Resuming execution for refresh");
244: }
245: RequestControlContext context = createControlContext(externalContext);
246: getListeners().fireRequestSubmitted(context);
247: try {
248: try {
249: resume(context);
250: State currentState = getCurrentState();
251: if (!(currentState instanceof ViewState)) {
252: throw new IllegalStateException(
253: "Current state is not a view state - cannot refresh; "
254: + "perhaps an unhandled exception occured in another state?");
255: }
256: ViewSelection selectedView = ((ViewState) currentState)
257: .refresh(context);
258: return pause(context, selectedView);
259: } catch (FlowExecutionException e) {
260: return pause(context, handleException(e, context));
261: } catch (Exception e) {
262: return pause(context, handleException(wrapException(e),
263: context));
264: }
265: } finally {
266: getListeners().fireRequestProcessed(context);
267: }
268: }
269:
270: /**
271: * Returns the listener list.
272: * @return the attached execution listeners.
273: */
274: FlowExecutionListeners getListeners() {
275: return listeners;
276: }
277:
278: /**
279: * Resume this flow execution.
280: * @param context the state request context
281: */
282: protected void resume(RequestControlContext context) {
283: getActiveSessionInternal().setStatus(FlowSessionStatus.ACTIVE);
284: getListeners().fireResumed(context);
285: }
286:
287: /**
288: * Pause this flow execution.
289: * @param context the request control context
290: * @param selectedView the initial selected view to render
291: * @return the selected view to render
292: */
293: protected ViewSelection pause(RequestControlContext context,
294: ViewSelection selectedView) {
295: if (!isActive()) {
296: // view selected by an end state
297: return selectedView;
298: }
299: getActiveSessionInternal().setStatus(FlowSessionStatus.PAUSED);
300: getListeners().firePaused(context, selectedView);
301: if (logger.isDebugEnabled()) {
302: if (selectedView != null) {
303: logger.debug("Paused to render " + selectedView
304: + " and wait for user input");
305: } else {
306: logger.debug("Paused to wait for user input");
307: }
308: }
309: return selectedView;
310: }
311:
312: /**
313: * Handles an exception that occured performing an operation on this flow
314: * execution. First trys the set of exception handlers associated with the
315: * offending state, then the handlers at the flow level.
316: * @param exception the exception that occured
317: * @param context the request control context the exception occured in
318: * @return the selected error view, never null
319: * @throws FlowExecutionException rethrows the exception if it was not handled
320: * at the state or flow level
321: */
322: protected ViewSelection handleException(
323: FlowExecutionException exception,
324: RequestControlContext context)
325: throws FlowExecutionException {
326: getListeners().fireExceptionThrown(context, exception);
327: if (logger.isDebugEnabled()) {
328: logger.debug("Attempting to handle [" + exception + "]");
329: }
330: try {
331: // the state could be null if the flow was attempting a start operation
332: ViewSelection selectedView = tryStateHandlers(exception,
333: context);
334: if (selectedView != null) {
335: if (logger.isDebugEnabled()) {
336: logger.debug("State '" + exception.getStateId()
337: + "' handled exception");
338: }
339: return selectedView;
340: }
341: selectedView = tryFlowHandlers(exception, context);
342: if (selectedView != null) {
343: if (logger.isDebugEnabled()) {
344: logger.debug("Flow '" + getCurrentFlow().getId()
345: + "' handled exception");
346: }
347: return selectedView;
348: }
349: } catch (FlowExecutionException newException) {
350: // exception handling resulted in a new FlowExecutionException, try to handle it
351: return handleException(newException, context);
352: } catch (Exception e) {
353: // a lower-level exception occured, wrap it in a flow execution exception and try to handle it
354: return handleException(wrapException(e), context);
355: }
356: if (logger.isDebugEnabled()) {
357: logger
358: .debug("Rethrowing unhandled flow execution exception");
359: }
360: throw exception;
361: }
362:
363: /**
364: * Try to handle given exception using execution exception handlers registered
365: * at the state level. Returns null if no handler handled the exception.
366: */
367: private ViewSelection tryStateHandlers(
368: FlowExecutionException exception,
369: RequestControlContext context) {
370: if (isActive() && exception.getStateId() != null) {
371: return getActiveFlow().getStateInstance(
372: exception.getStateId()).handleException(exception,
373: context);
374: } else {
375: return null;
376: }
377: }
378:
379: /**
380: * Try to handle given exception using execution exception handlers registered
381: * at the flow level. Returns null if no handler handled the exception.
382: */
383: private ViewSelection tryFlowHandlers(
384: FlowExecutionException exception,
385: RequestControlContext context) {
386: return getCurrentFlow().handleException(exception, context);
387: }
388:
389: // internal helpers
390:
391: /**
392: * Create a flow execution control context.
393: * @param externalContext the external context triggering this request
394: */
395: protected RequestControlContext createControlContext(
396: ExternalContext externalContext) {
397: return new RequestControlContextImpl(this , externalContext);
398: }
399:
400: /**
401: * Returns the currently active flow session.
402: * @throws IllegalStateException this execution is not active
403: */
404: FlowSessionImpl getActiveSessionInternal()
405: throws IllegalStateException {
406: assertActive();
407: return (FlowSessionImpl) flowSessions.getLast();
408: }
409:
410: /**
411: * Set the state that is currently active in this flow execution.
412: * @param newState the new current state
413: */
414: protected void setCurrentState(State newState) {
415: getActiveSessionInternal().setState(newState);
416: }
417:
418: /**
419: * Activate a new <code>FlowSession</code> for the flow definition.
420: * Creates the new flow session and pushes it onto the stack.
421: * @param flow the flow definition
422: * @return the new flow session
423: */
424: protected FlowSession activateSession(Flow flow) {
425: FlowSessionImpl session;
426: if (!flowSessions.isEmpty()) {
427: FlowSessionImpl parent = getActiveSessionInternal();
428: parent.setStatus(FlowSessionStatus.SUSPENDED);
429: session = createFlowSession(flow, parent);
430: } else {
431: session = createFlowSession(flow, null);
432: }
433: flowSessions.add(session);
434: session.setStatus(FlowSessionStatus.STARTING);
435: if (logger.isDebugEnabled()) {
436: logger.debug("Starting " + session);
437: }
438: return session;
439: }
440:
441: /**
442: * Create a new flow session object. Subclasses can override this to return
443: * a special implementation if required.
444: * @param flow the flow that should be associated with the flow session
445: * @param parent the flow session that should be the parent of the newly
446: * created flow session (may be null)
447: * @return the newly created flow session
448: */
449: FlowSessionImpl createFlowSession(Flow flow, FlowSessionImpl parent) {
450: return new FlowSessionImpl(flow, parent);
451: }
452:
453: /**
454: * End the active flow session, popping it of the stack.
455: * @return the ended session
456: */
457: public FlowSession endActiveFlowSession() {
458: FlowSessionImpl endingSession = (FlowSessionImpl) flowSessions
459: .removeLast();
460: endingSession.setStatus(FlowSessionStatus.ENDED);
461: if (!flowSessions.isEmpty()) {
462: if (logger.isDebugEnabled()) {
463: logger.debug("Resuming session '"
464: + getActiveSessionInternal().getDefinition()
465: .getId() + "' in state '"
466: + getActiveSessionInternal().getState().getId()
467: + "'");
468: }
469: getActiveSessionInternal().setStatus(
470: FlowSessionStatus.ACTIVE);
471: } else {
472: if (logger.isDebugEnabled()) {
473: logger
474: .debug("[Ended] - this execution is now inactive");
475: }
476: }
477: return endingSession;
478: }
479:
480: /**
481: * Make sure that this flow execution is active and throw an exception if it's
482: * not.
483: */
484: private void assertActive() throws IllegalStateException {
485: if (!isActive()) {
486: throw new IllegalStateException(
487: "This flow execution is not active, it has either ended or has never been started.");
488: }
489: }
490:
491: /**
492: * Returns the "current flow": which is the active flow if this execution is active, else the top-level flow definition.
493: */
494: private Flow getCurrentFlow() {
495: return isActive() ? getActiveFlow() : this .flow;
496: }
497:
498: /**
499: * Returns the id of the "current" state: a valid state identifier if the flow is active and in a state; null if the flow
500: * is not active or has not yet entered a state.
501: */
502: private String getCurrentStateId() {
503: if (isActive()) {
504: State state = getCurrentState();
505: if (state != null) {
506: return state.getId();
507: }
508: }
509: return null;
510: }
511:
512: /**
513: * Returns the currently active flow.
514: */
515: private Flow getActiveFlow() throws IllegalStateException {
516: return (Flow) getActiveSessionInternal().getDefinition();
517: }
518:
519: /**
520: * Returns the current state of this flow execution.
521: */
522: private State getCurrentState() throws IllegalStateException {
523: return (State) getActiveSessionInternal().getState();
524: }
525:
526: /**
527: * Wrap given exception in a FlowExecutionException.
528: */
529: private FlowExecutionException wrapException(Exception e) {
530: String flowId = getCurrentFlow().getId();
531: String stateId = getCurrentStateId();
532: return new FlowExecutionException(flowId, stateId,
533: "Exception thrown in state '" + stateId + "' of flow '"
534: + flowId + "'", e);
535: }
536:
537: // custom serialization (implementation of Externalizable for optimized
538: // storage)
539:
540: public void readExternal(ObjectInput in) throws IOException,
541: ClassNotFoundException {
542: flowId = (String) in.readObject();
543: flowSessions = (LinkedList) in.readObject();
544: }
545:
546: public void writeExternal(ObjectOutput out) throws IOException {
547: out.writeObject(flowId);
548: out.writeObject(flowSessions);
549: }
550:
551: public String toString() {
552: if (!isActive()) {
553: return "[Inactive " + getCaption() + "]";
554: } else {
555: if (flow != null) {
556: return new ToStringCreator(this ).append("flow",
557: flow.getId()).append("flowSessions",
558: flowSessions).toString();
559: } else {
560: return "[Unhydrated " + getCaption() + "]";
561: }
562: }
563: }
564:
565: // package private setters for restoring transient state
566: // used by FlowExecutionImplStateRestorer
567:
568: /**
569: * Restore the flow definition of this flow execution.
570: */
571: void setFlow(Flow flow) {
572: Assert.notNull(flow, "The root flow definition is required");
573: this .flow = flow;
574: this .flowId = flow.getId();
575: }
576:
577: /**
578: * Restore the listeners of this flow execution.
579: */
580: void setListeners(FlowExecutionListeners listeners) {
581: Assert.notNull(listeners,
582: "The execution listener list is required");
583: this .listeners = listeners;
584: }
585:
586: /**
587: * Restore the execution attributes of this flow execution.
588: */
589: void setAttributes(AttributeMap attributes) {
590: Assert.notNull(conversationScope,
591: "The execution attribute map is required");
592: this .attributes = attributes;
593: }
594:
595: /**
596: * Restore conversation scope for this flow execution.
597: */
598: void setConversationScope(MutableAttributeMap conversationScope) {
599: Assert.notNull(conversationScope,
600: "The conversation scope map is required");
601: this .conversationScope = conversationScope;
602: }
603:
604: /**
605: * Returns the flow definition id of this flow execution.
606: */
607: String getFlowId() {
608: return flowId;
609: }
610:
611: /**
612: * Returns the list of flow session maintained by this flow execution.
613: */
614: LinkedList getFlowSessions() {
615: return flowSessions;
616: }
617:
618: /**
619: * Are there any flow sessions in this flow execution?
620: */
621: boolean hasSessions() {
622: return !flowSessions.isEmpty();
623: }
624:
625: /**
626: * Are there any sessions for sub flows in this flow execution?
627: */
628: boolean hasSubflowSessions() {
629: return flowSessions.size() > 1;
630: }
631:
632: /**
633: * Returns the flow session for the root flow of this flow execution.
634: */
635: FlowSessionImpl getRootSession() {
636: return (FlowSessionImpl) flowSessions.getFirst();
637: }
638:
639: /**
640: * Returns an iterator looping over the subflow sessions
641: * in this flow execution.
642: */
643: ListIterator getSubflowSessionIterator() {
644: return flowSessions.listIterator(1);
645: }
646: }
|