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