View Javadoc

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 }