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:
020: import org.springframework.core.style.StylerUtils;
021: import org.springframework.core.style.ToStringCreator;
022: import org.springframework.webflow.execution.Action;
023: import org.springframework.webflow.execution.Event;
024: import org.springframework.webflow.execution.FlowExecutionException;
025: import org.springframework.webflow.execution.RequestContext;
026: import org.springframework.webflow.execution.ViewSelection;
027:
028: /**
029: * A transitionable state that executes one or more actions when entered. When
030: * the action(s) are executed this state responds to their result(s) to decide
031: * what state to transition to next.
032: * <p>
033: * If more than one action is configured they are executed in an ordered chain
034: * until one returns a result event that matches a state transition out of
035: * this state. This is a form of the Chain of Responsibility (CoR) pattern.
036: * <p>
037: * The result of an action's execution is typically the criteria for a
038: * transition out of this state. Additional information in the current
039: * {@link RequestContext} may also be tested as part of custom transitional
040: * criteria, allowing for sophisticated transition expressions that reason on
041: * contextual state.
042: * <p>
043: * Each action executed by this action state may be provisioned with a set of
044: * arbitrary execution properties. These properties are made available to the
045: * action at execution time and may be used to influence action execution
046: * behavior.
047: * <p>
048: * Common action execution properties include:
049: * <p>
050: * <table border="1">
051: * <th>Property</th>
052: * <th>Description</th>
053: * <tr>
054: * <td valign="top">name</td>
055: * <td>The 'name' property is used as a qualifier for an action's result event,
056: * and is typically used to allow the flow to respond to a specific action's
057: * outcome within a larger action chain. For example, if an action named
058: * <code>myAction</code> returns a <code>success</code> result, a transition
059: * that matches on event <code>myAction.success</code> will be searched, and
060: * if found, executed. If this action is not assigned a name a transition for
061: * the base <code>success</code> event will be searched and if found,
062: * executed.<br>
063: * This is useful in situations where you want to execute actions in an ordered
064: * chain as part of one action state, and wish to transition on the result of
065: * the last one in the chain. For example:
066: *
067: * <pre>
068: * <action-state id="setupForm">
069: * <action name="setup" bean="myAction" method="setupForm"/>
070: * <action name="referenceData" bean="myAction" method="setupReferenceData"/>
071: * <transition on="referenceData.success" to="displayForm"/>
072: * </action-state>
073: * </pre>
074: *
075: * When the 'setupForm' state above is entered, the 'setup' action will execute,
076: * followed by the 'referenceData' action. After 'referenceData' execution, the
077: * flow will then respond to the 'referenceData.success' event by transitioning
078: * to the 'displayForm' state. The 'setup.success' event that was signaled by
079: * the 'setup' action will effectively be ignored.</td>
080: * <tr>
081: * <td valign="top">method</td>
082: * <td>The 'method' property is the name of a target method on a
083: * <code>{@link org.springframework.webflow.action.MultiAction}</code> to
084: * execute. In the MultiAction scenario the named method must have the signature
085: * <code>public Event ${method}(RequestContext) throws Exception</code>.
086: * As an example of this scenario, a method property with value <code>setupForm</code>
087: * would bind to a method on a MultiAction instance with the signature:
088: * <code>public Event setupForm(RequestContext context)</code>. <br>
089: * As an alternative to a MultiAction method binding, this action state may
090: * excute a
091: * {@link org.springframework.webflow.action.AbstractBeanInvokingAction bean invoking action}
092: * that invokes a method on a POJO (Plain Old Java Object). If the method
093: * signature accepts arguments those arguments may be specified by using the
094: * format:
095: *
096: * <pre>
097: * methodName(${arg1}, ${arg2}, ...)
098: * </pre>
099: *
100: * Argument ${expressions} are evaluated against the current
101: * <code>RequestContext</code>, allowing for data stored in flow scope or
102: * request scope to be passed as arguments to the POJO. In addition, POJO return
103: * values may be exposed to the flow automatically. See the bean invoking action
104: * type hierarchy for more information. </td>
105: * </tr>
106: * </table>
107: *
108: * @see org.springframework.webflow.execution.Action
109: * @see org.springframework.webflow.action.MultiAction
110: * @see org.springframework.webflow.action.AbstractBeanInvokingAction
111: *
112: * @author Keith Donald
113: * @author Erwin Vervaet
114: */
115: public class ActionState extends TransitionableState {
116:
117: /**
118: * The list of actions to be executed when this state is entered.
119: */
120: private ActionList actionList = new ActionList();
121:
122: /**
123: * Creates a new action state.
124: * @param flow the owning flow
125: * @param id the state identifier (must be unique to the flow)
126: * @throws IllegalArgumentException when this state cannot be added to given flow,
127: * e.g. beasue the id is not unique
128: * @see #getActionList()
129: */
130: public ActionState(Flow flow, String id)
131: throws IllegalArgumentException {
132: super (flow, id);
133: }
134:
135: /**
136: * Returns the list of actions executable by this action state. The
137: * returned list is mutable.
138: * @return the state action list
139: */
140: public ActionList getActionList() {
141: return actionList;
142: }
143:
144: /*
145: * Overrides getRequiredTransition(RequestContext) to throw a local
146: * NoMatchingActionResultTransitionException if a transition on the
147: * occurence of an action result event cannot be matched. Used to facilitate
148: * an action invocation chain.
149: * <p>Note that we cannot catch NoMatchingTransitionException since that could lead to unwanted
150: * situations where we're catching an exception that's generated by another
151: * state, e.g. because of a configuration error!
152: */
153: public Transition getRequiredTransition(RequestContext context)
154: throws NoMatchingTransitionException {
155: Transition transition = getTransitionSet().getTransition(
156: context);
157: if (transition == null) {
158: throw new NoMatchingActionResultTransitionException(this ,
159: context.getLastEvent());
160: }
161: return transition;
162: }
163:
164: /**
165: * Specialization of State's <code>doEnter</code> template method that
166: * executes behaviour specific to this state type in polymorphic fashion.
167: * <p>
168: * This implementation iterates over each configured <code>Action</code>
169: * instance and executes it. Execution continues until an
170: * <code>Action</code> returns a result event that matches a transition in
171: * this request context, or the set of all actions is exhausted.
172: * @param context the control context for the currently executing flow, used
173: * by this state to manipulate the flow execution
174: * @return a view selection signaling that control should be returned to the
175: * client and a view rendered
176: * @throws FlowExecutionException if an exception occurs in this state
177: */
178: protected ViewSelection doEnter(RequestControlContext context)
179: throws FlowExecutionException {
180: int executionCount = 0;
181: String[] eventIds = new String[actionList.size()];
182: Iterator it = actionList.iterator();
183: while (it.hasNext()) {
184: Action action = (Action) it.next();
185: Event event = ActionExecutor.execute(action, context);
186: if (event != null) {
187: eventIds[executionCount] = event.getId();
188: try {
189: // will check both local state transitions and global transitions
190: return context.signalEvent(event);
191: } catch (NoMatchingActionResultTransitionException e) {
192: if (logger.isDebugEnabled()) {
193: logger
194: .debug("Action execution ["
195: + (executionCount + 1)
196: + "] resulted in no matching transition on event '"
197: + event.getId()
198: + "'"
199: + (it.hasNext() ? ": proceeding to the next action in the list"
200: : ": action list exhausted"));
201: }
202: }
203: } else {
204: if (logger.isDebugEnabled()) {
205: logger
206: .debug("Action execution ["
207: + (executionCount + 1)
208: + "] returned a [null] event"
209: + (it.hasNext() ? ": proceeding to the next action in the list"
210: : ": action list exhausted"));
211: }
212: eventIds[executionCount] = null;
213: }
214: executionCount++;
215: }
216: if (executionCount > 0) {
217: throw new NoMatchingTransitionException(
218: getFlow().getId(),
219: getId(),
220: context.getLastEvent(),
221: "No transition was matched on the event(s) signaled by the ["
222: + executionCount
223: + "] action(s) that executed in this action state '"
224: + getId()
225: + "' of flow '"
226: + getFlow().getId()
227: + "'; transitions must be defined to handle action result outcomes -- "
228: + "possible flow configuration error? Note: the eventIds signaled were: '"
229: + StylerUtils.style(eventIds)
230: + "', while the supported set of transitional criteria for this action state is '"
231: + StylerUtils.style(getTransitionSet()
232: .getTransitionCriterias()) + "'");
233: } else {
234: throw new IllegalStateException(
235: "No actions were executed, thus I cannot execute any state transition "
236: + "-- programmer configuration error; make sure you add at least one action to this state's action list");
237: }
238: }
239:
240: protected void appendToString(ToStringCreator creator) {
241: creator.append("actionList", actionList);
242: super .appendToString(creator);
243: }
244:
245: /**
246: * Local "no transition found" exception used to report that an action
247: * result could not be mapped to a state transition.
248: *
249: * @author Keith Donald
250: * @author Erwin Vervaet
251: */
252: private static class NoMatchingActionResultTransitionException
253: extends NoMatchingTransitionException {
254:
255: /**
256: * Creates a new exception.
257: * @param state the action state
258: * @param resultEvent the action result event
259: */
260: public NoMatchingActionResultTransitionException(
261: ActionState state, Event resultEvent) {
262: super (
263: state.getFlow().getId(),
264: state.getId(),
265: resultEvent,
266: "Cannot find a transition matching an action result event; continuing with next action...");
267: }
268: }
269: }
|