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 }