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.event.PhaseEvent; 26 import javax.faces.event.PhaseId; 27 import javax.faces.event.PhaseListener; 28 29 import org.apache.commons.logging.Log; 30 import org.apache.commons.logging.LogFactory; 31 import org.apache.myfaces.orchestra.conversation.AccessScopeManager; 32 import org.apache.myfaces.orchestra.conversation.Conversation; 33 import org.apache.myfaces.orchestra.conversation.ConversationAccessLifetimeAspect; 34 import org.apache.myfaces.orchestra.conversation.ConversationManager; 35 36 /** 37 * Handle access-scoped conversations. 38 * <p> 39 * At the end of request processing, delete any access-scope conversations for which no 40 * bean in that scope has been accessed during the <i>render phase</i> of the request. 41 * Therefore if a postback occurs which accesses a specific conversation A, and that 42 * conversation then forwards to a different view whose render phase does not refer 43 * to any bean in conversation A, then the conversation is discarded. This ensures 44 * that conversations are discarded as soon as possible, and in particular that if 45 * control returns to the a view that accesses conversation A on the next cycle then 46 * a new conversation instance is created. 47 * <p> 48 * If a view happens to want its postbacks handled by a bean in conversation A, 49 * but the render phase never references anything in that conversation, then the 50 * conversation will be effectively request-scoped. This is not expected to be a 51 * problem in practice as it would be a pretty odd view that has stateful event 52 * handling but either renders nothing or fetches its data from somewhere other 53 * than the same conversation. If such a case is necessary, the view can be modified 54 * to "ping" the conversation in order to keep it active via something like an 55 * h:outputText with rendered="#{backingBean.class is null}" (which always resolves 56 * to false, ie not rendered, but does force a method-invocation on the backingBean 57 * instance). Alternatively, a manual-scoped conversation can be used. 58 * <p> 59 * Note that if FacesContext.responseComplete is called during processing, then 60 * no phase-listeners for the RENDER_RESPONSE phase are executed. And any navigation 61 * rule that specifies "redirect" causes responseComplete to be invoked. Therefore 62 * access-scoped beans are not cleaned up immediately. However the view being 63 * redirected to always runs its "render" phase only, no postback. The effect, 64 * therefore, is exactly the same as when an internal forward is performed to 65 * the same view: in both cases, the access-scoped beans are kept if the next view 66 * refers to them, and discarded otherwise. 67 * <p> 68 * Note also that if responseComplete is invoked by another PhaseListener in the 69 * beforePhase for RENDER_RESPONSE then the beforePhase for all other PhaseListeners 70 * is still invoked, but no afterPhase methods are invoked. This means that this 71 * phase listener will not discard any access-scoped conversations, as that is 72 * only done in the afterPhase method. In particular, this applies to tomahawk 73 * PPR requests, where the PPRPhaseListener uses responseComplete(). This behaviour 74 * is exactly what is wanted; partial-page-rendering should not cause access-scoped 75 * conversations to terminate. 76 * 77 * @since 1.1 78 */ 79 public class AccessScopePhaseListener implements PhaseListener 80 { 81 private static final long serialVersionUID = 1L; 82 private final Log log = LogFactory.getLog(AccessScopePhaseListener.class); 83 84 public PhaseId getPhaseId() 85 { 86 return PhaseId.RENDER_RESPONSE; 87 } 88 89 public void beforePhase(PhaseEvent event) 90 { 91 AccessScopeManager accessManager = AccessScopeManager.getInstance(); 92 accessManager.beginRecording(); 93 } 94 95 public void afterPhase(PhaseEvent event) 96 { 97 invalidateAccessScopedConversations(event.getFacesContext().getViewRoot().getViewId()); 98 } 99 100 /** 101 * Invalidates any conversation with aspect {@link ConversationAccessLifetimeAspect} 102 * which has not been accessed during a http request 103 */ 104 protected void invalidateAccessScopedConversations(String viewId) 105 { 106 AccessScopeManager accessManager = AccessScopeManager.getInstance(); 107 if (accessManager.isIgnoreRequest()) 108 { 109 return; 110 } 111 112 if (accessManager.getAccessScopeManagerConfiguration() != null) 113 { 114 Set ignoredViewIds = accessManager.getAccessScopeManagerConfiguration().getIgnoreViewIds(); 115 if (ignoredViewIds != null && ignoredViewIds.contains(viewId)) 116 { 117 // The scope configuration has explicitly stated that no conversations should be 118 // terminated when processing this specific view, so just return. 119 // 120 // Special "ignored views" are useful when dealing with things like nested 121 // frames within a page that periodically refresh themselves while the "main" 122 // part of the page remains unsubmitted. 123 return; 124 } 125 } 126 127 ConversationManager conversationManager = ConversationManager.getInstance(false); 128 if (conversationManager == null) 129 { 130 return; 131 } 132 133 boolean isDebug = log.isDebugEnabled(); 134 Iterator iterConversations = conversationManager.iterateConversations(); 135 while (iterConversations.hasNext()) 136 { 137 Conversation conversation = (Conversation) iterConversations.next(); 138 139 // This conversation has "access" scope if it has an attached Aspect 140 // of type ConversationAccessLifetimeAspect. All other conversations 141 // are not access-scoped and should be ignored here. 142 ConversationAccessLifetimeAspect aspect = 143 (ConversationAccessLifetimeAspect) 144 conversation.getAspect(ConversationAccessLifetimeAspect.class); 145 146 if (aspect != null) 147 { 148 if (aspect.isAccessed()) 149 { 150 if (isDebug) 151 { 152 log.debug( 153 "Not clearing accessed conversation " + conversation.getName() 154 + " after rendering view " + viewId); 155 } 156 } 157 else 158 { 159 if (isDebug) 160 { 161 log.debug( 162 "Clearing access-scoped conversation " + conversation.getName() 163 + " after rendering view " + viewId); 164 } 165 conversation.invalidate(); 166 } 167 } 168 } 169 } 170 }