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.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 }