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 org.apache.commons.logging.Log;
019: import org.apache.commons.logging.LogFactory;
020: import org.springframework.core.style.ToStringCreator;
021: import org.springframework.util.Assert;
022: import org.springframework.webflow.definition.TransitionDefinition;
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 path from one {@link TransitionableState state} to another
030: * {@link State state}.
031: * <p>
032: * When executed a transition takes a flow execution from its current state,
033: * called the <i>source state</i>, to another state, called the </i>target
034: * state</i>. A transition may become eligible for execution on the occurence
035: * of an {@link Event} from within a transitionable source state.
036: * <p>
037: * When an event occurs within this transition's source
038: * <code>TransitionableState</code> the determination of the eligibility of
039: * this transition is made by a <code>TransitionCriteria</code> object called
040: * the <i>matching criteria</i>. If the matching criteria returns
041: * <code>true</code> this transition is marked eligible for execution for that
042: * event.
043: * <p>
044: * Determination as to whether an eligible transition should be allowed to
045: * execute is made by a <code>TransitionCriteria</code> object called the
046: * <i>execution criteria</i>. If the execution criteria test fails this
047: * transition will <i>roll back</i> and reenter its source state. If the
048: * execution criteria test succeeds this transition will execute and take the
049: * flow to the transition's target state.
050: * <p>
051: * The target state of this transition is typically specified at configuration
052: * time in a static manner. If the target state of this transition needs to be
053: * calculated in a dynamic fashion at runtime configure a {@link TargetStateResolver}
054: * that supports such calculations.
055: *
056: * @see TransitionableState
057: * @see TransitionCriteria
058: * @see TargetStateResolver
059: *
060: * @author Keith Donald
061: * @author Erwin Vervaet
062: */
063: public class Transition extends AnnotatedObject implements
064: TransitionDefinition {
065:
066: /**
067: * Logger, for use in subclasses.
068: */
069: protected final Log logger = LogFactory.getLog(Transition.class);
070:
071: /**
072: * The criteria that determine whether or not this transition matches as
073: * eligible for execution when an event occurs in the source state.
074: */
075: private TransitionCriteria matchingCriteria;
076:
077: /**
078: * The criteria that determine whether or not this transition, once matched,
079: * should complete execution or should <i>roll back</i>.
080: */
081: private TransitionCriteria executionCriteria = WildcardTransitionCriteria.INSTANCE;
082:
083: /**
084: * The resolver responsible for calculating the target state of this
085: * transition.
086: */
087: private TargetStateResolver targetStateResolver;
088:
089: /**
090: * Create a new transition that always matches and always executes,
091: * transitioning to the target state calculated by the provided
092: * targetStateResolver.
093: * @param targetStateResolver the resolver of the target state of this
094: * transition
095: * @see #setMatchingCriteria(TransitionCriteria)
096: * @see #setExecutionCriteria(TransitionCriteria)
097: */
098: public Transition(TargetStateResolver targetStateResolver) {
099: this (WildcardTransitionCriteria.INSTANCE, targetStateResolver);
100: }
101:
102: /**
103: * Create a new transition that matches on the specified criteria,
104: * transitioning to the target state calculated by the provided
105: * targetStateResolver.
106: * @param matchingCriteria the criteria for matching this transition
107: * @param targetStateResolver the resolver of the target state of this
108: * transition
109: * @see #setExecutionCriteria(TransitionCriteria)
110: */
111: public Transition(TransitionCriteria matchingCriteria,
112: TargetStateResolver targetStateResolver) {
113: setMatchingCriteria(matchingCriteria);
114: setTargetStateResolver(targetStateResolver);
115: }
116:
117: // implementing transition definition
118:
119: public String getId() {
120: return matchingCriteria.toString();
121: }
122:
123: public String getTargetStateId() {
124: return targetStateResolver.toString();
125: }
126:
127: /**
128: * Returns the criteria that determine whether or not this transition
129: * matches as eligible for execution.
130: * @return the transition matching criteria
131: */
132: public TransitionCriteria getMatchingCriteria() {
133: return matchingCriteria;
134: }
135:
136: /**
137: * Set the criteria that determine whether or not this transition matches as
138: * eligible for execution.
139: * @param matchingCriteria the transition matching criteria
140: */
141: public void setMatchingCriteria(TransitionCriteria matchingCriteria) {
142: Assert.notNull(matchingCriteria,
143: "The matching criteria is required");
144: this .matchingCriteria = matchingCriteria;
145: }
146:
147: /**
148: * Returns the criteria that determine whether or not this transition, once
149: * matched, should complete execution or should <i>roll back</i>.
150: * @return the transition execution criteria
151: */
152: public TransitionCriteria getExecutionCriteria() {
153: return executionCriteria;
154: }
155:
156: /**
157: * Set the criteria that determine whether or not this transition, once
158: * matched, should complete execution or should <i>roll back</i>.
159: * @param executionCriteria the transition execution criteria
160: */
161: public void setExecutionCriteria(
162: TransitionCriteria executionCriteria) {
163: Assert.notNull(executionCriteria,
164: "The execution criteria is required");
165: this .executionCriteria = executionCriteria;
166: }
167:
168: /**
169: * Returns this transition's target state resolver.
170: */
171: public TargetStateResolver getTargetStateResolver() {
172: return targetStateResolver;
173: }
174:
175: /**
176: * Set this transition's target state resolver, to calculate what state to
177: * transition to when this transition is executed.
178: * @param targetStateResolver the target state resolver
179: */
180: public void setTargetStateResolver(
181: TargetStateResolver targetStateResolver) {
182: Assert.notNull(targetStateResolver,
183: "The target state resolver is required");
184: this .targetStateResolver = targetStateResolver;
185: }
186:
187: /**
188: * Checks if this transition is elligible for execution given the state of
189: * the provided flow execution request context.
190: * @param context the flow execution request context
191: * @return true if this transition should execute, false otherwise
192: */
193: public boolean matches(RequestContext context) {
194: return matchingCriteria.test(context);
195: }
196:
197: /**
198: * Checks if this transition can complete its execution or should be rolled
199: * back, given the state of the flow execution request context.
200: * @param context the flow execution request context
201: * @return true if this transition can complete execution, false if it
202: * should roll back
203: */
204: public boolean canExecute(RequestContext context) {
205: return executionCriteria.test(context);
206: }
207:
208: /**
209: * Execute this state transition. Will only be called if the
210: * {@link #matches(RequestContext)} method returns true for given context.
211: * @param context the flow execution control context
212: * @return a view selection containing model and view information needed to
213: * render the results of the transition execution
214: * @throws FlowExecutionException when transition execution fails
215: */
216: public ViewSelection execute(State sourceState,
217: RequestControlContext context)
218: throws FlowExecutionException {
219: ViewSelection selectedView;
220: if (canExecute(context)) {
221: if (sourceState != null) {
222: if (logger.isDebugEnabled()) {
223: logger.debug("Executing " + this
224: + " out of state '" + sourceState.getId()
225: + "'");
226: }
227: if (sourceState instanceof TransitionableState) {
228: // make exit call back on transitionable state
229: ((TransitionableState) sourceState).exit(context);
230: }
231: } else {
232: if (logger.isDebugEnabled()) {
233: logger.debug("Executing " + this );
234: }
235: }
236: State targetState = targetStateResolver.resolveTargetState(
237: this , sourceState, context);
238: context.setLastTransition(this );
239: // enter the target state (note: any exceptions are propagated)
240: selectedView = targetState.enter(context);
241: } else {
242: if (sourceState != null
243: && sourceState instanceof TransitionableState) {
244: // 'roll back' and re-enter the transitionable source state
245: selectedView = ((TransitionableState) sourceState)
246: .reenter(context);
247: } else {
248: throw new IllegalStateException(
249: "Execution of '"
250: + this
251: + "' was blocked by '"
252: + getExecutionCriteria()
253: + "', "
254: + "; however, no source state is set at runtime. "
255: + "This is an illegal situation: check your flow definition.");
256: }
257: }
258: if (logger.isDebugEnabled()) {
259: if (context.getFlowExecutionContext().isActive()) {
260: logger.debug("Completed execution of " + this
261: + ", as a result the new state is '"
262: + context.getCurrentState().getId()
263: + "' in flow '"
264: + context.getActiveFlow().getId() + "'");
265: } else {
266: logger.debug("Completed execution of " + this
267: + ", as a result the flow execution has ended");
268: }
269: }
270: return selectedView;
271: }
272:
273: public String toString() {
274: return new ToStringCreator(this ).append("on",
275: getMatchingCriteria()).append("to",
276: getTargetStateResolver()).toString();
277: }
278: }
|