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.conversation.jsf;
21
22 import java.util.Iterator;
23 import java.util.Set;
24
25 import javax.faces.component.UIViewRoot;
26 import javax.faces.event.PhaseEvent;
27 import javax.faces.event.PhaseId;
28 import javax.faces.event.PhaseListener;
29
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32 import org.apache.myfaces.orchestra.conversation.AccessScopeManager;
33 import org.apache.myfaces.orchestra.conversation.Conversation;
34 import org.apache.myfaces.orchestra.conversation.ConversationAccessLifetimeAspect;
35 import org.apache.myfaces.orchestra.conversation.ConversationManager;
36
37 /**
38 * Handle access-scoped conversations.
39 * <p>
40 * After a <i>new view</i> has been rendered, delete any access-scope conversations for which no
41 * bean in that scope has been accessed <i>during the render phase</i> of the request.
42 * <p>
43 * This allows a page which handles a postback to store data into beans in an access-scoped
44 * conversation, then navigate to a new page. That information is available for the new
45 * page during its rendering. And if that data is referenced, it will remain around
46 * until the user does a GET request, or a postback that causes navigation again. Then
47 * following the rendering of that new target page, any access-scoped conversations will be
48 * discarded except for those that the new target page references.
49 * <p>
50 * Any access-scoped conversations that a page was using, but which the new page does NOT use
51 * are therefore automatically cleaned up at the earliest possibility - after rendering of the
52 * new page has completed.
53 * <p>
54 * Note: When a "master" and "detail" page pair exist, that navigating master->detail->master->detail
55 * correctly uses a fresh conversation for the second call to the detail page (and not reuse the
56 * access-scoped data from the first call). By only counting accesses during the render phase, this
57 * works correctly.
58 * <p>
59 * Note: Access-scoped conversations must be preserved when AJAX calls cause only
60 * part of a page to be processed, and must be preserved when conversion/validation failure
61 * cause reads of the values of input components to be skipped. By deleting unaccessed
62 * conversations only after the <i>first</i> render, this happens automatically.
63 * <p>
64 * Note: If a view happens to want its postbacks handled by a bean in conversation A,
65 * but the render phase never references anything in that conversation, then the
66 * conversation will be effectively request-scoped. This is not expected to be a
67 * problem in practice as it would be a pretty odd view that has stateful event
68 * handling but either renders nothing or fetches its data from somewhere other
69 * than the same conversation. If such a case is necessary, the view can be modified
70 * to "ping" the conversation in order to keep it active via something like an
71 * h:outputText with rendered="#{backingBean.class is null}" (which always resolves
72 * to false, ie not rendered, but does force a method-invocation on the backingBean
73 * instance). Alternatively, a manual-scoped conversation can be used.
74 * <p>
75 * Note: If FacesContext.responseComplete is called during processing of a postback,
76 * then no phase-listeners for the RENDER_RESPONSE phase are executed. And any navigation
77 * rule that specifies "redirect" causes responseComplete to be invoked. Therefore
78 * access-scoped beans are not cleaned up immediately. However the view being
79 * redirected to always runs its "render" phase only, no postback. The effect,
80 * therefore, is exactly the same as when an internal forward is performed to
81 * the same view: in both cases, the access-scoped beans are kept if the next view
82 * refers to them, and discarded otherwise.
83 * <p>
84 * Note: Some AJAX libraries effectively do their own "rendering" pass from within
85 * a custom PhaseListener, during the beforePhase for RENDER_RESPONSE. This could
86 * have undesirable effects on Orchestra - except that for all AJAX requests, the
87 * viewRoot restored during RESTORE_VIEW will be the same viewRoot used during
88 * render phase - so this PhaseListener will ignore the request anyway.
89 * <p>
90 * Backwards-compatibility note: The behaviour of this class has changed between
91 * releases 1.2 and 1.3. In earlier releases, the access-scope checking ran on every
92 * request (not just GET or navigation). Suppose a bean is in its own access-scoped
93 * conversation, and the only reference to that bean is from a component that is
94 * rendered or not depending upon a checkbox editable by the user. In the old version,
95 * hiding the component would cause the access-scoped conversation to be discarded
96 * (not accessed), while the current code will not discard it. The new behaviour does
97 * fix a couple of bugs: access-scoped conversations discarded during AJAX requests
98 * and after conversion/validation failure.
99 *
100 * @since 1.1
101 */
102 public class AccessScopePhaseListener implements PhaseListener
103 {
104 private static final long serialVersionUID = 1L;
105 private final Log log = LogFactory.getLog(AccessScopePhaseListener.class);
106
107 private static final String OLD_VIEW_KEY = AccessScopePhaseListener.class.getName() + ":oldView";
108
109 public PhaseId getPhaseId()
110 {
111 return PhaseId.ANY_PHASE;
112 }
113
114 public void beforePhase(PhaseEvent event)
115 {
116 PhaseId pid = event.getPhaseId();
117 if (pid == PhaseId.RENDER_RESPONSE)
118 {
119 doBeforeRenderResponse(event);
120 }
121 }
122
123 public void afterPhase(PhaseEvent event)
124 {
125 PhaseId pid = event.getPhaseId();
126 if (pid == PhaseId.RESTORE_VIEW)
127 {
128 doAfterRestoreView(event);
129 }
130 else if (pid == PhaseId.RENDER_RESPONSE)
131 {
132 doAfterRenderResponse(event);
133 }
134 }
135
136 /**
137 * Handle "afterPhase" callback for RESTORE_VIEW phase.
138 *
139 * @since 1.3
140 */
141 private void doAfterRestoreView(PhaseEvent event)
142 {
143 javax.faces.context.FacesContext fc = event.getFacesContext();
144 if (fc.getResponseComplete())
145 {
146 return;
147 }
148 UIViewRoot oldViewRoot = fc.getViewRoot();
149 if ((oldViewRoot != null) && fc.getRenderResponse())
150 {
151 // No view was restored; instead the viewRoot that FacesContext just returned
152 // is a *newly created* view that should be rendered, not a postback to be processed.
153 // In this case, save null as the "old" view to indicate that no view was restored,
154 // which will trigger the access-scope checking after rendering is complete.
155 oldViewRoot = null;
156 }
157 fc.getExternalContext().getRequestMap().put(OLD_VIEW_KEY, oldViewRoot);
158 }
159
160 /**
161 * Handle "beforePhase" callback for RENDER_RESPONSE phase.
162 *
163 * @since 1.3
164 */
165 private void doBeforeRenderResponse(PhaseEvent event)
166 {
167 AccessScopeManager accessManager = AccessScopeManager.getInstance();
168 accessManager.beginRecording();
169 }
170
171 /**
172 * Handle "afterPhase" callback for RENDER_RESPONSE phase.
173 *
174 * @since 1.3
175 */
176 private void doAfterRenderResponse(PhaseEvent event)
177 {
178 javax.faces.context.FacesContext fc = event.getFacesContext();
179 UIViewRoot viewRoot = fc.getViewRoot();
180 UIViewRoot oldViewRoot = (UIViewRoot) fc.getExternalContext().getRequestMap().get(OLD_VIEW_KEY);
181 if (viewRoot != oldViewRoot)
182 {
183 // Either this is a GET request (oldViewRoot is null) or this is a postback which
184 // triggered a navigation (oldViewRoot is not null, but is a different instance).
185 // In these cases (and only in these cases) we want to discard unaccessed conversations at
186 // the end of the render phase.
187 //
188 // There are reasons why it is not a good idea to run the invalidation check
189 // on every request:
190 // (a) it doesn't work well with AJAX requests; an ajax request that only accesses
191 // part of the page should not cause access-scoped conversations to be discarded.
192 // (b) on conversion or validation failure, conversations that are only referenced
193 // via the "value" attribute of an input component will not be accessed because
194 // the "submittedValue" for the component is used rather than fetching the value
195 // from the backing bean.
196 // (c) running each time is somewhat inefficient
197 //
198 // Note that this means that an access-scoped conversation will continue to live
199 // even when the components that reference it are not rendered, ie it was not
200 // technically "accessed" during a request.
201 invalidateAccessScopedConversations(event.getFacesContext().getViewRoot().getViewId());
202 }
203 }
204
205 /**
206 * Invalidates any conversation with aspect {@link ConversationAccessLifetimeAspect}
207 * which has not been accessed during a http request
208 */
209 protected void invalidateAccessScopedConversations(String viewId)
210 {
211 AccessScopeManager accessManager = AccessScopeManager.getInstance();
212 if (accessManager.isIgnoreRequest())
213 {
214 return;
215 }
216
217 if (accessManager.getAccessScopeManagerConfiguration() != null)
218 {
219 Set ignoredViewIds = accessManager.getAccessScopeManagerConfiguration().getIgnoreViewIds();
220 if (ignoredViewIds != null && ignoredViewIds.contains(viewId))
221 {
222 // The scope configuration has explicitly stated that no conversations should be
223 // terminated when processing this specific view, so just return.
224 //
225 // Special "ignored views" are useful when dealing with things like nested
226 // frames within a page that periodically refresh themselves while the "main"
227 // part of the page remains unsubmitted.
228 return;
229 }
230 }
231
232 ConversationManager conversationManager = ConversationManager.getInstance(false);
233 if (conversationManager == null)
234 {
235 return;
236 }
237
238 boolean isDebug = log.isDebugEnabled();
239 Iterator iterConversations = conversationManager.iterateConversations();
240 while (iterConversations.hasNext())
241 {
242 Conversation conversation = (Conversation) iterConversations.next();
243
244 // This conversation has "access" scope if it has an attached Aspect
245 // of type ConversationAccessLifetimeAspect. All other conversations
246 // are not access-scoped and should be ignored here.
247 ConversationAccessLifetimeAspect aspect =
248 (ConversationAccessLifetimeAspect)
249 conversation.getAspect(ConversationAccessLifetimeAspect.class);
250
251 if (aspect != null)
252 {
253 if (aspect.isAccessed())
254 {
255 if (isDebug)
256 {
257 log.debug(
258 "Not clearing accessed conversation " + conversation.getName()
259 + " after rendering view " + viewId);
260 }
261 }
262 else
263 {
264 if (isDebug)
265 {
266 log.debug(
267 "Clearing access-scoped conversation " + conversation.getName()
268 + " after rendering view " + viewId);
269 }
270 conversation.invalidate();
271 }
272 }
273 }
274 }
275 }