1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.myfaces.orchestra.flow;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.Map;
26
27 import javax.faces.FacesException;
28 import javax.faces.application.ViewHandler;
29 import javax.faces.component.UIViewRoot;
30 import javax.faces.context.ExternalContext;
31 import javax.faces.context.FacesContext;
32 import javax.faces.event.PhaseId;
33 import javax.servlet.http.HttpServletResponse;
34
35 import org.apache.commons.lang.StringUtils;
36 import org.apache.commons.logging.Log;
37 import org.apache.commons.logging.LogFactory;
38 import org.apache.myfaces.orchestra.conversation.ConversationContext;
39 import org.apache.myfaces.orchestra.conversation.ConversationManager;
40 import org.apache.myfaces.orchestra.flow.components.ClearOnCommit;
41 import org.apache.myfaces.orchestra.flow.components.ModalFlow;
42 import org.apache.myfaces.orchestra.flow.config.FlowAccept;
43 import org.apache.myfaces.orchestra.flow.config.FlowCall;
44 import org.apache.myfaces.orchestra.flow.config.FlowConfig;
45 import org.apache.myfaces.orchestra.flow.config.FlowOnCommit;
46 import org.apache.myfaces.orchestra.flow.config.FlowParamAccept;
47 import org.apache.myfaces.orchestra.flow.config.FlowParamSend;
48 import org.apache.myfaces.orchestra.flow.config.FlowReturnAccept;
49 import org.apache.myfaces.orchestra.flow.config.FlowReturnSend;
50 import org.apache.myfaces.orchestra.flow.digest.FlowDigester;
51 import org.apache.myfaces.orchestra.lib.OrchestraException;
52 import org.xml.sax.InputSource;
53
54 /**
55 * Common logic for managing orchestra flows, called from both FlowNavigationHandler
56 * and FlowViewHandler.
57 */
58 public class FlowHandler
59 {
60 private static final Log log = LogFactory.getLog(FlowHandler.class);
61 private static final String VIEW_TO_RESTORE_KEY = FlowHandler.class.getName() + ":viewToRestore";
62
63 /**
64 * Return the FlowCall object associated with the current conversation
65 * context (if any).
66 * <p>
67 * There will be one if the current context was created by some page
68 * doing a "call" to a flow.
69 * <p>
70 * Returns null if there is no FlowCall object in the current conversation
71 * context.
72 */
73 static FlowCall getFlowCall(FacesContext facesContext, UIViewRoot viewRoot, String outcome)
74 {
75 if (viewRoot == null)
76 {
77 return null;
78 }
79
80 String viewId = viewRoot.getViewId();
81 FlowConfig config = getFlowConfig(facesContext, viewId);
82 if (config == null)
83 {
84 log.debug("No flowcall for " + viewId);
85 return null;
86 }
87
88 FlowCall fc = config.getFlowCall(outcome);
89 log.debug("Flowcall for " + viewRoot.getViewId() + " outcome: " + outcome + " is " + String.valueOf(fc));
90 return fc;
91 }
92
93 /**
94 * Return the FlowAccept object associated with the current conversation
95 * context (if any).
96 * <p>
97 * There will be one if the current context was created by some page
98 * doing a "call" to a flow, AND the flow call initialisation has
99 * completed.
100 */
101 static FlowAccept getFlowAccept(FacesContext facesContext, FlowInfo flowInfo, String viewId)
102 {
103 if ((flowInfo != null) && (flowInfo.getFlowAccept() != null))
104 {
105 // ok, we are already within a configured flow.
106 return flowInfo.getFlowAccept();
107 }
108
109 FlowConfig config = getFlowConfig(facesContext, viewId);
110 if (config == null)
111 {
112 if (log.isDebugEnabled())
113 {
114 log.debug("No flowaccept for " + viewId);
115 }
116 return null;
117 }
118
119 FlowAccept fa = config.getFlowAccept();
120 if (log.isDebugEnabled())
121 {
122 log.debug("FlowAccept for " + viewId + " is " + String.valueOf(fa));
123 }
124 return fa;
125 }
126
127
128 /**
129 * Tell the browser to fetch the specified viewId.
130 * <p>
131 * For a non-modal flow, this simply sends an http redirect to the appropriate url.
132 * <p>
133 * For a modal flow, we render some javascript that closes the current modal popup window
134 * and tells the parent window to load the appropriate url. Sending a redirect won't work
135 * as that will load the view into the modal window/frame instead.
136 */
137 private static void redirectTo(FacesContext facesContext, String viewId, FlowInfo flowInfo)
138 {
139 ExternalContext ec = facesContext.getExternalContext();
140 ViewHandler viewHandler = facesContext.getApplication().getViewHandler();
141 String url = viewHandler.getActionURL(facesContext, viewId);
142 url = ec.encodeActionURL(url);
143
144 if (!flowInfo.isModalFlow())
145 {
146 try
147 {
148 ec.redirect(url);
149 return;
150 }
151 catch(IOException ioe)
152 {
153 throw new FacesException("Unable to redirect to " + viewId, ioe);
154 }
155 }
156
157 // Ok, we are closing a modal flow. We therefore need to render some javascript that
158 // will close the modal window and force the parent window to fetch the appropriate url.
159 //
160 // Note that we cannot allow navigation to occur then send the redirect in the createView
161 // method because we would need a valid outcome to return here; without that the NavigationHandler
162 // just does a "null" navigation, ie returns the current UIViewRoot and never calls the ViewHandler
163 // object.
164 //
165 // And we cannot re-render the popup window because that would need the child context to be
166 // active in order to work in all cases (though re-rendering the popup window after the postback
167 // that closes it might itself cause problems eg reuse of committed persistent objects). But
168 // if we need the child context in order to render the child view, then when do we destroy it?
169 //
170 // So here we do a truly ugly hack: send some raw html to the response stream right here.
171 // The major problem here is that this assumes an HTML renderkit. Well, actually the below
172 // is also valid XHTML too..
173 //
174 // TODO: some modal dialog libraries (eg myfaces sandbox s:modalDialog) provide a "cancel button"
175 // that closes the modal dialog. If this is done then the webapp will work but the child context
176 // will not get deleted. Eventually the conversationContext-timeout will remove it but that could
177 // be 30 minutes or more. One option could be to always delete all child contexts whenever a
178 // request for a context is received. That doesn't work when multiple windows have the same
179 // conversationContext id, but that is broken for other reasons anyway (eg access-scope).
180
181 StringBuffer buf = new StringBuffer();
182 buf.append("<html><head><script>//<![CDATA[\n");
183 buf.append("var url='"+ url + "';\n");
184 buf.append(flowInfo.getOnExitScript());
185 buf.append("\n//]]>\n");
186 buf.append("</script></head></html>");
187 try
188 {
189 // Here we cannot use facesContext.getResponseWriter(). That returns null
190 // until after ViewHandler.renderView has been invoked, but here we expect to
191 // be in the "event broadcast" part of either APPLY_REQUEST_VALUES (for immediate
192 // command components) or INVOKE_APPLICATION. So here we simply use the raw
193 // servlet response stream. Hey, all this html-generation is so hacky it seems no
194 // worse to access the response stream here.
195 //
196 // Of course this won't work with portlets, but the idea of a portlet popping
197 // up a modal window to invoke a flow is rather unlikely anyway..
198 HttpServletResponse rsp = (HttpServletResponse) ec.getResponse();
199 rsp.getWriter().write(buf.toString());
200 }
201 catch(IOException ioe)
202 {
203 throw new FacesException(ioe);
204 }
205 facesContext.responseComplete();
206 }
207
208 private static void restoreViewRoot(FacesContext facesContext, String viewId, UIViewRoot viewRoot)
209 {
210 if (viewRoot == null)
211 {
212 viewRoot = createDummyViewRoot(facesContext, viewId);
213 }
214 facesContext.setViewRoot(viewRoot);
215 }
216
217 private static UIViewRoot createDummyViewRoot(FacesContext facesContext, String viewId)
218 {
219 // Create dummy viewRoot with the right viewId so that navigation
220 // uses the desired from-view-id during navigation case lookup.
221 //
222 // Unfortunately when the navigation rule matched is not a forward,
223 // then ViewHandler.createView is executed and that expects to be able to
224 // copy properties locale and renderKitId from the current view root into
225 // the new view root. So we need to ensure that those properties are also
226 // correctly set up on our dummy view root.
227
228 java.util.Locale locale;
229 String renderKitId;
230
231 if (facesContext.getViewRoot() != null)
232 {
233 UIViewRoot currViewRoot = facesContext.getViewRoot();
234 locale = currViewRoot.getLocale();
235 renderKitId = currViewRoot.getRenderKitId();
236 }
237 else
238 {
239 ViewHandler vh = facesContext.getApplication().getViewHandler();
240 locale = vh.calculateLocale(facesContext);
241 renderKitId = vh.calculateRenderKitId(facesContext);
242 }
243
244 UIViewRoot dummyViewRoot = new UIViewRoot();
245 dummyViewRoot.setViewId(viewId);
246 dummyViewRoot.setLocale(locale);
247 dummyViewRoot.setRenderKitId(renderKitId);
248
249 return dummyViewRoot;
250 }
251
252 private static String handleCancelFlow(FacesContext facesContext, FlowInfo flowInfo, String outcome)
253 {
254 String callerViewId = flowInfo.getCallerViewId();
255 UIViewRoot callerViewRoot = flowInfo.getCallerViewRoot();
256
257 ConversationManager cm = ConversationManager.getInstance(true);
258 ConversationContext ctx = cm.getCurrentConversationContext();
259 ConversationContext parent = ctx.getParent();
260 cm.activateConversationContext(parent);
261 cm.removeAndInvalidateConversationContext(ctx);
262
263 // Mark flowInfo as cancelled
264 flowInfo.cancel();
265
266 // Store viewRoot from flowInfo into parent context. Note that this might be null;
267 // in this case the user of this property will simply ignore it, and create a new
268 // view tree using the viewId in the incoming request url.
269 parent.setAttribute(VIEW_TO_RESTORE_KEY, callerViewRoot);
270
271 // And redirect. This sets responseComplete, so navigation is skipped.
272 redirectTo(facesContext, callerViewId, flowInfo);
273 return outcome;
274 }
275
276 private static String handleCommitFlow(FacesContext facesContext, FlowInfo flowInfo, String outcome)
277 {
278 String callerViewId = flowInfo.getCallerViewId();
279 UIViewRoot callerViewRoot = flowInfo.getCallerViewRoot();
280
281 ConversationManager cm = ConversationManager.getInstance(true);
282 ConversationContext ctx = cm.getCurrentConversationContext();
283 ConversationContext parent = ctx.getParent();
284
285 // With the child conversationContext currently active, read
286 // all the output parameters
287 Map<String, Object> map = flowInfo.getFlowAccept().readReturnParams(facesContext);
288
289 // Mark flowInfo as committed.
290 flowInfo.commit(map);
291
292 // Activate the parent and discard the child context
293 cm.activateConversationContext(parent);
294 cm.removeAndInvalidateConversationContext(ctx);
295
296 // Now restore the view tree of the caller.
297 //
298 // WARNING: this approach means that any "binding" attributes on the components
299 // being restored are not run. If a binding to a request-scope attribute exists,
300 // then it will be null during the following render phase :-(
301 restoreViewRoot(facesContext, callerViewId, callerViewRoot);
302
303 if (callerViewRoot != null)
304 {
305 // Clear any input components in the caller view which reference values that
306 // are return values.
307 ClearOnCommit.executeAllInstances(callerViewRoot, flowInfo.getCallerOutcome());
308 }
309
310 // And with the original context active, write parameters back then
311 // invoke any custom actions the caller wants to run.
312 //
313 // Note that the setters invoked when copying data back into the caller
314 // flow should *not* access any bindings. When the command component that
315 // originally triggered the flowcall was not immediate, then the view is
316 // just a UIViewRoot with none of the expected child components in it. When
317 // the original command component was immediate, the whole view tree is here
318 // but no bindings have been restored because we did not deserialize the tree.
319 // Of course the backing beans should *not* have kept any binding references
320 // for longer than the current request; that's bad practice for a lot of reasons.
321 // Therefore all those bindings will be null. This shouldn't be a problem;
322 // property setter methods should not need to access bindings.
323 //
324 // Possibly with JSF1.2 we could run the "build component tree" part of the
325 // render cycle, then invoke UIViewRoot.restoreState. There is no possibility of
326 // doing something similar for JSF1.1 though, and it shouldn't be necessary
327 // anyway.
328 //
329 // Note that the ViewController methods are not invoked even though
330 // the backing bean for the view is. This is not a problem really, just
331 // something to be aware of.
332 FlowCall flowCall = flowInfo.getFlowCall();
333 flowCall.writeAcceptParams(facesContext, map);
334 FlowOnCommit onCommit = flowCall.getOnCommit();
335 outcome = null;
336 if (onCommit != null)
337 {
338 outcome = (String) onCommit.execute(facesContext);
339 }
340 if (outcome == null)
341 {
342 // Store viewRoot from flowInfo into parent context
343 parent.setAttribute(VIEW_TO_RESTORE_KEY, callerViewRoot);
344 redirectTo(facesContext, callerViewId, flowInfo);
345 }
346 else
347 {
348 // Compute the viewId that this outcome maps to, then redirect to it. Note that
349 // we have restored the caller's view so navigation rules for the caller's view
350 // are used.
351 //
352 // Tricky: any attempt to compute the new viewId will trigger createView. We therefore
353 // need to set a flag telling the ViewHandler to just do a redirect in createView. Ugly,
354 // and might have problems when other custom ViewHandlers are also configured, but JSF
355 // gives us no option here.
356 facesContext.getExternalContext().getRequestMap().put("isRedirectOnReturn", Boolean.TRUE);
357 }
358
359 return outcome;
360 }
361
362 /**
363 * Do actions that do not depend on properties of the called flow (if any).
364 * <p>
365 * Without knowing anything about the target of the navigation (other than
366 * its viewId), the following can still be done:
367 * <ul>
368 * <li>commit or cancel the current flow if the nav outcome matches</li>
369 * <li>determine whether this navigation triggers a new flow. If so, create
370 * a child context and place a partially-initialised FlowInfo object in it.</li>
371 * </ul>
372 * <p>
373 * This is expected to be called from the FlowNavigationHandler after some
374 * postback has caused a navigation to occur. Note that in that case we do
375 * not yet know what viewId we are going <i>to</i>.
376 *
377 * @return a navigation outcome string to pass to the NavigationHandler. Note
378 * however that if this method has sent a redirect then the return value will
379 * simply be ignored.
380 */
381 static String processPreNav(FacesContext facesContext, String outcome)
382 {
383 String oldViewId = facesContext.getViewRoot().getViewId();
384 log.debug("processCall: [" + String.valueOf(oldViewId) + "] outcome: " + outcome);
385
386 if (outcome == null)
387 {
388 // not calling or returning from a flow; just ignore.
389 return outcome;
390 }
391
392 // Handle COMMIT and CANCEL
393 FlowInfo flowInfo = getFlowInfo();
394 if (flowInfo != null)
395 {
396 // we are in a flow - are we leaving it?
397 FlowAccept fa = flowInfo.getFlowAccept();
398
399 if (outcome.equals(fa.getCancelWhen()))
400 {
401 return handleCancelFlow(facesContext, flowInfo, outcome);
402 }
403
404 if (outcome.equals(fa.getCommitWhen()))
405 {
406 return handleCommitFlow(facesContext, flowInfo, outcome);
407 }
408
409 if (outcome.equals(fa.getRestartWhen()))
410 {
411 // restart current flow
412 throw new FacesException("flow restart not yet implemented");
413 }
414 }
415
416 // OK, we know we are not committing or canceling a flow. Are we trying to
417 // enter one?
418 UIViewRoot oldViewRoot = facesContext.getViewRoot();
419 FlowCall flowCall = getFlowCall(facesContext, oldViewRoot, outcome);
420 if (flowCall != null)
421 {
422 // ugly hack: remove any FlowConfig object that the caller may have placed in the
423 // request scope. If we don't do that, then when looking for the FlowConfig for
424 // the called page we find this instead of loading the -flow.xml file for the
425 // caller. There may be a nicer alternative to this, but it will do for now.
426 facesContext.getExternalContext().getRequestMap().remove(FlowConfig.CONFIG_KEY);
427
428 // fetch parameters from caller into a map
429 Map<String,Object> data = flowCall.readSendParams(facesContext);
430
431 // Only when the triggering commandButton is immediate should we save the viewRoot.
432 if (PhaseId.APPLY_REQUEST_VALUES == FlowPhaseTracker.getCurrentPhase())
433 {
434 // yep, this was triggered by an immediate component
435 //
436 // Ideally we would serialize the state here, and just cache the serialized object.
437 // That would save memory, flush transient properties on restore, work better with
438 // distributed html sessions and just generally be tidier.
439 //
440 // However the StateManager api in JSF1.1 and JSF1.2 (and maybe later) is brain-dead;
441 // it provides a public API for saving the state but the public api for restoring it
442 // doesn't allow the old saved-state to be passed in! The only use for the saved state
443 // is to pass it to StateManager.writeState; the public API for restoring state instead
444 // uses implementation-specific code to extract the state to restore directly from either
445 // the request or the session. Sigh. Curse. So instead, we always cache a full
446 // UIViewRoot in the FlowInfo object.
447 //
448 // Note that when the view is "restored" by simply resetting the viewroot reference,
449 // component bindings are not invoked. But that is ok as
450 }
451
452 // Build a FlowInfo object for the called flow to use.
453 flowInfo = new FlowInfo(outcome, oldViewId, oldViewRoot, flowCall, data);
454
455 // create child context and activate it
456 ConversationManager cm = ConversationManager.getInstance(true);
457 ConversationContext parent = cm.getCurrentConversationContext();
458 ConversationContext child = cm.createConversationContext(parent);
459 cm.activateConversationContext(child);
460
461 // Handle triggering of modalflow if any
462 ModalFlow f = ModalFlow.getForOutcome(facesContext, outcome);
463 if (f != null)
464 {
465 // The current view contains a ModalFlow component that declares that it will handle
466 // the new flow by opening the entry page in a new frame or window. We mark that component
467 // as "active" so that the component renders javascript to open that window. We also mark
468 // the FlowInfo as modal; this is checked in FlowViewHandler.createView.
469 flowInfo.setModalFlow(true);
470 flowInfo.setOnExitScript(f.getOnExit());
471 f.setActive(true);
472 }
473
474 // Store the flowInfo in the child context
475 child.setAttribute("flowInfo", flowInfo);
476
477 String newViewId = flowCall.getViewId();
478 if (newViewId != null)
479 {
480 // The flowcall has directly specified a view to render.
481 ViewHolder viewHolder = new ViewHolder();
482 ViewHandler viewHandler = facesContext.getApplication().getViewHandler();
483 boolean isNewFlow = doNewFlowEntry(facesContext, viewHandler, viewHolder, flowInfo, newViewId);
484 if (isNewFlow == false)
485 {
486 throw new OrchestraException("viewId specified in flowCall is not a flow entry point");
487 }
488
489 if (viewHolder.root == null)
490 {
491 redirectTo(facesContext, newViewId, flowInfo);
492 }
493 else
494 {
495 // this is a modal flow; we need to re-render the current view, ie do a
496 // null navigation.
497 return null;
498 }
499 }
500 else
501 {
502 // And let normal navigation occur. Hopefully the new page will be the entry-point
503 // for a flow, in which case we finish setting up the FlowInfo object then redirect
504 // to the flow entry point; see FlowViewHander.createView. If something goes wrong,
505 // then FlowViewHandler.createView will clean up.
506 }
507 }
508
509 return outcome;
510 }
511
512 /**
513 * Handle case where the user is in a flow, then navigates somehow to a page that is not
514 * within the flow.
515 */
516 private static boolean isAbnormalFlowExit(FlowInfo flowInfo, String newViewId)
517 {
518 // Are we leaving a flow in an abnormal manner?
519 if (flowInfo != null)
520 {
521 String flowPath = getFlowPath(newViewId);
522 if (!flowPath.startsWith(flowInfo.getFlowPath()))
523 {
524 // User must have navigated away from the current flow. Walk up the tree invalidating
525 // each context until we find a flow that does match the new path, or we find the root
526 // context (which never has a FlowInfo in it).
527 ConversationManager cm = ConversationManager.getInstance(true);
528 ConversationContext ctx = cm.getCurrentConversationContext();
529 ConversationContext parent = ctx.getParent();
530 for(;;)
531 {
532 if (parent == null)
533 {
534 // After discarding all invalid flows, let normal navigation occur
535 // to the new viewId. There is no view to "restore"..
536 break;
537 }
538
539 cm.activateConversationContext(parent);
540 cm.removeAndInvalidateConversationContext(ctx);
541 ctx = parent;
542 parent = ctx.getParent();
543
544 FlowInfo fi = getFlowInfo(ctx);
545 if ((fi != null) && (flowPath.startsWith(fi.getFlowPath())))
546 {
547 // ok, the view the user wants to navigate to is within
548 // the context just activated, so we now have things set
549 // up as required; break out of loop.
550 break;
551 }
552 }
553
554 return true;
555 }
556 }
557
558 // nope, this is just a normal navigation
559 return false;
560 }
561
562 private static class ViewHolder
563 {
564 UIViewRoot root;
565 }
566
567 /**
568 * Handle case where we have just navigated to a new page, and the page that caused the
569 * navigation did a flow-call.
570 * <p>
571 * When this is true, a child context will already have been created, and a half-initialised
572 * FlowInfo object will be in the context. We need to now fetch the called flow's specs,
573 * check that the caller and called flows match in type and parameters, then finish
574 * initialising the FlowInfo and import the passed params into the child context.
575 * <p>
576 * This is expected to be called from FlowViewHandler when a new view is being created
577 * (due either to a GET or POST from the user, or an internal forward due to navigation.
578 */
579 static boolean doNewFlowEntry(FacesContext facesContext, ViewHandler viewHandler,
580 ViewHolder viewHolder, FlowInfo flowInfo, String newViewId)
581 {
582 if (flowInfo == null)
583 {
584 // No, the processCall method has not created a new partially-initialised
585 // FlowInfo object.
586 return false;
587 }
588
589 if (flowInfo.getFlowAccept() != null)
590 {
591 // No, the processCall method has not created a new partially-initialised
592 // FlowInfo object.
593 return false;
594 }
595
596 FlowAccept flowAccept = getFlowAccept(facesContext, flowInfo, newViewId);
597 if (flowAccept == null)
598 {
599 // TODO: discard all flow stacks here for safety?
600 StringBuffer msg = new StringBuffer();
601 msg.append("Invocation of flow without callee declaration. ");
602 msg.append("Calling view is " + flowInfo.getCallerViewId() + ". ");
603 msg.append("Called view is " + newViewId + ".");
604 if (log.isDebugEnabled())
605 {
606 log.debug("isNewFlowEntry: Error: " + msg.toString());
607 }
608 throw new OrchestraException(msg.toString());
609 }
610
611 if (log.isDebugEnabled())
612 {
613 log.debug("isNewFlowEntry: new flow detected.");
614 }
615 // validate flowCall against flowAccept. TODO: discard flow stacks on error?
616 validateCall(flowInfo.getFlowCall(), flowAccept);
617
618 // finish initialising the new flowInfo
619 String flowPath = getFlowPath(newViewId);
620 flowInfo.setAcceptInfo(newViewId, flowPath, flowAccept);
621
622 // push parameters from caller to callee
623 Map<String,Object> argsIn = flowInfo.getArgsIn();
624 flowAccept.writeAcceptParams(facesContext, argsIn);
625
626 ExternalContext externalContext = facesContext.getExternalContext();
627
628 // The "postback" part of the current request must have started a flow call.
629 if (flowInfo.isModalFlow())
630 {
631 // First compute the URL for the *new* viewId (the flow entry page) while the child
632 // context is active so the conversationContext query param gets set right.
633 String newViewUrl = viewHandler.getActionURL(facesContext, newViewId);
634 newViewUrl = externalContext.encodeActionURL(newViewUrl);
635
636 // Verify that this really is the entry point for a new flow.
637 // Do import On error, reset
638 // context to parent then throw exception.
639 // And copy input parameters for the
640 // Now we want to re-render the *old* view, so we need to activate the parent
641 // context for the remainder of this request. Note that the ConversationManager
642 // must exist as we have a FlowInfo object.
643 ConversationManager cm = ConversationManager.getInstance();
644 ConversationContext child = cm.getCurrentConversationContext();
645 ConversationContext parent = child.getParent();
646 cm.activateConversationContext(parent);
647
648 // Set some variables that allows components in the page to
649 // access the URL that the new modal window should fetch.
650 setNewViewInfo(newViewId, newViewUrl);
651
652 // And just return the current view rather than creating a new one.
653 viewHolder.root = facesContext.getViewRoot();
654 return true;
655 }
656
657 // Force a redirect to the entry page for the flow. Note that the currently activated context
658 // is the child one, so getActionURL encodes the conversationContext value appropriately.
659 String newViewUrl = viewHandler.getActionURL(facesContext, newViewId);
660 newViewUrl = externalContext.encodeActionURL(newViewUrl);
661 try
662 {
663 facesContext.getExternalContext().redirect(newViewUrl);
664 return true;
665 }
666 catch(IOException ioe)
667 {
668 throw new FacesException("Unable to enter flow", ioe);
669 }
670
671 }
672
673 private static Object rmvContextAttribute(String id)
674 {
675 ConversationManager cm = ConversationManager.getInstance(false);
676 if (cm == null)
677 {
678 return null;
679 }
680
681 ConversationContext ctx = cm.getCurrentConversationContext();
682 if (ctx == null)
683 {
684 return null;
685 }
686
687 Object o = ctx.removeAttribute(id);
688 return o;
689 }
690
691 private static void setNewViewInfo(String viewId, String viewUrl)
692 {
693 FacesContext fc = FacesContext.getCurrentInstance();
694 Map<String, Object> reqMap = fc.getExternalContext().getRequestMap();
695 reqMap.put("orchestraFlowId", viewId);
696 reqMap.put("orchestraFlowUrl", viewUrl);
697 }
698
699 /**
700 * Special createView handling for Orchestra Flows.
701 * <p>
702 * When this request was a postback that started a flowcall:
703 * <ul>
704 * <li>validate that this new view is a suitable flow entry point
705 * <li>import passed parameters into child context
706 * <li>for non-modal call: send a redirect to this view
707 * <li>for modal call: rerender the *old* view, which should then
708 * trigger a GET to the new view in a new window or frame.
709 * </ul>
710 * <p>
711 * When this request was a postback that started a flowreturn:
712 * <ul>
713 * <li>Normally the view to return to is known so this code is not executed.
714 * <li>When the view to return to has a "return handler" that provides
715 * a nav-outcome then this code is executed; just redirect to the desired view.
716 * </ul>
717 * <p>
718 * Throws OrchestraException if there is a flow error, eg if the view
719 * specified is the entry point for a flow but the previous view did
720 * not do a FlowCall.
721 *
722 * @return a UIViewRoot to use when rendering this request. If null is returned
723 * then the standard ViewHandler.createView method is used to create one. Note
724 * however that if this method has sent a redirect then the return value will
725 * simply be ignored.
726 */
727 static UIViewRoot processPreCreateView(FacesContext facesContext, ViewHandler viewHandler, String newViewId)
728 {
729 log.debug("processAccept: [" + newViewId + "]");
730
731 FlowInfo flowInfo = getFlowInfo();
732
733 boolean isRedirectOnReturn = facesContext.getExternalContext().getRequestMap()
734 .containsKey("redirectOnReturn");
735 if (isRedirectOnReturn)
736 {
737 // The current request contained a postback that triggered a flow-return. The caller also had a
738 // return-handler that returned an outcome to navigate to, ie the caller wants an immediate
739 // bounce to somewhere else rather than redisplaying itself. We couldn't compute the url to redirect
740 // to earlier due to the brain-dead NavigationHandler api, so have to handle it here.
741 //
742 // Force a redirect to the entry page for the flow. Note that the currently activated context
743 // is the child one, so getActionURL encodes the conversationContext value appropriately.
744 ExternalContext ec = facesContext.getExternalContext();
745 String newViewUrl = viewHandler.getActionURL(facesContext, newViewId);
746 newViewUrl = ec.encodeActionURL(newViewUrl);
747 try
748 {
749 ec.redirect(newViewUrl);
750 return null;
751 }
752 catch(IOException ioe)
753 {
754 throw new FacesException("Unable to enter flow", ioe);
755 }
756 }
757
758 UIViewRoot viewToRestore = (UIViewRoot) rmvContextAttribute(VIEW_TO_RESTORE_KEY);
759 if (viewToRestore != null)
760 {
761 // The previous request triggered a flowreturn, ie this request is re-rendering the calling view.
762 // We therefore may have a cached viewRoot object that holds the view saved when the call was made..
763 //
764 // TODO: possibly check that viewToRestore.viewId == newViewId. This should always be true, as we
765 // only set that attribute immediately before sending a redirect to the matching id.
766 return viewToRestore;
767 }
768
769 ViewHolder viewHolder = new ViewHolder();
770 if (doNewFlowEntry(facesContext, viewHandler, viewHolder, flowInfo, newViewId))
771 {
772 // We have just entered a new flow
773 return viewHolder.root;
774 }
775
776 if (isAbnormalFlowExit(flowInfo, newViewId))
777 {
778 // User has leapt out of an existing flow
779 return null;
780 }
781
782 // Below here are just sanity checks, looking for weird situations..
783
784 FlowAccept flowAccept = getFlowAccept(facesContext, flowInfo, newViewId);
785 boolean isFlowEntryPoint = (flowAccept != null);
786 boolean isInFlow = (flowInfo != null);
787
788 if (isFlowEntryPoint && !isInFlow)
789 {
790 // The new page is a flow entry point, but the earlier page did a normal navigation,
791 // without any flow-call. Therefore we do not have a child context set up, and no
792 // FlowInfo object. Unfortunately we therefore do not know what the previous
793 // page was...
794 StringBuffer msg = new StringBuffer();
795 msg.append("Invocation of flow without caller declaration. ");
796 msg.append("Called view is " + newViewId + ".");
797 log.debug("processAccept: Error: " + msg.toString());
798 throw new OrchestraException(msg.toString());
799 }
800
801
802 if (isInFlow)
803 {
804 // We can safely fetch the current conversation context this without checking for
805 // nulls; flowInfo is non-null so they must exist.
806 ConversationManager cm = ConversationManager.getInstance();
807 ConversationContext context = cm.getCurrentConversationContext();
808
809 // Sanity check: verify that the current context has no children. If it
810 // does, then presumably someone has used a back button to go from a
811 // nested flow to a parent flow. Possibly we could handle this by just
812 // discarding the children, but for now report an error.
813 //
814 // Note that the case of the user simply clicking a link that goes outside
815 // the current flow is handled in isAbnormalFlowExit; this is just for cases
816 // where the viewId and the contextId have changed together which should only
817 // happen for back-button or maybe bookmarks.
818 //
819 // Hmm..should we do this for any context, not just when isInFlow is true?
820 // Or should the orchestra core check for this?
821 if (context.hasChildren())
822 {
823 throw new OrchestraException("Child flow not properly terminated.");
824 }
825 }
826
827 // all ok, let view be created in normal manner
828 return null;
829 }
830
831 /**
832 * Return the FlowConfig object associated with the specified viewId, or null if
833 * none exists.
834 * <p>
835 * The current implementation just looks for a "-flow.xml" file relative to the
836 * specified view.
837 */
838 private static FlowConfig getFlowConfig(FacesContext facesContext, String viewId)
839 {
840 log.debug("getflowConfig for [" + viewId + "]");
841 if (viewId == null)
842 {
843 // This can happen when someone has a navigation-case that specifies
844 // an outcome but no to-view-id.
845 return null;
846 }
847
848 // first, see if a FlowConfig is cached in the request
849 ExternalContext externalContext = facesContext.getExternalContext();
850 FlowConfig cfg = (FlowConfig) externalContext.getRequestMap().get(FlowConfig.CONFIG_KEY);
851 if (cfg != null)
852 {
853 log.debug("Found flowConfig cached in viewRoot");
854 return cfg;
855 }
856
857 int lastDot = viewId.lastIndexOf(".");
858 if (lastDot == -1)
859 {
860 return null;
861 }
862
863
864 String path = viewId.substring(0, lastDot) + "-flow.xml";
865 InputStream is = externalContext.getResourceAsStream(path);
866 if (is == null)
867 {
868 log.debug("getFlowConfig: not found at " + path);
869 return null;
870 }
871 try
872 {
873 InputSource source = new InputSource(is);
874 source.setSystemId(path);
875 cfg = FlowDigester.digest(source);
876 log.debug("getFlowConfig: found at " + path);
877 return cfg;
878 }
879 finally
880 {
881 try
882 {
883 is.close();
884 }
885 catch(Exception e)
886 {
887 // ignore
888 }
889 }
890 }
891
892 /**
893 * Return the FlowInfo object stored in the current conversation
894 * context (if any).
895 */
896 static FlowInfo getFlowInfo()
897 {
898 ConversationManager cm = ConversationManager.getInstance(false);
899 if (cm == null)
900 {
901 return null;
902 }
903
904 ConversationContext context = cm.getCurrentConversationContext();
905 if (context == null)
906 {
907 return null;
908 }
909
910 return getFlowInfo(context);
911 }
912
913 /**
914 * Return the FlowInfo object associated with the specified conversation
915 * context (if any).
916 */
917 private static FlowInfo getFlowInfo(ConversationContext context)
918 {
919 return (FlowInfo) context.getAttribute("flowInfo");
920 }
921
922 /**
923 * Assuming that the specified viewId is the entry page for a flow, return a
924 * viewId prefix that will match all views in the same flow.
925 * <p>
926 * This implementation assumes that a flow always lives in its own directory.
927 */
928 private static String getFlowPath(String viewId)
929 {
930 // just return everything up to (and including) the final slash
931 int idx = viewId.lastIndexOf('/');
932 if (idx <= 0)
933 {
934 // this cannot possibly be a flow
935 return null;
936 }
937 return viewId.substring(0, idx +1);
938 }
939
940 /**
941 * Ensure that the specified FlowCall object is compatible with this FlowAccept.
942 * <p>
943 * The parameters sent by the caller should match the params accepted by the called
944 * flow. On return, the parameters sent by the called flow should match the params
945 * accepted by the caller.
946 * <p>
947 * This method needs to be consistent with FlowAccept.writeAcceptParams method, but
948 * should report better error messages. Possibly the writeAcceptParams method's error
949 * messages could just be improved. However it is useful to be able to perform this
950 * check earlier than before actually processing the return result.
951 * <p>
952 * This method also needs to be consistent with FlowCall.writeAcceptParams method.
953 */
954 static private void validateCall(FlowCall flowCall, FlowAccept flowAccept)
955 {
956 // check type matches
957 if (!flowCall.getService().equals(flowAccept.getService()))
958 {
959 throw new OrchestraException(
960 "FlowCall service [" + flowCall.getService()
961 + "] does not match FlowAccept service [" + flowAccept.getService() + "]");
962 }
963
964 List<String> errors = new ArrayList<String>();
965
966 // check input params
967 List<String> callParamNames = new ArrayList<String>();
968 for(FlowParamSend p : flowCall.getParams())
969 {
970 callParamNames.add(p.getName());
971 }
972
973 for(FlowParamAccept p : flowAccept.getParams())
974 {
975 String name = p.getName();
976 if (!callParamNames.remove(name) && (p.getDflt() == null))
977 {
978 // Accept param does not have call param equivalent, and there is no default parameter value
979 // defined. In other words, the called flow expects an input parameter but we have nothing that
980 // we can pass it.
981 errors.add(name);
982 }
983 }
984 // Add any leftover call params that do not have accept param equivalents, ie where the caller is
985 // trying to pass a parameter but the called flow is not expecting it.
986 errors.addAll(callParamNames);
987
988 if (!errors.isEmpty())
989 {
990 throw new OrchestraException("Parameter names mismatch:" + StringUtils.join(errors.iterator(), ","));
991 }
992
993 // check return params
994 List<String> returnParamNames = new ArrayList<String>();
995 for(FlowReturnSend p : flowAccept.getReturns())
996 {
997 returnParamNames.add(p.getName());
998 }
999
1000 for(FlowReturnAccept p : flowCall.getReturns())
1001 {
1002 String name = p.getName();
1003 if (!returnParamNames.remove(name))
1004 {
1005 // Returned param has no matching entry in the caller, ie the calling view is expecting a
1006 // return value but the called flow is not returning it.
1007 errors.add(name);
1008 }
1009 }
1010 // Add any leftover return params that do not have accept return param equivalents, ie where the
1011 // called flow is trying to return a value but the caller is not expecting it.
1012 errors.addAll(returnParamNames);
1013
1014 if (!errors.isEmpty())
1015 {
1016 throw new OrchestraException("Return parameter names mismatch:" + StringUtils.join(errors.iterator(), ","));
1017 }
1018
1019 // all ok
1020 return;
1021 }
1022
1023 // no instances of this class expected
1024 private FlowHandler()
1025 {
1026 }
1027 }