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 }