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.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         UIViewRoot oldViewRoot = fc.getViewRoot();
145         if ((oldViewRoot != null) && fc.getRenderResponse())
146         {
147             // No view was restored; instead the viewRoot that FacesContext just returned
148             // is a *newly created* view that should be rendered, not a postback to be processed.
149             // In this case, save null as the "old" view to indicate that no view was restored,
150             // which will trigger the access-scope checking after rendering is complete.
151             oldViewRoot = null;
152         }
153         fc.getExternalContext().getRequestMap().put(OLD_VIEW_KEY, oldViewRoot);
154     }
155 
156     /**
157      * Handle "beforePhase" callback for RENDER_RESPONSE phase.
158      * 
159      * @since 1.3
160      */
161     private void doBeforeRenderResponse(PhaseEvent event)
162     {
163         AccessScopeManager accessManager = AccessScopeManager.getInstance();
164         accessManager.beginRecording();
165     }
166 
167     /**
168      * Handle "afterPhase" callback for RENDER_RESPONSE phase.
169      * 
170      * @since 1.3
171      */
172     private void doAfterRenderResponse(PhaseEvent event)
173     {
174         javax.faces.context.FacesContext fc = event.getFacesContext();
175         UIViewRoot viewRoot = fc.getViewRoot();
176         UIViewRoot oldViewRoot = (UIViewRoot) fc.getExternalContext().getRequestMap().get(OLD_VIEW_KEY);
177         if (viewRoot != oldViewRoot)
178         {
179             // Either this is a GET request (oldViewRoot is null) or this is a postback which
180             // triggered a navigation (oldViewRoot is not null, but is a different instance).
181             // In these cases (and only in these cases) we want to discard unaccessed conversations at
182             // the end of the render phase.
183             //
184             // There are reasons why it is not a good idea to run the invalidation check
185             // on every request:
186             // (a) it doesn't work well with AJAX requests; an ajax request that only accesses
187             //    part of the page should not cause access-scoped conversations to be discarded.
188             // (b) on conversion or validation failure, conversations that are only referenced
189             //    via the "value" attribute of an input component will not be accessed because
190             //    the "submittedValue" for the component is used rather than fetching the value
191             //    from the backing bean.
192             // (c) running each time is somewhat inefficient
193             //
194             // Note that this means that an access-scoped conversation will continue to live
195             // even when the components that reference it are not rendered, ie it was not
196             // technically "accessed" during a request.
197             invalidateAccessScopedConversations(event.getFacesContext().getViewRoot().getViewId());
198         }
199     }
200 
201     /**
202      * Invalidates any conversation with aspect {@link ConversationAccessLifetimeAspect}
203      * which has not been accessed during a http request
204      */
205     protected void invalidateAccessScopedConversations(String viewId)
206     {
207         AccessScopeManager accessManager = AccessScopeManager.getInstance();
208         if (accessManager.isIgnoreRequest())
209         {
210             return;
211         }
212 
213         if (accessManager.getAccessScopeManagerConfiguration() != null)
214         {
215             Set ignoredViewIds = accessManager.getAccessScopeManagerConfiguration().getIgnoreViewIds();
216             if (ignoredViewIds != null && ignoredViewIds.contains(viewId))
217             {
218                 // The scope configuration has explicitly stated that no conversations should be
219                 // terminated when processing this specific view, so just return.
220                 // 
221                 // Special "ignored views" are useful when dealing with things like nested
222                 // frames within a page that periodically refresh themselves while the "main"
223                 // part of the page remains unsubmitted.
224                 return;
225             }
226         }
227 
228         ConversationManager conversationManager = ConversationManager.getInstance(false);
229         if (conversationManager == null)
230         {
231             return;
232         }
233 
234         boolean isDebug = log.isDebugEnabled();
235         Iterator iterConversations = conversationManager.iterateConversations();
236         while (iterConversations.hasNext())
237         {
238             Conversation conversation = (Conversation) iterConversations.next();
239             
240             // This conversation has "access" scope if it has an attached Aspect
241             // of type ConversationAccessLifetimeAspect. All other conversations
242             // are not access-scoped and should be ignored here.
243             ConversationAccessLifetimeAspect aspect =
244                 (ConversationAccessLifetimeAspect)
245                     conversation.getAspect(ConversationAccessLifetimeAspect.class);
246 
247             if (aspect != null)
248             {
249                 if (aspect.isAccessed())
250                 {
251                     if (isDebug)
252                     {
253                         log.debug(
254                             "Not clearing accessed conversation " + conversation.getName()
255                             + " after rendering view " + viewId);
256                     }
257                 }
258                 else
259                 {
260                     if (isDebug)
261                     {
262                         log.debug(
263                             "Clearing access-scoped conversation " + conversation.getName()
264                             + " after rendering view " + viewId);
265                     }
266                     conversation.invalidate();
267                 }
268             }
269         }
270     }
271 }