10 Replies Latest reply on Apr 7, 2010 1:14 AM by kragoth

    The neverending story: Conversation leftovers!

    jlemire

      WARNING: This is a fresh thread on an old subject. I have been surprised to find out that a problem that seems so obvious to me (and others) in typical use cases did not get a real solution and was dismissed as 'left as a proof to the reader' by the seam guys. While I agree there are workarounds, I seem to miss the obvious solution that does not deviate too much from the usual Seam philosophy


      I have been following a few threads on the forum of users asking how to really end a conversation on redirect. I will explain what I mean by really ending a conversation, but let me first give you my very simple and I think typical use case:


      ----- USE CASE: Creating/editing patients -----



      1. The user logs onto the site

      2. The user navigates to the patients list

      3. The user clicks the edit link on a given patient in the list. He is directed to the creation/edition page.

      4. The user corrects the last name of the patient and clicks save. He is brought back to the users list. In the h:messages section, he sees The patient's information was successfully updated..

      5. The user clicks on the Create user button, which directs him to the creation/edition page.



      When the user executes the last step, the patientHome already has a value and he falls on the edit user page for the user he edited a few steps before. This happens because both my list and my detail creation/edition pages define long-running conversations. As long as you go from one LRC to another LRC, the target LRC keeps all the variables defined in the previous LRC.


      Now you may argue that in this particular case:



      • I should not use a LRC on the list and/or the edition screen: There are many occasions where a conversational page may navigate to another one when it is done. This is a navigation concern. The originating code should not care about that.

      • I should not use the same page for multiple purposes: just make it that the edit and creation pages are different and the problem persists

      • I should not reuse the same seam conversational component in different use cases: Oh, come on.



      -------- END OF USE CASE --------


      What I expected from Seam is that when a conversation is ended, the variables within its scope are destroyed. I do not really care when this happens. I just want it to be transparent, i.e. the next conversation does not contain stuff I didn't put there. However, that's not how it works. Conversation buckets can and will be reused, depending on the next page's conversation propagation configuration.


      For the record: when you end a conversation, you have two choices:



      1. After redirect (default): Seam demotes the conversation to temporary, but re-promotes it again before redirect so the conversation context is propagated. It is however marked as should be demoted and is demoted to temporary conversation as soon as it is restored on the GET.

      2. Before redirect (as an attribute in page.xml or on the End annotation): Conversation is demoted to temporary and consequently destroyed at the end of the request.



      In the first case, the conversation gets destroyed as long as the next page does not initiate a LRC. If it does, the whole bucket is re-promoted along with its previous content. As long as the user goes from one LRC to the next, it accumulates garbage.


      The second case does indeed destroy the conversation. Great! you say. But at least one conversational Seam component does benefit use the conversation's temporary promotion: facesMessages (and derivatives). And since the messages are not resolved yet, it also needs the whole context along with it. This means you losing the conversation makes you lose your messages.


      So there are two built-in workarounds to my problem:



      1. use beforeRedirect=true and lose the facesmessages (maybe other stuff to?)

      2. make sure all the conversational components and their dependencies clean up their context themselves on conversation End.



      The first solution works, but I still need the messages. I would hate to tell the people here that we cannot output feedback without an intermediary page (a la Mantis) after each action because of platform limitations.


      The second workaround is as impractical and error prone as the original problem.


      Seam enables and encourages a seamless post-redirect behavior in order to be restful while keeping JSF's functionality (i.e. facesmessages). Doing this, along with the extended persistence context, Seam also encourages a general use of conversations. On the functional side, I can see no reason why one would like to reuse the whole conversation state cross-redirect on conversation end.


      Shouldn't there be a way, on conversation end, to clear up the current conversation bucket, propagating only what's necessary for the next page to display? That's what we have elected to do here for the time being. Either by always ending our conversation with beforeRedirect=true and keep the resolved facesmessages in a bucket we pass to the next page (creating our own mini-conversation), or playing with the current conversation objects to try to empty the conversation's contents on demotion and keep only the resolved messages to the temporary conversation.


      Anybody has a better solution?

        • 1. Re: The neverending story: Conversation leftovers!
          rogermorituesta.rogermori.yahoo.com

          Facing a similar dilemma, I found a solution that might work for you.


          The problem is not the conversation itself but the Seam-Button or Seam-Link within the LIST VIEW pointing to the EDIT or ADD views. On advanced, the HOME component is created while rendering the LIST VIEW because of being referenced on the EDIT page descriptor.


          I did not try to replace the s-button with an h-outputLink but it might work with a lot of pain.


          Our solution was to save the parameters required to restore the LIST VIEW context,  invoke the EDIT VIEW passing those parameters, end the conversation and redirect to a VIEW VIEW (no long running conversation) keeping the parameters, and finally to return (ending the conversation) to the LIST VIEW (creating a new conversation) carrying the same parameters in order to restore the previous context. 


          Adding the same generic parameters names on the page descriptors of the associated views and using a Facelet include directive made this solution affordable.


          Thank you.


          Roger.

          • 2. Re: The neverending story: Conversation leftovers!
            swd847

            For the use case you described I have the create new instance button call a method that clears the old object from the scope (I don't have to write this method every time, it is handled automatically by my framework code).


            It is not ideal and not a solution for all use cases I know, but it does the job.

            • 3. Re: The neverending story: Conversation leftovers!
              rogermorituesta.rogermori.yahoo.com

              May you please post your method code that clears the old object instance?


              Thank you.


              Roger.

              • 4. Re: The neverending story: Conversation leftovers!

                Jacques Lemire wrote on Nov 12, 2009 20:03:



                ----- USE CASE: Creating/editing patients -----


                1. The user logs onto the site

                2. The user navigates to the patients list

                3. The user clicks the edit link on a given patient in the list. He is directed to the creation/edition page.

                4. The user corrects the last name of the patient and clicks save. He is brought back to the users list. In the h:messages section, he sees The patient's information was successfully updated..

                5. The user clicks on the Create user button, which directs him to the creation/edition page.



                When the user executes the last step, the patientHome already has a value and he falls on the edit user page for the user he edited a few steps before. This happens because both my list and my detail creation/edition pages define long-running conversations. As long as you go from one LRC to another LRC, the target LRC keeps all the variables defined in the previous LRC.



                I had something like this happening to me, and I solved it by modifying the Create user button. I added the attribute: includePageParams="false" to the s:button. After that, the patientHome stopped keeping the leftovers of the previous conversation.

                • 5. Re: The neverending story: Conversation leftovers!
                  jlemire

                  While you are right stating that evaluating the parameters on a s:link or s:button does add stuff to the context, it is not exactly the problem I am talking about.


                  My problem occurs during any post->redirect situation, with or without parameters. You see, for the conversation to survive redirects, Seam will temporarily promote the current POST conversation to a LRC, to be demoted again ASAP during the following GET. Even if the POST conversation was explicitely ended. If the next page requests a LRC (@Begin or <begin-conversation ...>), Seam will simply promote the previously demoted conversation and keep all its state.


                  The only way for the state to get cleaned is if your post-redirect results in a page that does not require a LRC. Until that, all the previous garbage stays in the conversation context...

                  • 6. Re: The neverending story: Conversation leftovers!
                    jlemire

                    Here is our current solution to cleanup the temporary conversation contents when it gets its short lived promotion to survive redirects. Before redirect, when a conversation is not a LRC, we want to destroy everything it contains but some items that we explicitly want to survive the redirect. For now, we only keep the facesmessages.


                    @Scope( ScopeType.EVENT )
                    @Name( "org.jboss.seam.core.manager" )
                    @Install( precedence= Install.APPLICATION )
                    @BypassInterceptors
                    public class ScnManager extends FacesManager {
                         
                         private final static Logger log = Logger.getLogger( ScnManager.class );
                         
                         private boolean isTemporaryPromotion;
                         
                         @Override
                         public void beforeRedirect() {
                              super.beforeRedirect();          
                              isTemporaryPromotion = getCurrentConversationEntry().isRemoveAfterRedirect();
                         }
                    
                         @Observer( "org.jboss.seam.afterPhase" )
                         public void onAfterPhase( Object event ) {
                              if( isTemporaryPromotion ) {
                                   log.info( "destroying conversation contents (except for statusMessages)" );
                                   destroy();               
                              }
                         }
                    
                         private void destroy() {          
                              String key = "org.jboss.seam.international.statusMessages";
                              Object statusMessages = Contexts.getConversationContext().get( key );
                              if( statusMessages != null ) {
                                   Contexts.getConversationContext().remove( key );
                              }
                              
                              Lifecycle.destroyConversationContext( 
                                        FacesContext.getCurrentInstance().getExternalContext().getSessionMap(),
                                        getCurrentConversationId() );
                              
                              Contexts.getConversationContext().set( key, statusMessages );
                         }
                    }
                    



                    It is a pretty neat solution, as it does not force us to use beforeRedirect everywhere, losing the facesmessages functionality. I cannot see right now why we would keep anything else in the conversation context for a temporary conversation. We just implemented it, so we still need to see if it causes some other problems we have not thought about.


                    I would be very interested to hear a veteran's seam's take on that solution. If it makes sense, I wonder why Seam does not currently offer it. This solution could be generalized by adding some kind of @SurvivesRedirect flag so that we can pick out easily which items to purge when redirecting a temporary conversation.

                    • 7. Re: The neverending story: Conversation leftovers!
                      fuzzy333

                      if you destroy the conversation's contents in onAfterPhase, the entityManager will complain that there is an active transaction, so a better place to do this in in the endRequest:


                      @Override
                      public void endRequest( Map<String, Object> session ) {
                           destroy( session );
                           super.endRequest(session);
                      }
                      
                      private void destroy( Map<String, Object> session ) {          
                      
                           String key = "org.jboss.seam.international.statusMessages";
                           Object statusMessages = Contexts.getConversationContext().get( key );
                      
                           Lifecycle.destroyConversationContext(
                                session,
                                getCurrentConversationId() );
                      
                           if( statusMessages != null ) {
                                Contexts.getConversationContext().set( key, statusMessages );
                           }
                      }



                      • 8. Re: The neverending story: Conversation leftovers!
                        fuzzy333

                        oops forgot a piece...



                        @Override
                        public void endRequest( Map<String, Object> session ) {
                             if( isTemporaryPromotion ) {
                                  destroy( session );
                             }
                             super.endRequest(session);
                        }
                        
                        private void destroy( Map<String, Object> session ) {          
                        
                             String key = "org.jboss.seam.international.statusMessages";
                             Object statusMessages = Contexts.getConversationContext().get( key );
                        
                             Lifecycle.destroyConversationContext(
                                  session,
                                  getCurrentConversationId() );
                        
                             if( statusMessages != null ) {
                                  Contexts.getConversationContext().set( key, statusMessages );
                             }
                        }



                        • 9. Re: The neverending story: Conversation leftovers!
                          fuzzy333

                          Here's an update with some fixes for issues we ran into. Mainly don't destroy conversation when the session is invalid and flush the conversation before destroying it.




                          private boolean isTemporaryPromotion;
                               
                          @Override
                          public void beforeRedirect() {
                               super.beforeRedirect();          
                               isTemporaryPromotion = getCurrentConversationEntry().isRemoveAfterRedirect();          
                          }
                          
                          @Override
                          public void endRequest( Map<String, Object> session ) {     
                                    
                               boolean destroyConversation = !Session.instance().isInvalid() && isTemporaryPromotion;
                               
                               if( destroyConversation ) {
                                    Contexts.getConversationContext().flush();     
                                    String key = "org.jboss.seam.international.statusMessages";
                                    Object statusMessages = Contexts.getConversationContext().get( key );
                                    Lifecycle.destroyConversationContext( 
                                         session,
                                         getCurrentConversationId() );
                                    if( statusMessages != null ) {
                                         Contexts.getConversationContext().set( key, statusMessages );
                                    }
                               }          
                               
                               super.endRequest( session );
                                         
                          }



                          • 10. Re: The neverending story: Conversation leftovers!
                            kragoth

                            Seam's cross conversation navigation is pretty much broken out of the box.


                            I would recommend taking a look at how we solved this problem by reading this thread http://www.seamframework.org/Community/CrazyIdeaProgrammaticNavigationStaticOrDynamic


                            I implemented the concept of a NavigationManager which takes care of the swapping between conversations and the passing of params between conversations without the problems described in this thread.