Benito Vega
Benito Vega

Reputation: 51

When using MyFaces Orchestra, conversation.access beans NOT being removed when navigating to a different view

We are building an application using JSF 2, Spring, and Hibernate. MyFaces Orchestra is being used to to provide conversation scope which we're using for most of the pages in the application (to take advantage of Orchestra's management of the Hibernate Session). All of our beans are declared to use the conversation.access scope which (according to the Orchestra documentation) should mean that the beans are removed from scope as soon as the user navigates to a page that does not contain any references to that backing bean instance.

The problem I'm encountering is that, if I navigate a way from a view without explicitly invalidating the conversation, if they come back to that view later it still has the same data as before. I implemented ConversationBindingListener methods in all my backing beans and I can see when they're being removed from the conversation and I can see that they're NOT in many cases.

What makes the issue more perplexing is that the backing beans are removed when I navigate to some pages (views) but not to others. I thought maybe that was because pages had an unintended reference to other backing beans in the EL but I was not able find any. I also thought that maybe this issue only happened when I navigated from one page that had a conversation.access scoped bean to another page using a different conversation.scoped bean. However, the cases where it is removed from the conversation, that page also contains references to a conversation.access scoped bean.

As I said early, explicitly invalidating the conversation using Conversation.getCurrentInstance().invalidate() works. However, explicitly invalidating the conversation is not possible for every use case since it will be a very common use case for the user can leave a view just by clicking on one of the navigation links.

ADDITIONAL DETAILS: We're using Hibernate 3.6 (instead of JPA) so that meant we had to use the HibernatePersistenceContextFactory.

Here is what my Spring context configuration looks like (for Orchestra).

<!-- 1. initialization of all orchestra modules (required for core15 module) -->
<import resource="classpath*:/META-INF/spring-orchestra-init.xml" />

<!-- 2. the conversation scopes -->
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
        <map>
            <entry key="conversation.manual">
                <bean
                    class="org.apache.myfaces.orchestra.conversation.spring.SpringConversationScope">
                    <property name="timeout" value="30" />
                    <property name="advices">
                        <list>
                            <ref bean="persistentContextConversationInterceptor" />
                        </list>
                    </property>
                </bean>
            </entry>
            <entry key="conversation.access">
                <bean
                    class="org.apache.myfaces.orchestra.conversation.spring.SpringConversationScope">
                    <property name="timeout" value="30" />
                    <property name="advices">
                        <list>
                            <ref bean="persistentContextConversationInterceptor" />
                        </list>
                    </property>
                    <property name="lifetime" value="access" />
                </bean>
            </entry>
        </map>
    </property>
 </bean>    


<!-- 3. the "entity manager" manager -->
<bean id="persistentContextConversationInterceptor"
    class="org.apache.myfaces.orchestra.conversation.spring.PersistenceContextConversationInterceptor">
    <property name="persistenceContextFactory" ref="persistentContextFactory" />
</bean>



<!-- 4. conversation - persistence adapter -->
<bean id="persistentContextFactory"
    class="com.acme.infra.orchestra.hibernate.HibernatePersistenceContextFactory">
    <property name="entityManagerFactory" ref="sessionFactory" />
</bean>

<!-- 5. persistence -->
<bean id="managedDataSource"
    class="org.apache.myfaces.orchestra.connectionManager.ConnectionManagerDataSource">
    <property name="dataSource" ref="dataSource" />
</bean>

Here are a couple examples of JSF backing bean declarations.

<bean id="quoteSummaryBackingBean" class="com.acme.ui.backing.QuoteSummaryBackingBean"
        scope="conversation.access" orchestra:conversationName="QuoteSummaryConversation">
    <property name="quotingBusinessService" ref="quotingBusinessService"/>
    <property name="customerBusinessService" ref="customerBusinessService"/>
    <property name="referenceDataBusinessService" ref="referenceDataBusinessService"/>
    <property name="quoteExportBusinessService" ref="quoteExportBusinessService" />
</bean>

<bean id="createQuoteBackingBean" class="com.acme.ui.backing.CreateQuoteBackingBean" 
        scope="conversation.access" orchestra:conversationName="CreateQuoteConversation">  
    <property name="quotingBusinessService" ref="quotingBusinessService"/>
    <property name="customerBusinessService" ref="customerBusinessService"/>
    <property name="referenceDataBusinessService" ref="referenceDataBusinessService"/>


Upvotes: 3

Views: 3133

Answers (2)

Pavel S.
Pavel S.

Reputation: 1224

As Benito Vega correctly said in his comment, the problem manifests when you do a GET request to a view that contains <f:metadata/> tag. This is because Orchestra's AccessScopePhaseListener.doAfterRestoreView() differentiates between POST and GET by testing FacesContext.getRenderResponse(). However this is not true in case of a GET request to a view containing an <f:metadata/> tag (see line 244 of RestoreViewPhase.java for why it is so). That's why this case looks to the subsequent code in AccessScopePhaseListener.doAfterRenderResponse() like a postback to the same view, which is a reason for skipping invalidation of not accessed beans.

I've created my own phase listener to fix this problem. It adds to the result of AccessScopePhaseListener.doAfterRestoreView() a snippet which makes the state of 'oldView' request attribute look equally for any GET request regardless of whether the view contains <f:metadata/> or not. The snippet runs before RENDER_RESPONSE phase, so the mutual order of the Orchestra's listener and the mine is not important.

import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import org.apache.myfaces.orchestra.conversation.jsf.AccessScopePhaseListener;

public class OrchestraAccessScopeBugFixer implements PhaseListener {
    /**
     * @see AccessScopePhaseListener#OLD_VIEW_KEY
     */
    private static final String OLD_VIEW_KEY = AccessScopePhaseListener.class.getName() + ":oldView";

    @Override
    public void beforePhase(PhaseEvent event) {
        FacesContext facesContext = event.getFacesContext();
        if (!facesContext.isPostback()) {
            // this makes it think that we are on a new view, not posting back to the same one
            facesContext.getExternalContext().getRequestMap().put(OLD_VIEW_KEY, null);
        }
    }

    @Override
    public void afterPhase(PhaseEvent event) {
    }

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }
}

I've tested this solution for GET to a view with or without <f:metadata/>, for POST to the same view and for POST with navigation to another one, and it's working as expected. Yet I am not sure why Orchestra developers could not use FacesContext.isPostback() instead of FacesContext.getRenderResponse() in AccessScopePhaseListener.doAfterRestoreView() in order to make difference between a postback and a not one.

Upvotes: 0

Benito Vega
Benito Vega

Reputation: 51

This is not the most elegant solution and I'm guessing it could be introducing new bugs (since the check used by Orchestra was meant to handle situations with AJAX requests). I added a new method to my backing beans (using a base class) that handles resetting the org.apache.myfaces.orchestra.conversation.jsf.AccessScopePhaseListener:oldView request scope variable to null.

public void clearPreviousConversation() {
    if (firstHit) {
        String keyName = 
            "org.apache.myfaces.orchestra.conversation.jsf.AccessScopePhaseListener:oldView";

        FacesContext.getCurrentInstance().getExternalContext()
        .getRequestMap().put(keyName, null);

        firstHit = false;
    }
}   

To ensure this method only gets called one time per view, I have the "firstHit" flag which is a boolean member variable.

Then, since this particular issue only manifests itself on views that use f:metadata, I take advantage of that fact to only call this method where it's needed. I add it as a pre-render call in my f:metadata.

<f:metadata>
    <f:event type="preRenderView" listener="#{controlPanelBackingBean.clearPreviousConversation}" />
</f:metadata>

If you're using f:viewParam or other f:event elements, you can mix them together.

<f:metadata>
    <f:viewParam name="tabIndex" value="#{controlBackingBean.tabIndex}" />
    <f:event type="preRenderView" listener="#{controlPanelBackingBean.clearPreviousConversation}" />
    <f:event type="preRenderView" listener="#{controlPanelBackingBean.init}" />
</f:metadata>

Upvotes: 2

Related Questions