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 }