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.executor.struts;
017:
018: import java.util.HashMap;
019: import java.util.Map;
020:
021: import javax.servlet.http.HttpServletRequest;
022: import javax.servlet.http.HttpServletResponse;
023:
024: import org.apache.struts.action.ActionForm;
025: import org.apache.struts.action.ActionForward;
026: import org.apache.struts.action.ActionMapping;
027: import org.springframework.validation.Errors;
028: import org.springframework.web.context.WebApplicationContext;
029: import org.springframework.web.struts.ActionSupport;
030: import org.springframework.web.struts.DelegatingActionProxy;
031: import org.springframework.web.struts.SpringBindingActionForm;
032: import org.springframework.web.util.WebUtils;
033: import org.springframework.webflow.action.FormObjectAccessor;
034: import org.springframework.webflow.context.ExternalContext;
035: import org.springframework.webflow.execution.support.ApplicationView;
036: import org.springframework.webflow.execution.support.ExternalRedirect;
037: import org.springframework.webflow.execution.support.FlowDefinitionRedirect;
038: import org.springframework.webflow.execution.support.FlowExecutionRedirect;
039: import org.springframework.webflow.executor.FlowExecutor;
040: import org.springframework.webflow.executor.ResponseInstruction;
041: import org.springframework.webflow.executor.support.FlowExecutorArgumentHandler;
042: import org.springframework.webflow.executor.support.FlowRequestHandler;
043: import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler;
044: import org.springframework.webflow.executor.support.ResponseInstructionHandler;
045:
046: /**
047: * Point of integration between Struts and Spring Web Flow: a Struts Action that
048: * acts a front controller entry point into the web flow system. A single
049: * FlowAction may launch any new FlowExecution. In addition, a single Flow
050: * Action may signal events in any existing/restored FlowExecutions.
051: * <p>
052: * Requests are managed by and delegated to a {@link FlowExecutor}, which this
053: * class delegates to using a {@link FlowRequestHandler} (allowing reuse of
054: * common front flow controller logic in other environments). Consult the
055: * JavaDoc of those classes for more information on how requests are processed.
056: * <p>
057: * <li>By default, to have this controller launch a new flow execution
058: * (conversation), have the client send a
059: * {@link FlowExecutorArgumentHandler#getFlowIdArgumentName()} request
060: * parameter indicating the flow definition to launch.
061: * <li>To have this controller participate in an existing flow execution
062: * (conversation), have the client send a
063: * {@link FlowExecutorArgumentHandler#getFlowExecutionKeyArgumentName()}
064: * request parameter identifying the conversation to participate in.
065: * <p>
066: * On each request received by this action, a {@link StrutsExternalContext}
067: * object is created as input to the web flow system. This external source event
068: * provides access to the action form, action mapping, and other Struts-specific
069: * constructs.
070: * <p>
071: * This class also is aware of the {@link SpringBindingActionForm} adapter,
072: * which adapts Spring's data binding infrastructure (based on POJO binding, a
073: * standard Errors interface, and property editor type conversion) to the Struts
074: * action form model. This option gives backend web-tier developers full support
075: * for POJO-based binding with minimal hassel, while still providing consistency
076: * to view developers who already have a lot of experience with Struts for
077: * markup and request dispatching.
078: * <p>
079: * Below is an example <code>struts-config.xml</code> configuration for a
080: * FlowAction:
081: *
082: * <pre>
083: * <action path="/userRegistration"
084: * type="org.springframework.webflow.executor.struts.FlowAction"
085: * name="springBindingActionForm" scope="request">
086: * </action>
087: * </pre>
088: *
089: * This example maps the logical request URL <code>/userRegistration.do</code>
090: * as a Flow controller (<code>FlowAction</code>). It is expected that flows
091: * to launch be provided in a dynamic fashion by the views (allowing this single
092: * <code>FlowAction</code> to manage any number of flow executions). A Spring
093: * binding action form instance is set in request scope, acting as an adapter
094: * enabling POJO-based binding and validation with Spring.
095: * <p>
096: * Other notes regarding Struts/Spring Web Flow integration:
097: * <ul>
098: * <li>Logical view names returned when <code>ViewStates</code> and
099: * <code>EndStates</code> are entered are mapped to physical view templates
100: * using standard Struts action forwards (typically global forwards).</li>
101: * <li>Use of the <code>SpringBindingActionForm</code> requires no special
102: * setup in <code>struts-config.xml</code>: simply declare a form bean in
103: * request scope of the class
104: * <code>org.springframework.web.struts.SpringBindingActionForm</code> and use
105: * it with your FlowAction.</li>
106: * <li>This class depends on a {@link FlowExecutor} instance to be configured.
107: * If relying on Spring's {@link DelegatingActionProxy} (which is recommended),
108: * a FlowExecutor reference can simply be injected using standard Spring
109: * dependency injection techniques. If you are not using the proxy-based
110: * approach, this class will attempt a root context lookup on initialization,
111: * first querying for a bean of instance {@link FlowExecutor} named
112: * {@link #FLOW_EXECUTOR_BEAN_NAME}.</li>
113: * <li>The
114: * {@link org.springframework.webflow.executor.support.FlowExecutorArgumentHandler}
115: * used by the FlowAction can be configured in the root context using a bean of
116: * name {@link #FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME}. If not explicitly
117: * specified it will default to a normal
118: * {@link org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler}
119: * with standard configuration.</li>
120: * </ul>
121: * <p>
122: * The benefits here are considerable: developers now have a powerful web flow
123: * capability integrated with Struts, with a consistent-approach to POJO-based
124: * binding and validation that addresses the proliferation of
125: * <code>ActionForm</code> classes found in traditional Struts-based apps.
126: *
127: * @see org.springframework.webflow.executor.FlowExecutor
128: * @see org.springframework.webflow.executor.support.FlowRequestHandler
129: * @see org.springframework.web.struts.SpringBindingActionForm
130: * @see org.springframework.web.struts.DelegatingActionProxy
131: *
132: * @author Keith Donald
133: * @author Erwin Vervaet
134: */
135: public class FlowAction extends ActionSupport {
136:
137: /**
138: * The flow executor will be retreived from the application context using
139: * this bean name if no executor is explicitly set. ("flowExecutor")
140: */
141: protected static final String FLOW_EXECUTOR_BEAN_NAME = "flowExecutor";
142:
143: /**
144: * The flow executor argument handler will be retreived from the
145: * application context using this bean name if no argument handler is
146: * explicitly set. ("argumentHandler")
147: */
148: protected static final String FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME = "argumentHandler";
149:
150: /**
151: * The service responsible for launching and signaling Struts-originating
152: * events in flow executions.
153: */
154: private FlowExecutor flowExecutor;
155:
156: /**
157: * Delegate to handle flow executor arguments.
158: */
159: private FlowExecutorArgumentHandler argumentHandler;
160:
161: /**
162: * Returns the flow executor used by this controller.
163: * @return the flow executor
164: */
165: public FlowExecutor getFlowExecutor() {
166: return flowExecutor;
167: }
168:
169: /**
170: * Configures the flow executor implementation to use. Required.
171: * @param flowExecutor the fully configured flow executor
172: */
173: public void setFlowExecutor(FlowExecutor flowExecutor) {
174: this .flowExecutor = flowExecutor;
175: }
176:
177: /**
178: * Returns the flow executor argument handler used by this controller.
179: * @return the argument handler
180: */
181: public FlowExecutorArgumentHandler getArgumentHandler() {
182: return argumentHandler;
183: }
184:
185: /**
186: * Sets the flow executor argument handler to use.
187: * @param argumentHandler the fully configured argument handler
188: */
189: public void setArgumentHandler(
190: FlowExecutorArgumentHandler argumentHandler) {
191: this .argumentHandler = argumentHandler;
192: }
193:
194: protected void onInit() {
195: WebApplicationContext context = getWebApplicationContext();
196: if (getFlowExecutor() == null) {
197: if (context.containsBean(FLOW_EXECUTOR_BEAN_NAME)) {
198: setFlowExecutor((FlowExecutor) context.getBean(
199: FLOW_EXECUTOR_BEAN_NAME, FlowExecutor.class));
200: } else {
201: throw new IllegalStateException(
202: "No '"
203: + FLOW_EXECUTOR_BEAN_NAME
204: + "' bean definition could be found; to use Spring Web Flow with Struts you must "
205: + "configure this FlowAction with a FlowExecutor");
206: }
207: }
208: if (getArgumentHandler() == null) {
209: if (context
210: .containsBean(FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME)) {
211: setArgumentHandler((FlowExecutorArgumentHandler) context
212: .getBean(
213: FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME,
214: FlowExecutorArgumentHandler.class));
215: } else {
216: // default
217: argumentHandler = new RequestParameterFlowExecutorArgumentHandler();
218: }
219: }
220: }
221:
222: public ActionForward execute(ActionMapping mapping,
223: ActionForm form, HttpServletRequest request,
224: HttpServletResponse response) throws Exception {
225: ExternalContext context = new StrutsExternalContext(mapping,
226: form, getServletContext(), request, response);
227: ResponseInstruction responseInstruction = createRequestHandler()
228: .handleFlowRequest(context);
229: return toActionForward(responseInstruction, mapping, form,
230: request, response, context);
231: }
232:
233: /**
234: * Factory method that creates a new helper for processing a request into
235: * this flow controller.
236: * @return the controller helper
237: */
238: protected FlowRequestHandler createRequestHandler() {
239: return new FlowRequestHandler(getFlowExecutor(),
240: getArgumentHandler());
241: }
242:
243: /**
244: * Return a Struts ActionForward given a ResponseInstruction. Adds all
245: * attributes from the ResponseInstruction as request attributes.
246: */
247: protected ActionForward toActionForward(
248: final ResponseInstruction responseInstruction,
249: final ActionMapping mapping, final ActionForm form,
250: final HttpServletRequest request,
251: final HttpServletResponse response,
252: final ExternalContext context) throws Exception {
253: return (ActionForward) new ResponseInstructionHandler() {
254: protected void handleApplicationView(ApplicationView view)
255: throws Exception {
256: // forward to a view as part of an active conversation
257: Map model = new HashMap(view.getModel());
258: argumentHandler.exposeFlowExecutionContext(
259: responseInstruction.getFlowExecutionKey(),
260: responseInstruction.getFlowExecutionContext(),
261: model);
262: WebUtils.exposeRequestAttributes(request, model);
263: if (form instanceof SpringBindingActionForm) {
264: SpringBindingActionForm bindingForm = (SpringBindingActionForm) form;
265: // expose the form object and associated errors as the
266: // "current form object" in the request
267: Errors currentErrors = (Errors) model
268: .get(FormObjectAccessor
269: .getCurrentFormErrorsName());
270: bindingForm.expose(currentErrors, request);
271: }
272: setResult(findForward(view, mapping));
273: }
274:
275: protected void handleFlowDefinitionRedirect(
276: FlowDefinitionRedirect redirect) throws Exception {
277: // restart the flow by redirecting to flow launch URL
278: String flowUrl = argumentHandler
279: .createFlowDefinitionUrl(redirect, context);
280: setResult(createRedirectForward(flowUrl, response));
281: }
282:
283: protected void handleFlowExecutionRedirect(
284: FlowExecutionRedirect redirect) throws Exception {
285: // redirect to active flow execution URL
286: String flowExecutionUrl = argumentHandler
287: .createFlowExecutionUrl(responseInstruction
288: .getFlowExecutionKey(),
289: responseInstruction
290: .getFlowExecutionContext(),
291: context);
292: setResult(createRedirectForward(flowExecutionUrl,
293: response));
294: }
295:
296: protected void handleExternalRedirect(
297: ExternalRedirect redirect) throws Exception {
298: // redirect to external URL
299: String externalUrl = argumentHandler.createExternalUrl(
300: redirect, responseInstruction
301: .getFlowExecutionKey(), context);
302: setResult(createRedirectForward(externalUrl, response));
303: }
304:
305: protected void handleNull() throws Exception {
306: // no response to issue
307: setResult(null);
308: }
309: }.handle(responseInstruction).getResult();
310: }
311:
312: /**
313: * Handles a redirect. This implementation simply calls sendRedirect on the
314: * response object.
315: * @param url the url to redirect to
316: * @param response the http response
317: * @return the redirect forward, this implementation returns null
318: * @throws Exception an excpetion occured processing the redirect
319: * @see HttpServletResponse#sendRedirect(java.lang.String)
320: */
321: protected ActionForward createRedirectForward(String url,
322: HttpServletResponse response) throws Exception {
323: response.sendRedirect(url);
324: return null;
325: }
326:
327: /**
328: * Find an action forward for given application view. If no suitable forward
329: * is found in the action mapping using the view name as a key, this method
330: * will create a new action forward using the view name.
331: * @param forward the application view to find a forward for
332: * @param mapping the action mapping to use
333: * @return the action forward, never null
334: */
335: protected ActionForward findForward(ApplicationView forward,
336: ActionMapping mapping) {
337: // note that this method is always creating a new ActionForward to make
338: // sure that the redirect flag is false -- redirect is controlled by SWF
339: // itself, not Struts
340: ActionForward actionForward = mapping.findForward(forward
341: .getViewName());
342: if (actionForward != null) {
343: // the 1.2.1 copy constructor would ideally be better to
344: // use, but it is not Struts 1.1 compatible
345: actionForward = new ActionForward(actionForward.getName(),
346: actionForward.getPath(), false);
347: } else {
348: actionForward = new ActionForward(forward.getViewName(),
349: false);
350: }
351: return actionForward;
352: }
353: }
|