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.test.execution;
017:
018: import java.util.Collection;
019: import java.util.Map;
020:
021: import junit.framework.TestCase;
022:
023: import org.springframework.binding.expression.ExpressionParser;
024: import org.springframework.core.style.StylerUtils;
025: import org.springframework.util.Assert;
026: import org.springframework.webflow.context.ExternalContext;
027: import org.springframework.webflow.core.DefaultExpressionParserFactory;
028: import org.springframework.webflow.core.collection.MutableAttributeMap;
029: import org.springframework.webflow.core.collection.ParameterMap;
030: import org.springframework.webflow.definition.FlowDefinition;
031: import org.springframework.webflow.engine.impl.FlowExecutionImplFactory;
032: import org.springframework.webflow.execution.FlowExecution;
033: import org.springframework.webflow.execution.FlowExecutionException;
034: import org.springframework.webflow.execution.FlowExecutionFactory;
035: import org.springframework.webflow.execution.ViewSelection;
036: import org.springframework.webflow.execution.support.ApplicationView;
037: import org.springframework.webflow.execution.support.ExternalRedirect;
038: import org.springframework.webflow.execution.support.FlowDefinitionRedirect;
039: import org.springframework.webflow.execution.support.FlowExecutionRedirect;
040: import org.springframework.webflow.test.MockExternalContext;
041:
042: /**
043: * Base class for integration tests that verify a flow executes as expected.
044: * Flow execution tests captured by subclasses should test that a flow responds
045: * to all supported transition criteria correctly, transitioning to the correct
046: * states and producing the expected results on the occurence of possible
047: * external (user) events.
048: * <p>
049: * More specifically, a typical flow execution test case will test:
050: * <ul>
051: * <li>That the flow execution starts as expected given a request from an
052: * external context containing potential input attributes (see the
053: * {@link #startFlow(MutableAttributeMap, ExternalContext)} variants).
054: * <li>That given the set of supported state transition criteria a state
055: * executes the appropriate transition when a matching event is signaled (with
056: * potential input request parameters, see the
057: * {@link #signalEvent(String, ExternalContext)} variants). A test case should
058: * be coded for each logical event that can occur, where an event drives a
059: * possible path through the flow. The goal should be to exercise all possible
060: * paths of the flow. Use a test coverage tool like Clover or Emma to assist
061: * with measuring your test's effectiveness.
062: * <li>That given a transition that leads to an interactive state type (a view
063: * state or an end state) that the view selection returned to the client matches
064: * what was expected and the current state of the flow matches what is expected.
065: * </ul>
066: * <p>
067: * A flow execution test can effectively automate and validate the orchestration
068: * required to drive an end-to-end business task that spans several steps
069: * involving the user to complete. Such tests are a good way to test your system
070: * top-down starting at the web-tier and pushing through all the way to the DB
071: * without having to deploy to a servlet or portlet container. In addition, they
072: * can be used to effectively test a flow's execution (the web layer)
073: * standalone, typically with a mock service layer. Both styles of testing are
074: * valuable and supported.
075: *
076: * @author Keith Donald
077: */
078: public abstract class AbstractFlowExecutionTests extends TestCase {
079:
080: /**
081: * The factory that will create the flow execution to test.
082: */
083: private FlowExecutionFactory flowExecutionFactory;
084:
085: /**
086: * The expression parser for parsing evaluatable model attribute
087: * expressions.
088: */
089: private ExpressionParser expressionParser = DefaultExpressionParserFactory
090: .getExpressionParser();
091:
092: /**
093: * The flow execution running the flow when the test is active (runtime
094: * object).
095: */
096: private FlowExecution flowExecution;
097:
098: /**
099: * Constructs a default flow execution test.
100: * @see #setName(String)
101: */
102: public AbstractFlowExecutionTests() {
103: super ();
104: }
105:
106: /**
107: * Constructs a flow execution test with given name.
108: * @param name the name of the test
109: * @since 1.0.2
110: */
111: public AbstractFlowExecutionTests(String name) {
112: super (name);
113: }
114:
115: /**
116: * Set the expression parser responsible for parsing expression strings into
117: * evaluatable expression objects.
118: */
119: public void setExpressionParser(ExpressionParser expressionParser) {
120: Assert.notNull(expressionParser,
121: "The expression parser is required");
122: this .expressionParser = expressionParser;
123: }
124:
125: /**
126: * Gets the factory that will create the flow execution to test. This method
127: * will create the factory if it is not already set.
128: * @return the flow execution factory
129: * @see #createFlowExecutionFactory()
130: */
131: protected FlowExecutionFactory getFlowExecutionFactory() {
132: if (flowExecutionFactory == null) {
133: flowExecutionFactory = createFlowExecutionFactory();
134: }
135: return flowExecutionFactory;
136: }
137:
138: /**
139: * Creates an ExternalContext instance. Defaults to using {@link MockExternalContext}.
140: * Subclasses can override if they which to use another external context
141: * implementation.
142: * @param requestParameters request parameters to put into the
143: * external context (optional)
144: * @return a new ExternalContext instance
145: */
146: protected ExternalContext createExternalContext(
147: ParameterMap requestParameters) {
148: return new MockExternalContext(requestParameters);
149: }
150:
151: /**
152: * Start the flow execution to be tested.
153: * <p>
154: * Convenience operation that starts the execution with:
155: * <ul>
156: * <li>no input attributes
157: * <li>an empty {@link ExternalContext} with no environmental request
158: * parameters set
159: * </ul>
160: * @return the view selection made as a result of starting the flow
161: * (returned when the first interactive state (a view state or end state) is
162: * entered)
163: * @throws FlowExecutionException if an exception was thrown while starting
164: * the flow execution
165: */
166: protected ViewSelection startFlow() throws FlowExecutionException {
167: return startFlow(null, createExternalContext(null));
168: }
169:
170: /**
171: * Start the flow execution to be tested.
172: * <p>
173: * Convenience operation that starts the execution with:
174: * <ul>
175: * <li>the specified input attributes, eligible for mapping by the root
176: * flow
177: * <li>an empty {@link ExternalContext} with no environmental request
178: * parameters set
179: * </ul>
180: * @param input the flow execution input attributes eligible for mapping by
181: * the root flow
182: * @return the view selection made as a result of starting the flow
183: * (returned when the first interactive state (a view state or end state) is
184: * entered)
185: * @throws FlowExecutionException if an exception was thrown while starting
186: * the flow execution
187: */
188: protected ViewSelection startFlow(MutableAttributeMap input)
189: throws FlowExecutionException {
190: return startFlow(input, createExternalContext(null));
191: }
192:
193: /**
194: * Start the flow execution to be tested.
195: * <p>
196: * This is the most flexible of the start methods. It allows you to specify:
197: * <ol>
198: * <li>a map of input attributes to pass to the flow execution, eligible
199: * for mapping by the root flow definition
200: * <li>an external context that provides the flow execution being tested
201: * access to the calling environment for this request
202: * </ol>
203: * @param input the flow execution input attributes eligible for mapping by
204: * the root flow
205: * @param context the external context providing information about the
206: * caller's environment, used by the flow execution during the start
207: * operation
208: * @return the view selection made as a result of starting the flow
209: * (returned when the first interactive state (a view state or end state) is
210: * entered)
211: * @throws FlowExecutionException if an exception was thrown while starting
212: * the flow execution
213: */
214: protected ViewSelection startFlow(MutableAttributeMap input,
215: ExternalContext context) throws FlowExecutionException {
216: flowExecution = getFlowExecutionFactory().createFlowExecution(
217: getFlowDefinition());
218: return flowExecution.start(input, context);
219: }
220:
221: /**
222: * Signal an occurence of an event in the current state of the flow
223: * execution being tested.
224: * @param eventId the event that occured
225: * @throws FlowExecutionException if an exception was thrown within a state
226: * of the resumed flow execution during event processing
227: */
228: protected ViewSelection signalEvent(String eventId)
229: throws FlowExecutionException {
230: return signalEvent(eventId, createExternalContext(null));
231: }
232:
233: /**
234: * Signal an occurence of an event in the current state of the flow
235: * execution being tested.
236: * @param eventId the event that occured
237: * @param requestParameters request parameters needed by the flow execution
238: * to complete event processing
239: * @throws FlowExecutionException if an exception was thrown within a state
240: * of the resumed flow execution during event processing
241: */
242: protected ViewSelection signalEvent(String eventId,
243: ParameterMap requestParameters)
244: throws FlowExecutionException {
245: return signalEvent(eventId,
246: createExternalContext(requestParameters));
247: }
248:
249: /**
250: * Signal an occurence of an event in the current state of the flow
251: * execution being tested.
252: * <p>
253: * Note: signaling an event will cause state transitions to occur in a chain
254: * until control is returned to the caller. Control is returned once an
255: * "interactive" state type is entered: either a view state when the flow is
256: * paused or an end state when the flow terminates. Action states are
257: * executed without returning control, as their result always triggers
258: * another state transition, executed internally. Action states can also be
259: * executed in a chain like fashion (e.g. action state 1 (result), action
260: * state 2 (result), action state 3 (result), view state <control returns so
261: * view can be rendered>).
262: * <p>
263: * If you wish to verify expected behavior on each state transition (and not
264: * just when the view state triggers return of control back to the client),
265: * you have a few options:
266: * <p>
267: * First, you may implement standalone unit tests for your
268: * {@link org.springframework.webflow.execution.Action} implementations.
269: * There you can verify that an Action executes its logic properly in
270: * isolation. When you do this, you may mock or stub out services the Action
271: * implementation needs that are expensive to initialize. You can also
272: * verify there that the action puts everything in the flow or request scope
273: * it was expected to (to meet its contract with the view it is prepping for
274: * display, for example).
275: * <p>
276: * Second, you can attach one or more FlowExecutionListeners to the flow
277: * execution at start time within your test code, which will allow you to
278: * receive a callback on each state transition (among other points). It is
279: * recommended you extend
280: * {@link org.springframework.webflow.execution.FlowExecutionListenerAdapter}
281: * and only override the callback methods you are interested in.
282: * @param eventId the event that occured
283: * @param context the external context providing information about the
284: * caller's environment, used by the flow execution during the signal event
285: * operation
286: * @return the view selection that was made, returned once control is
287: * returned to the client (occurs when the flow enters a view state, or an
288: * end state)
289: * @throws FlowExecutionException if an exception was thrown within a state
290: * of the resumed flow execution during event processing
291: */
292: protected ViewSelection signalEvent(String eventId,
293: ExternalContext context) throws FlowExecutionException {
294: Assert
295: .state(
296: flowExecution != null,
297: "The flow execution to test is [null]; "
298: + "you must start the flow execution before you can signal an event against it!");
299: return flowExecution.signalEvent(eventId, context);
300: }
301:
302: /**
303: * Refresh the flow execution being tested, asking the current view state to
304: * make a "refresh" view selection. This is idempotent operation that may be
305: * safely called on an active but currently paused execution. Used to
306: * simulate a browser flow execution redirect.
307: * @return the current view selection for this flow execution
308: * @throws FlowExecutionException if an exception was thrown during refresh
309: */
310: protected ViewSelection refresh() throws FlowExecutionException {
311: return refresh(createExternalContext(null));
312: }
313:
314: /**
315: * Refresh the flow execution being tested, asking the current view state
316: * state to make a "refresh" view selection. This is idempotent operation
317: * that may be safely called on an active but currently paused execution.
318: * Used to simulate a browser flow execution redirect.
319: * @param context the external context providing information about the
320: * caller's environment, used by the flow execution during the refresh
321: * operation
322: * @return the current view selection for this flow execution
323: * @throws FlowExecutionException if an exception was thrown during refresh
324: */
325: protected ViewSelection refresh(ExternalContext context)
326: throws FlowExecutionException {
327: Assert
328: .state(
329: flowExecution != null,
330: "The flow execution to test is [null]; you must start the flow execution before you can refresh it!");
331: return flowExecution.refresh(context);
332: }
333:
334: // convenience accessors
335:
336: /**
337: * Returns the flow execution being tested.
338: * @return the flow execution
339: * @throws IllegalStateException the execution has not been started
340: */
341: protected FlowExecution getFlowExecution()
342: throws IllegalStateException {
343: Assert
344: .state(
345: flowExecution != null,
346: "The flow execution to test is [null]; you must start the flow execution before you can query it!");
347: return flowExecution;
348: }
349:
350: /**
351: * Returns the attribute in conversation scope. Conversation-scoped
352: * attributes are shared by all flow sessions.
353: * @param attributeName the name of the attribute
354: * @return the attribute value
355: */
356: protected Object getConversationAttribute(String attributeName) {
357: return getFlowExecution().getConversationScope().get(
358: attributeName);
359: }
360:
361: /**
362: * Returns the required attribute in conversation scope; asserts the
363: * attribute is present. Conversation-scoped attributes are shared by all
364: * flow sessions.
365: * @param attributeName the name of the attribute
366: * @return the attribute value
367: * @throws IllegalStateException if the attribute was not present
368: */
369: protected Object getRequiredConversationAttribute(
370: String attributeName) throws IllegalStateException {
371: return getFlowExecution().getConversationScope().getRequired(
372: attributeName);
373: }
374:
375: /**
376: * Returns the required attribute in conversation scope; asserts the
377: * attribute is present and of the required type. Conversation-scoped
378: * attributes are shared by all flow sessions.
379: * @param attributeName the name of the attribute
380: * @return the attribute value
381: * @throws IllegalStateException if the attribute was not present or not of
382: * the required type
383: */
384: protected Object getRequiredConversationAttribute(
385: String attributeName, Class requiredType)
386: throws IllegalStateException {
387: return getFlowExecution().getConversationScope().getRequired(
388: attributeName, requiredType);
389: }
390:
391: /**
392: * Returns the attribute in flow scope. Flow-scoped attributes are local to
393: * the active flow session.
394: * @param attributeName the name of the attribute
395: * @return the attribute value
396: */
397: protected Object getFlowAttribute(String attributeName) {
398: return getFlowExecution().getActiveSession().getScope().get(
399: attributeName);
400: }
401:
402: /**
403: * Returns the required attribute in flow scope; asserts the attribute is
404: * present. Flow-scoped attributes are local to the active flow session.
405: * @param attributeName the name of the attribute
406: * @return the attribute value
407: * @throws IllegalStateException if the attribute was not present
408: */
409: protected Object getRequiredFlowAttribute(String attributeName)
410: throws IllegalStateException {
411: return getFlowExecution().getActiveSession().getScope()
412: .getRequired(attributeName);
413: }
414:
415: /**
416: * Returns the required attribute in flow scope; asserts the attribute is
417: * present and of the correct type. Flow-scoped attributes are local to the
418: * active flow session.
419: * @param attributeName the name of the attribute
420: * @return the attribute value
421: * @throws IllegalStateException if the attribute was not present or was of
422: * the wrong type
423: */
424: protected Object getRequiredFlowAttribute(String attributeName,
425: Class requiredType) throws IllegalStateException {
426: return getFlowExecution().getActiveSession().getScope()
427: .getRequired(attributeName, requiredType);
428: }
429:
430: /**
431: * Returns the attribute in flash scope. Flash-scoped attributes are local to
432: * the active flow session and cleared on the next user event.
433: * @param attributeName the name of the attribute
434: * @return the attribute value
435: * @since 1.0.2
436: */
437: protected Object getFlashAttribute(String attributeName) {
438: return getFlowExecution().getActiveSession().getFlashMap().get(
439: attributeName);
440: }
441:
442: /**
443: * Returns the required attribute in flash scope; asserts the attribute is
444: * present. Flash-scoped attributes are local to the active flow session and cleared on
445: * the next user event.
446: * @param attributeName the name of the attribute
447: * @return the attribute value
448: * @throws IllegalStateException if the attribute was not present
449: * @since 1.0.2
450: */
451: protected Object getRequiredFlashAttribute(String attributeName)
452: throws IllegalStateException {
453: return getFlowExecution().getActiveSession().getFlashMap()
454: .getRequired(attributeName);
455: }
456:
457: /**
458: * Returns the required attribute in flash scope; asserts the attribute is
459: * present and of the correct type. Flash-scoped attributes are local to the
460: * active flow session and cleared on the next user event.
461: * @param attributeName the name of the attribute
462: * @return the attribute value
463: * @throws IllegalStateException if the attribute was not present or was of
464: * the wrong type
465: */
466: protected Object getRequiredFlashAttribute(String attributeName,
467: Class requiredType) throws IllegalStateException {
468: return getFlowExecution().getActiveSession().getFlashMap()
469: .getRequired(attributeName, requiredType);
470: }
471:
472: // assert helpers
473:
474: /**
475: * Assert that the active flow session is for the flow with the provided id.
476: * @param expectedActiveFlowId the flow id that should have a session active
477: * in the tested flow execution
478: */
479: protected void assertActiveFlowEquals(String expectedActiveFlowId) {
480: assertEquals("The active flow id '"
481: + getFlowExecution().getActiveSession().getDefinition()
482: .getId()
483: + "' does not equal the expected active flow id '"
484: + expectedActiveFlowId + "'", expectedActiveFlowId,
485: getFlowExecution().getActiveSession().getDefinition()
486: .getId());
487: }
488:
489: /**
490: * Assert that the entire flow execution is active; that is, it has not
491: * ended and has been started.
492: */
493: protected void assertFlowExecutionActive() {
494: assertTrue("The flow execution is not active but it should be",
495: getFlowExecution().isActive());
496: }
497:
498: /**
499: * Assert that the entire flow execution has ended; that is, it is no longer
500: * active.
501: */
502: protected void assertFlowExecutionEnded() {
503: assertTrue(
504: "The flow execution is still active but it should have ended",
505: !getFlowExecution().isActive());
506: }
507:
508: /**
509: * Assert that the current state of the flow execution equals the provided
510: * state id.
511: * @param expectedCurrentStateId the expected current state
512: */
513: protected void assertCurrentStateEquals(
514: String expectedCurrentStateId) {
515: assertEquals("The current state '"
516: + getFlowExecution().getActiveSession().getState()
517: .getId()
518: + "' does not equal the expected state '"
519: + expectedCurrentStateId + "'", expectedCurrentStateId,
520: getFlowExecution().getActiveSession().getState()
521: .getId());
522: }
523:
524: /**
525: * Assert that the view name equals the provided value.
526: * @param expectedViewName the expected name
527: * @param viewSelection the selected view
528: */
529: protected void assertViewNameEquals(String expectedViewName,
530: ApplicationView viewSelection) {
531: assertEquals("The view name is wrong:", expectedViewName,
532: viewSelection.getViewName());
533: }
534:
535: /**
536: * Assert that the selected view contains the specified model attribute with
537: * the provided expected value.
538: * @param expectedValue the expected value
539: * @param attributeName the attribute name (can be an expression)
540: * @param viewSelection the selected view with a model attribute map to
541: * assert against
542: */
543: protected void assertModelAttributeEquals(Object expectedValue,
544: String attributeName, ApplicationView viewSelection) {
545: assertEquals("The model attribute '" + attributeName
546: + "' value is wrong:", expectedValue,
547: evaluateModelAttributeExpression(attributeName,
548: viewSelection.getModel()));
549: }
550:
551: /**
552: * Assert that the selected view contains the specified collection model
553: * attribute with the provided expected size.
554: * @param expectedSize the expected size
555: * @param attributeName the collection attribute name (can be an expression
556: * @param viewSelection the selected view with a model attribute map to
557: * assert against
558: */
559: protected void assertModelAttributeCollectionSize(int expectedSize,
560: String attributeName, ApplicationView viewSelection) {
561: assertModelAttributeNotNull(attributeName, viewSelection);
562: Collection c = (Collection) evaluateModelAttributeExpression(
563: attributeName, viewSelection.getModel());
564: assertEquals("The model attribute '" + attributeName
565: + "' collection size is wrong:", expectedSize, c.size());
566: }
567:
568: /**
569: * Assert that the selected view contains the specified model attribute.
570: * @param attributeName the attribute name (can be an expression)
571: * @param viewSelection the selected view with a model attribute map to
572: * assert against
573: */
574: protected void assertModelAttributeNotNull(String attributeName,
575: ApplicationView viewSelection) {
576: assertNotNull("The model attribute '" + attributeName
577: + "' is null but should not be; model contents are "
578: + StylerUtils.style(viewSelection.getModel()),
579: evaluateModelAttributeExpression(attributeName,
580: viewSelection.getModel()));
581: }
582:
583: /**
584: * Assert that the selected view does not contain the specified model
585: * attribute.
586: * @param attributeName the attribute name (can be an expression)
587: * @param viewSelection the selected view with a model attribute map to
588: * assert against
589: */
590: protected void assertModelAttributeNull(String attributeName,
591: ApplicationView viewSelection) {
592: assertNull("The model attribute '" + attributeName
593: + "' is not null but should be; model contents are "
594: + StylerUtils.style(viewSelection.getModel()),
595: evaluateModelAttributeExpression(attributeName,
596: viewSelection.getModel()));
597: }
598:
599: // other helpers
600:
601: /**
602: * Assert that the returned view selection is an instance of
603: * {@link ApplicationView}.
604: * @param viewSelection the view selection
605: */
606: protected ApplicationView applicationView(
607: ViewSelection viewSelection) {
608: Assert.isInstanceOf(ApplicationView.class, viewSelection,
609: "Unexpected class of view selection: ");
610: return (ApplicationView) viewSelection;
611: }
612:
613: /**
614: * Assert that the returned view selection is an instance of
615: * {@link FlowExecutionRedirect}.
616: * @param viewSelection the view selection
617: */
618: protected FlowExecutionRedirect flowExecutionRedirect(
619: ViewSelection viewSelection) {
620: Assert.isInstanceOf(FlowExecutionRedirect.class, viewSelection,
621: "Unexpected class of view selection: ");
622: return (FlowExecutionRedirect) viewSelection;
623: }
624:
625: /**
626: * Assert that the returned view selection is an instance of
627: * {@link FlowDefinitionRedirect}.
628: * @param viewSelection the view selection
629: */
630: protected FlowDefinitionRedirect flowDefinitionRedirect(
631: ViewSelection viewSelection) {
632: Assert.isInstanceOf(FlowDefinitionRedirect.class,
633: viewSelection, "Unexpected class of view selection: ");
634: return (FlowDefinitionRedirect) viewSelection;
635: }
636:
637: /**
638: * Assert that the returned view selection is an instance of
639: * {@link ExternalRedirect}.
640: * @param viewSelection the view selection
641: */
642: protected ExternalRedirect externalRedirect(
643: ViewSelection viewSelection) {
644: Assert.isInstanceOf(ExternalRedirect.class, viewSelection,
645: "Unexpected class of view selection: ");
646: return (ExternalRedirect) viewSelection;
647: }
648:
649: /**
650: * Assert that the returned view selection is the
651: * {@link ViewSelection#NULL_VIEW}.
652: * @param viewSelection the view selection
653: */
654: protected void nullView(ViewSelection viewSelection) {
655: assertEquals("Not the null view selection:", viewSelection,
656: ViewSelection.NULL_VIEW);
657: }
658:
659: /**
660: * Evaluates a model attribute expression.
661: * @param attributeName the attribute expression
662: * @param model the model map
663: * @return the attribute expression value
664: */
665: protected Object evaluateModelAttributeExpression(
666: String attributeName, Map model) {
667: return expressionParser.parseExpression(attributeName)
668: .evaluate(model, null);
669: }
670:
671: /**
672: * Factory method to create the flow execution factory. Subclasses
673: * could override this if they want to use a custom flow execution factory
674: * or custom configuration of the flow execution factory, registering
675: * flow execution listeners for instance.
676: * The default implementation just returns a {@link FlowExecutionImplFactory}
677: * instance.
678: * @return the flow execution factory
679: */
680: protected FlowExecutionFactory createFlowExecutionFactory() {
681: return new FlowExecutionImplFactory();
682: }
683:
684: /**
685: * Directly update the flow execution used by the test by setting
686: * it to given flow execution. Use this if you have somehow manipulated
687: * the flow execution being tested and want to continue the test
688: * with another flow execution.
689: * @param flowExecution the flow execution to use
690: */
691: protected void updateFlowExecution(FlowExecution flowExecution) {
692: this .flowExecution = flowExecution;
693: }
694:
695: /**
696: * Returns the flow definition to be tested. Subclasses must implement.
697: * @return the flow definition
698: */
699: protected abstract FlowDefinition getFlowDefinition();
700: }
|