14 Replies Latest reply on Mar 5, 2010 3:24 PM by vesuvius.vesuvius.prime.gmail.com

    Passing data into a new conversation (in JSF)

    vesuvius.vesuvius.prime.gmail.com

      I was wondering about the best way to pass data into a newly-created long-running conversation without polluting the conversation with unnecessary beans.


      A very simplistic scenario would be: I am working with a JSF page (Page1) whose backing bean is @ConversationScoped for some reason (for example, it must be able to survive a post-redirect-get operation). From that page, I wish to navigate to another page (Page2) which must start a new long-running conversation. And I wish to pass data (say, customer name) from the first page into the second page. But I don't want the first page's backing bean to be part of the newly created conversation.


      I don't know the best way -- I'm asking about it here. But I can think of two ways to pass data:



      One way would be to pass the data REST-style (as request parameters):



      page1.xhtml:


      <h:inputText value="#{page1.custName}" /><br/>
      <h:commandButton value="To Page2" action="#{page1.toPage2}"/>



      Page1.java:


      @ConversationScoped
      @Named
      public class Page1 implements Serializable {
      
          public String toPage2() {
              return "page2?faces-redirect=true&custName=" + custName;
          }
          ...
      




      page2.xhtml:


      <f:metadata>
          <f:viewParam name="custName" value="#{page2.custName}"/>
      </f:metadata>
      ...
      



      Page2.java:


      @ConversationScoped
      @Named
      public class Page2 implements Serializable {
      
          @Inject Conversation conversation;
      
          @PostConstruct
          public void init() {
              conversation.begin();
          }
          ...
      



      I have no idea if I'm doing it right, but this works.



      However, I'm interested in the other way -- passing data via server-side state instead of passing it as request parameters:


      Page1.java:


      @ConversationScoped
      @Named
      public class Page1 implements Serializable {
      
          @Inject Page2 page2;
      
          public String toPage2() {
              page2.setCustName(custName);
              return "page2";
          }
          ...
      



      Here's my real problem: when using this method, and if Page2.java starts a long-running conversation (see the Page2 snippet further above that starts the conversation), then Page1.java will be placed in the long-running conversation too. I don't want it there. I want ONLY Page2.java in the conversation. How can I do that? I understand it's impossible to remove beans from a context, but I don't quite understand why? Why shouldn't I be allowed to remove Page1 from the conversation? I don't need it there. Or... what can I do, so that Page1 doesn't get included into the conversation in the first place?


      If you reached this point, thank you for your patience! I just want to learn some 'best practices'.

        • 1. Re: Passing data into a new conversation (in JSF)

          I would also love to know the answer to this

          • 2. Re: Passing data into a new conversation (in JSF)

            Maybe what is needed is to have an special scope type for information that is going to be sent from one conversation into another?

            • 3. Re: Passing data into a new conversation (in JSF)

              Or some new kind of annotation/interceptor? for the method that makes the transition to the other conversation? something like @ConversationTransition


              @ConversationScoped
              @Named
              public class Page1 implements Serializable {
              
                  @Inject Page2 page2;
              
                  @ConversationTransition
                  public String toPage2() {
                      page2.setCustName(custName);
                      return "page2";
                  }
              

              • 4. Re: Passing data into a new conversation (in JSF)
                chasetec

                You want the Page1 bean and the Page2 bean to have different lifetimes so you shouldn't be putting them in the same scope. Having Page1 be responsible for copying it's data into the (not yet in existence) Page2 bean is not something you want to do.


                Sounds like you need the flash scope from JSF. You could put the Page1 bean in flash scope and have the Page2 bean in conversation scope. The Page2 bean would read the state out of Page1 via injection before Page1 went out of scope.


                Maybe Seam3 will have a flash scope, I think there was some discussion about it.


                The other answer is; Put the information that you need to outlast the 1st conversation in a scope that out lasts the conversation. Putting the customer name in the Session scope comes to mind.

                • 5. Re: Passing data into a new conversation (in JSF)
                  vesuvius.vesuvius.prime.gmail.com

                  Thank you for your reply!


                  Cay Horstmann is not particularly enthusiastic about the Flash scope: http://weblogs.java.net/blog/cayhorstmann/archive/2010/01/12/flash-pan


                  He's a famous guy and he co-authored Core JavaServer Faces. I suppose he knows what he's talking about but I'm beginning to learn the stuff now, so I can't be sure of anything. In time, I will develop my own views on the various matters. For example, I'm not super-enthusiastic about CDI's limitations (especially it's insistence to control everything, it's lack of support for PhaseListeners, and its lack of APIs to manipulate the contexts programmatically or to request a bean from a context programmatically), but I might be wrong. I will learn and see...


                  Regarding our scenario -- I need Page1 to be in temporary @ConversationScope. (Temporary conversations are actually quite similar to the Flash scope -- they survive a redirect.) Of course, it would be perfect if Page1 was @RequestScoped -- this would mean that it would not be included in the newly-created long-running conversation, but instead would be discarded at the end of the request. Unfortunately, the scenario stipulates that this is not an option, because I need Page1 to be able to survive a redirect. @RequestScoped beans don't survive redirects. We need @ConversationScope.


                  To me, it would have been great to have some API that would allow me to remove a bean from a context. In fact, a rich API would have suited me perfectly. Alas, no luck. We need to make do with what we have (or use some other DI framework?). And since I would love to know the 'best practice' for our scenario, I asked in the forum.   :(


                  It seems to me that it's better for Page1 to inject Page2, instead of the other way around. The reason, IMHO, is that Page2 might be the entry point for some common wizard or something. It's not supposed to know that Page1 (or any other page) exists. Instead, the clients of the wizard need to pass some data into the wizard. At least, that's what I think is more appropriate.

                  • 6. Re: Passing data into a new conversation (in JSF)
                    hirowla.ian.rowlands.three.com.au

                    I think I know what you mean, but maybe a different line of thought might clarify things (or blow my ideas out of the water!).


                    I tend not to use PRG if I'm redirecting to the same page. Usually this is the case when form validation fails (basic and extended/custom validation). I would simply be not using PRG until you are ready to go away from that page and all data submitted is valid. Once you are ready to do that, then grab Page2 and insert the values there and go from there.


                    Obviously you've simplified the context, but maybe Page2 is actually part of the same conversation as Page1 and hence should be treated that way. Hard to tell. Or Page1 doesn't need to be a conversation scoped thing - or at least a different conversation.


                    Hope that makes some sense.


                    Ian

                    • 7. Re: Passing data into a new conversation (in JSF)
                      gavin.king

                      Bisser, I recommend that you try working with CDI not against it. Suspend disbelief for a bit and learn the CDI Way. Don't try to do things the way you're used to doing them in other frameworks. The limitations you're describing are there by design. We thought very carefully about them, and we think they are the Right Thing.


                      (P.S. it's very easy to obtain a bean programmatically.)

                      • 8. Re: Passing data into a new conversation (in JSF)
                        vesuvius.vesuvius.prime.gmail.com

                        Thank you, guys!


                        @Ian


                        I didn't mean to use PRG for simple postbacks. I meant that Page1 needed it for some other purpose -- say, to receive some data from Page0. Whatever the reason, we imagine that Page1 is standalone and @ConversationScoped and Page2 is the beginning of a wizard. When the wizard starts, a long-running conversation starts. I don't want Page1, which is not part of the wizard, to be in the conversation context.



                        @Gavin


                        Could you, please, tell me the recommended way to start a long-running conversation without automatically inheriting everything from the current temporary conversation? In other words, using the scenario I described above, to start a conversation that contains only Page2, but not Page1. (Assuming that both Page1 and Page2 are @ConversationScoped.)


                        As for obtaining a bean programmatically, I meant a different thing. I already read the Weld reference but I couldn't find what I need there. Here's a detailed explanation:


                        1. I'm in a PhaseListener.  (Of course, it's NOT instantiated by Weld/CDI (!), because PhaseListeners are not supported by CDI, so there's no dependency injection.)


                        2. The PhaseListener needs a bean from the Session Context. (For example, to perform some authentication activity.)


                        How can the PhaseListener get that bean from the session? I know how to do it without CDI:
                        externalContext.getSessionMap().get(myBean);


                        (Of course, I would not be using a String literal directly. I would be using either a static final constant, or, better yet, a static method in MyBean that would know how to obtain the bean.)


                        How can I force CDI to give me the bean from the Session context?



                        Thank you all for reading and responding!

                        • 9. Re: Passing data into a new conversation (in JSF)
                          hirowla.ian.rowlands.three.com.au

                          Ok, I thought it wouldn't be that simple! Thought I'd say it anyway just it case.


                          Maybe you need to end one conversation and start another one. That's one reason for not starting the conversation as you construct the bean - you lose some flexibility. You can start the conversation when you call one of the methods. You could end the conversation on Page1, put the data into Page2 then start a new conversation on Page2 (possibly with the same method that sets the data).


                          Without stepping on Gavin's toes, I thought that a PhaseListener could be injected into. The PhaseListener can't be a managed bean itself (you can't inject it anywhere else), but it can be injected into - the container would manage that. If that is true, inject yourself a BeanManager or even the bean directly and do it that way. The BeanManager is just a JNDI resource, so I can't see why not. I'm sure somebody will correct me if I'm wrong.


                          Ian


                          • 10. Re: Passing data into a new conversation (in JSF)
                            nickarls

                            I recall filing a JIRA for the Phase Listener injection. It's more of a platform / JSF decision anyway but I agree that injection should be supported wherever possible

                            • 11. Re: Passing data into a new conversation (in JSF)
                              vesuvius.vesuvius.prime.gmail.com

                              @Ian:


                              I cannot end a temporary conversation. I'd get an exception, if I tried. But when I start a long-running conversation, everything that's already in the temporary conversation becomes part of the long-running conversation. This includes both Page1 and Page2. I don't want Page1 to be part of the long-running conversation.


                              (By the way, I'm sorry about being ambiguous as to when I start the long-running conversation. In my second example, where I try to transfer state via injection and not via request parameters, I don't begin() the conversation in the @PostConstruct method. That wouldn't be practical and may lead to exceptions if I try to begin() a conversation that's already long-running.)


                              PhaseListeners cannot be injected into. That's quite unfortunate because I have always been using a PhaseListener to verify a user's state and to redirect to the login page in case of session expiration. (I don't use Filters but PhaseListeners, because quite often I need to redirect while processing an Ajax request.)


                              @Nicklas


                              PhaseListeners are not my real problem. They were just an example.


                              The problem is what to do if I'm inside a manually-created (or, rather, not-created-by-CDI) bean and I want to hook into CDI. The PhaseListener example just illustrates how easy we can get into such a situation.


                              I wish to be able to work with instances that were not created by CDI, and from time to time to ask CDI to do some injection for me. This is quite possible with other DI frameworks.


                              For example, we now have a gigantic application that does not use CDI. Imagine that we wish to migrate to CDI but that migration cannot be done all at once. Instead, we wish to migrate incrementally. This means that initially we wish to hook into CDI at a couple of places and to get dependency injection there. The rest of the application remains the same. In time, we may increase our CDI usage and do more injection at more places. But it is quite likely that we will never need to migrate 100% of our application into CDI. We only wish some injection at some places. Is that possible with CDI? My (bad) experience with PhaseListeners seems to indicate that it's not possible. If I wish to use CDI, it must take control over my whole application. Frankly, that's something that scares me. I wish to retain control over my own application and only use CDI at certain places where I need it.



                              Again, thank you all for taking the time to reply. I truly wish to like CDI and, if I learn the right way to use it, as well as how to do certain things, I may begin to like it. (I already like the fact that I can inject session-scoped beans containing data about the logged-in user into EJBs -- this opens up quite a few possibilities beyond the basic role-based authorization.) However, right now I'm afraid of CDI and its desire to dominate my application.

                              • 12. Re: Passing data into a new conversation (in JSF)
                                vesuvius.vesuvius.prime.gmail.com

                                @Ian:


                                Hey, Ian, in addition to my comment in the post above, I decided to give the BeanManager thing a try, even though it's an SPI class and I've been reluctant to try it until now. Here's something that works -- it can be used to manually lookup beans from inside a PhaseListener:


                                public class CdiUtils {
                                
                                    public static BeanManager getBeanManager() {
                                        try {
                                            return (BeanManager) new InitialContext().lookup("java:comp/BeanManager");//"java:app/BeanManager");
                                        } catch (NamingException e) {
                                            throw new BeanManagerNotFoundException("Couldn't locate the CDI BeanManager", e);
                                        }
                                    }
                                
                                    public static <S extends Annotation,T> T getBean(Class<S> scopeType, Class<T> beanType, Annotation... qualifiers) {
                                        final BeanManager bm = getBeanManager();
                                        final Context ctx = bm.getContext(scopeType);
                                
                                        final Set<Bean<?>> beans = bm.getBeans(beanType, qualifiers);
                                        for (Bean<?> bean : beans) {
                                            return (T)ctx.get(bean);
                                        }
                                
                                        return null;
                                    }
                                
                                }



                                NOTE: I have removed most of the error-handling code (for the sake of simplicity). Resolution errors, and all other errors, should be handled accordingly.


                                It can be used like this:


                                UserState userState = CdiUtils.getBean(SessionScoped.class, UserState.class);



                                I don't know if what I'm doing is legal or correct. I didn't even try to optimize it or to see if there's a better/easier way. I just gave it a try, that's all.


                                P.S. I know such code is not the epitome of testability, but I'm currently only interested in getting the work done. Beautiful testability will be left to the reader.

                                • 13. Re: Passing data into a new conversation (in JSF)
                                  hirowla.ian.rowlands.three.com.au

                                  Funny you should mention it - I have an InjectionUtilities class class to do similar things. But I have a slightly different approach - not necessarily the cleanest, but it does certain things for me.


                                  I have a ServletContextListener, and I inject my InjectionUtilities class into it. I inject my BeanManager into my InjectionUtilities class, so I don't need to look it up via JNDI (I have managed to avoid JNDI altogther in this project). But my InjectionUtilities class has a private constructor, and a @Singleton stereotype. Once I have instantiated it, I only have static methods on it. The main one - getBeanManager(). That way, every class can get a BeanManager without needing to look it up via JNDI and you know it is available once the application has started up.


                                  The other methods on this class - injectClass() (manually forces the injection of the object passed into it) and createManagedClass() (which creates it via CDI and then does the injection). Damn useful and when I start instantiating classes rather than the container doing it, I have injection available. And it is needed - for example, I have a JSF error handler that isn't container managed. I liked @Injecting so much people might think I'm a drug addict!

                                  • 14. Re: Passing data into a new conversation (in JSF)
                                    vesuvius.vesuvius.prime.gmail.com

                                    Well, I suppose that if you want the BeanManager injected, and if you do not want to depend on things like ServletContextListener, you might do something like this:


                                    public class CdiUtils implements Extension {
                                    
                                        public void init(@Observes AfterBeanDiscovery event, BeanManager beanMgr) {
                                            ... some initialization & setup here ...
                                        }
                                    
                                        ...



                                    It worked for me -- it injected the BeanManager.


                                    Of course, at the moment I cannot be sure which method is a 'best practice', and which one is not. I'm just mentioning it as an option.