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  
20  package org.apache.myfaces.orchestra.flow.components;
21  
22  import java.io.IOException;
23  import java.util.HashMap;
24  import java.util.Map;
25  
26  import javax.faces.FacesException;
27  import javax.faces.component.UIComponentBase;
28  import javax.faces.context.FacesContext;
29  import javax.faces.context.ResponseWriter;
30  
31  /**
32   * A component which allows an Orchestra Flow to be rendered into a popup window embedded
33   * in the page that calls the flow.
34   * <p>
35   * When this component is present in a page, and a postback of the page triggers 
36   * navigation to the entry point of a flow, then instead of actually navigating to
37   * the flow entry page the current page is re-rendered. This component will then
38   * take care of rendering appropriate javascript to open the popup window and load
39   * the flow entry page into it.
40   * <p>
41   * This component does not implement a "modal dialog" itself; it assumes that some other
42   * library is used to actually manage the dialog (eg the s:modalDialog component from
43   * the Tomahawk Sandbox). Instead, the page author is responsible for placing a hidden
44   * modal dialog component in the page, and then configuring this component  with an
45   * appropriate javascript command to execute when a flow begins. This
46   * component then renders that javascript only in the right circumstances.
47   * <p>
48   * This component has no effect at all on navigation that is not a "flow call". 
49   * <p>
50   * Note that triggering a flow is always done via a normal postback of the calling
51   * page, just as it happens for a non-modal flow call. This keeps things consistent.
52   * When using flow.xml files for configuration, the decision on whether a particular
53   * navigation is a flow call or not is done only in the flow.xml file, and not in the
54   * page. But whether a flow (if one is triggered) is normal or modal is made only in
55   * the page, and not in the flow.xml files. There is no overlap in responsibility
56   * here, ie no place where configuration must be consistent in two different places
57   * in order for the system to work. Whether a navigation is to a flow is a programmer
58   * decision; whether it is a popup or not is a UI designer decision and keeping these
59   * separated is important.
60   * <p>
61   * The alternative of popping up a window then posting from that is not used because
62   * that would mean that the page is making assumptions about whether a flow call is
63   * going to happen or not; it couples the page and the flow behaviour too tightly.
64   * If the postback did *not* trigger the start of a flow for example, then things
65   * would get very confusing.
66   * <p>
67   * This design should be fully compatible with AJAX; an AJAX postback can trigger
68   * a flow on the server. As long as the page "updated region" includes this modalFlow
69   * component, and the javascript returned to the browser in that region is executed
70   * then a popup modal flow should also work fine.
71   */
72  public class ModalFlow extends UIComponentBase
73  {
74      public static final String COMPONENT_FAMILY = "javax.faces.Component";
75      public static final String COMPONENT_TYPE = "org.apache.myfaces.orchestra.flow.components.ModalFlow";
76  
77      private String outcome;
78      private String onEntry;
79      private String onExit;
80  
81      // transient property
82      private boolean active = false;
83  
84      @Override
85      public String getFamily()
86      {
87          return COMPONENT_FAMILY;
88      }
89  
90      /**
91       * The optional flow outcome that causes this component to render the onEntry script.
92       * <p>
93       * When set, then this component will render the onEntry script (ie cause a new flow
94       * to be modal) only when a new flow is triggered by a matching outcome value. Calls
95       * to flows triggered by other outcomes will cause the current page to be replaced
96       * by the flow entry page (as normal) rather than running the flow in a modal window.
97       * <p>
98       * When not set (ie when null), any flowCall triggered by the containing page will
99       * activate this component (run as a modal flow).
100      * <p>
101      * Static value only (EL expressions not supported).
102      */
103     public String getOutcome()
104     {
105         return outcome;
106     }
107 
108     public void setOutcome(String outcome)
109     {
110         this.outcome = outcome;
111     }
112 
113     /**
114      * Specify some javascript that will be executed when this component is
115      * triggered by a flow call.
116      * <p>
117      * Static value only (EL expressions not supported).
118      */
119     public String getOnEntry()
120     {
121         return onEntry;
122     }
123 
124     public void setOnEntry(String script)
125     {
126         this.onEntry = script;
127     }
128 
129     /**
130      * Specify some javascript that will be executed when the flow which
131      * triggered this component returns.
132      * <p>
133      * Static value only (EL expressions not supported).
134      */
135     public String getOnExit()
136     {
137         return onExit;
138     }
139 
140     public void setOnExit(String script)
141     {
142         this.onExit = script;
143     }
144 
145     /**
146      * Cause this component to render the onEntry script that shows the associated modal dialog.
147      * <p> 
148      * This is called by the Orchestra Flow framework when the containing view has returned an
149      * outcome which triggers a flow and which matches the outcome property of this component.
150      */
151     public void setActive(boolean state)
152     {
153         this.active = state;
154     }
155 
156     // ================ State Methods =================
157 
158     @Override
159     public void restoreState(FacesContext context, Object state)
160     throws FacesException
161     {
162         Object[] states = (Object[]) state;
163         super.restoreState(context, states[0]);
164         outcome = (String) states[1];
165         onEntry = (String) states[2];
166         onExit = (String) states[3];
167 
168         // While restoring view, add self to request scope, so that the
169         // FlowNavigationHandler can find it, activate it, and trigger
170         // re-render of the current view rather than actually doing
171         // navigation to the flow entry page.
172         //
173         // Note that a null outcome is valid.
174         
175         Map<String, Object> reqMap = context.getExternalContext().getRequestMap();
176         @SuppressWarnings("unchecked")
177         Map<String, ModalFlow> flowMap = (Map<String, ModalFlow>) reqMap.get("modalFlows");
178         if (flowMap == null)
179         {
180             flowMap = new HashMap<String, ModalFlow>();
181             reqMap.put("modalFlows", flowMap);
182         }
183         if (flowMap.containsKey(outcome))
184         {
185             throw new FacesException("Duplicate ModalFlows with same outcome");
186         }
187         flowMap.put(outcome, this);
188     }
189 
190     @Override
191     public Object saveState(FacesContext context)
192     {
193         return new Object[]
194             {
195                 super.saveState(context),
196                 outcome,
197                 onEntry,
198                 onExit
199             };
200     }
201 
202     // ============ Renderer methods =================
203 
204     /**
205      * When this component is active (postback for the containing view
206      * has just triggered a flowcall using an outcome matching this
207      * component), render the onEntry javascript.
208      * <p>
209      * When this component is not active, nothing is rendered.
210      */
211     @Override
212     public void encodeBegin(FacesContext context) throws IOException
213     {
214         super.encodeBegin(context);
215         
216         if (active)
217         {
218             // Here, output script. We assume that this component is later
219             // within the page than whatever defines the function that this
220             // script calls.
221             //
222             // TODO: maybe here we should use StringSubstitutor to insert
223             // variables like ${viewURL} into the provided script?
224             ResponseWriter rw = context.getResponseWriter();
225             rw.startElement("script", this);
226             rw.write(onEntry);
227             rw.endElement("script");
228 
229             // Reset active flag immediately. When a flow is entered the calling view root
230             // is saved, and restored on return. When the view is stored in serialized form
231             // then the saveState/restoreState methods will reset the active flag. But if
232             // a simple reference to the viewroot is cached, then re-rendering the original
233             // view after return would cause this active section to run again, which is
234             // definitely not wanted. So here, reset the (transient) active flag.
235             active = false;
236         }
237     }
238 
239     // ============ Static methods =================
240 
241     /**
242      * Determines whether the most recently restored view contains a ModalFlow
243      * component that maps to this outcome;
244      */
245     public static ModalFlow getForOutcome(FacesContext context, String outcome)
246     {
247         Map<String, Object> reqMap = context.getExternalContext().getRequestMap();
248         @SuppressWarnings("unchecked")
249         Map<String, ModalFlow> flowMap = (Map<String, ModalFlow>) reqMap.get("modalFlows");
250         if (flowMap == null)
251         {
252             return null;
253         }
254 
255         ModalFlow mf = (ModalFlow) flowMap.get(outcome);
256         if (mf != null)
257         {
258             return mf;
259         }
260 
261         // look for a global one
262         return (ModalFlow) flowMap.get(null);
263     }
264 }