3 Replies Latest reply on Jul 9, 2007 9:26 PM by jakec

    Getting objects from conversation in a custom PhaseListener

    jakec

      I need to make a URL that can download a binary file based on information stored in a long running conversation. I can get it to work fine once from a page, but the second time, it fails with:

      ERROR [PhaseListenerManager] Exception in PhaseListener RESTORE_VIEW(1) afterPhase
      java.lang.IllegalStateException: No active event context
       at org.jboss.seam.core.Manager.instance(Manager.java:267)


      I'm pretty sure something I'm doing is ending the long running conversation, because if I just download a document once, then try to continue with my workflow by going to the next page, or even just refresh the current page, I get a similar error:
      org.jboss.seam.NoConversationException: no long-running conversation for @Conversational bean


      I'm pretty new at Seam, so if I'm making a colossal blunder, please be kind... :-) I've searched all over trying to figure out what is going on, but I just can't seem to find anything relevant.

      Here is my PhaseListener:
      package com.myco.myproj;
      
      import java.util.Set;
      
      import javax.faces.context.FacesContext;
      import javax.faces.el.ValueBinding;
      import javax.faces.event.PhaseEvent;
      import javax.faces.event.PhaseId;
      import javax.servlet.http.HttpServletResponse;
      
      import org.jboss.seam.annotations.Name;
      import org.jboss.seam.annotations.security.Restrict;
      import org.jboss.seam.contexts.EntityBean;
      import org.jboss.seam.core.Manager;
      import org.jboss.seam.jsf.TransactionalSeamPhaseListener;
      import org.jboss.seam.log.Log;
      
      @Name("docPhaseListener")
      @Restrict("#{identity.loggedIn}")
      @SuppressWarnings("serial")
      public class DocPhaseListener extends TransactionalSeamPhaseListener {
       public final static int BUF_SIZE = 16384;
       public final static String DOC_VIEW_ID = "/document/";
      
       private static Log log = (Log) org.jboss.seam.log.Logging.getLog(DocPhaseListener.class);
      
       private MyObject getMyObject(FacesContext context) {
       MyObject myObject = null;
       log.info("Manager.instance().getCurrentConversationId()="+Manager.instance().getCurrentConversationId());
       ValueBinding vb = context.getApplication().createValueBinding("#{myObject}");
       if(vb == null) {
       log.info("vb is null!");
       } else {
       Object o = null;
       log.info("vb type="+vb.getType(context)+", vb value="+(o=vb.getValue(context)));
       if(o instanceof MyObject)
       myObject = (MyObject) o;
       else
       log.info("Not MyObject!");
       }
      
       return myObject;
       }
      
       public void afterPhase(PhaseEvent event) {
       super.afterPhase(event);
       FacesContext context = event.getFacesContext();
       String viewId = context.getViewRoot().getViewId();
       int index = viewId.indexOf(DOC_VIEW_ID);
       if (PhaseId.RESTORE_VIEW.equals(event.getPhaseId()) && index != -1 && viewId.endsWith(".xhtml")) {
       MyObject myObject = getMyObject(context);
       if(myObject != null)
       Util.handleDocumentRequest(context, myObject);
       else {
       // TODO: Handle bad requests better
       ((HttpServletResponse)context.getExternalContext().getResponse()).setContentType("application/octet-stream");
       context.responseComplete();
       }
       }
       }
      }
      


      Since I'm extending TransactionalSeamPhaseListener (it seems necessary to get into the conversation the first time), and you can't have more than one SeamPhaseListener, I had to replace the existing one with mine, so my faces-config.xml looks like this:

      <lifecycle>
       <!--phase-listener>org.jboss.seam.jsf.TransactionalSeamPhaseListener</phase-listener-->
       <phase-listener>com.medorder.mazama.DocumentPhaseListener</phase-listener>
       </lifecycle>
      


      My UI is part of a rich:dataTable, and the column looks like this:

      <h:column>
       <f:facet name="header">Download</f:facet>
       <h:outputLink value="document/download.seam?cid=#{conversation.id}&clr=true">
       <f:verbatim>Download</f:verbatim>
       </h:outputLink>
       </h:column>
      


      Eventually, this URL will be called from an embedded Flash object, so I really need to use a PhaseListener. Can anyone tell me what I'm doing wrong to end the conversation?

      I have also tried just implementing PhaseListener, and pulling the object from the SessionMap directly using the key "org.jboss.seam.CONVERSATION#xx$myObject", but I get the same kind of problem. The second time I access the EntityBean that is returned, the instance it points to is null.

      Also, can someone confirm my suspicion that @Name and @Restrict are pointless to use on a PhaseListener? I don't think any injection has worked on it, and I'm pretty sure that it is not possible to use those on something that pretty much by definition lives outside the life-cycle.

        • 1. Re: Getting objects from conversation in a custom PhaseListe
          pmuir

          It's probably better to do this in a servlet - take a look at the org.jboss.seam.ui.resource.StyleResource.

          • 2. Re: Getting objects from conversation in a custom PhaseListe
            jakec

            That looks like exactly what I need, but I can't use 2.0, with a handy-dandy ContextualHttpServletRequest. Here is what I've come up with for 1.1.5GA:
            components.xml

            <component name="documentResource" class="com.myco.myproject.DocumentResource" scope="APPLICATION" auto-create="true"/>
            


            DocumentResource.java
            package com.myco.myproject;
            
            import static org.jboss.seam.InterceptionType.NEVER;
            import static org.jboss.seam.ScopeType.APPLICATION;
            import static org.jboss.seam.annotations.Install.BUILT_IN;
            
            import java.io.File;
            import java.io.InputStream;
            import java.io.IOException;
            import java.util.Set;
            
            import javax.faces.context.FacesContext;
            import javax.servlet.http.HttpServletRequest;
            import javax.servlet.http.HttpServletResponse;
            
            import org.jboss.seam.annotations.Install;
            import org.jboss.seam.annotations.Intercept;
            import org.jboss.seam.annotations.Name;
            import org.jboss.seam.annotations.Scope;
            import org.jboss.seam.annotations.Startup;
            import org.jboss.seam.annotations.security.Restrict;
            import org.jboss.seam.contexts.ContextAdaptor;
            import org.jboss.seam.contexts.Lifecycle;
            import org.jboss.seam.core.Expressions;
            import org.jboss.seam.core.Manager;
            import org.jboss.seam.core.Expressions.ValueBinding;
            import org.jboss.seam.log.Log;
            import org.jboss.seam.log.Logging;
            import org.jboss.seam.servlet.AbstractResource;
            
            import com.myco.myproject.db.Document;
            import com.myco.myproject.db.DocumentSet;
            
            @Startup
            @Scope(APPLICATION)
            @Name("documentResource")
            @Restrict("#{identity.loggedIn}")
            @Install(precedence = BUILT_IN)
            @Intercept(NEVER)
            public class DocumentResource extends AbstractResource {
             private static final String RESOURCE_PATH = "/document";
            
             public static final String WEB_RESOURCE_PATH = "/seam/resource" + RESOURCE_PATH;
            
             private static Log log = (Log) Logging.getLog(DocumentResource.class);
            
             private static String docDir = "C:\\resources\\docs";
            
             static final int BUF_SIZE = 16384;
            
             @Override
             protected String getResourcePath() {
             return RESOURCE_PATH;
             }
            
             Document getDocument(int index) {
             Set<Document> attachments = null;
             try {
             ValueBinding vb = Expressions.instance().createValueBinding("#{currentDocumentSet}");
             if (vb == null)
             log.error("VB is null!");
             else {
             Object o = vb.getValue();
             log.info("vb type=" + vb.getType() + ", value=" + o);
             if (o != null && o instanceof DocumentSet)
             attachments = ((DocumentSet) o).getAttachments();
             }
             } catch (Throwable t) {
             log.error("Exception getting VB", t);
             }
            
             return attachments == null || index >= attachments.size() ? null : (Document) attachments.toArray()[index];
             }
            
             @Override
             public void getResource(HttpServletRequest request,
             HttpServletResponse response) throws IOException {
             String pathInfo = request.getPathInfo().substring(getResourcePath().length());
             log.info("pathInfo=" + pathInfo);
             beginConversation(request);
            
             String path[] = pathInfo.split("/");
             int i = 0;
             boolean src = false;
             if ("".equals(path))
             ++i;
             if ("src".equals(path)) {
             src = true;
             ++i;
             }
             Document doc = null;
             try {
             doc = getDocument(Integer.parseInt(path));
             } catch (Throwable t) {
             log.error("Exception parsing document index", t);
             }
            
             InputStream is = null;
             if (doc != null)
             try {
             if(docDir == null) {
             try {
             docDir = getServletContext().getInitParameter("document.folder");
             } catch (Throwable t) {
             log.error("Exception getting init parameter", t);
             }
             }
             is = doc.getInputStream(src, docDir);
             int len = -1;
             byte[] buf = new byte[BUF_SIZE];
             while((len = is.read(buf)) != -1)
             response.getOutputStream().write(buf, 0, len);
             } catch (IllegalArgumentException e) {
             response.sendError(HttpServletResponse.SC_NOT_FOUND);
             return;
             } catch (Throwable t) {
             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
             return;
             } finally {
             if(is != null) try {is.close();} catch (Exception ignored){}
             }
             else {
             response.sendError(HttpServletResponse.SC_NOT_FOUND);
             return;
             }
             response.setHeader("Cache-Control", "no-store");
             response.setHeader("Pragma", "no-cache");
             response.setDateHeader("Expires", 0);
             response.setContentType("application/octet-stream");
             response.getOutputStream().flush();
             response.getOutputStream().close();
             endConversation(request);
             }
            
             private void beginConversation(HttpServletRequest request) {
             Lifecycle.beginRequest(getServletContext(), request.getSession(), request);
             Manager.instance().restoreConversation(request.getParameterMap());
             Lifecycle.resumeConversation(request.getSession());
             Manager.instance().handleConversationPropagation(request.getParameterMap());
             }
             private void endConversation(HttpServletRequest request) {
             Manager.instance().endRequest(ContextAdaptor.getRequest(request));
             Lifecycle.endRequest();
             }
             }
            


            The code in my beginConversation() and endConversation() methods seem to be roughly equivalent to code in ContextualHttpServletRequest, but I'm sure that I'm not doing it correctly, as I'm still getting an exception, and still killing the conversation.

            The exception I get now is:
            ERROR [DocumentResource] Exception getting VB
            javax.el.PropertyNotFoundException: ELResolver cannot handle a null base Object with identifier
             'currentDocumentSet'


            If I log "Manager.instance().getCurrentConversationId()", I get the correct ID, but I can't seem to get the information out of the conversation. And once again, if I try to continue after this, I get an error:
            Caused by: javax.ejb.EJBTransactionRolledbackException: org.jboss.seam.NoConversationException:
             no long-running conversation for @Conversational bean:
             documentSetController
            


            Also, it is ignoring the @Restrict and allowing access to un-logged-in users, although, of course, they also get the javax.el.PropertyNotFoundException.

            • 3. Re: Getting objects from conversation in a custom PhaseListe
              jakec

              OK, I've found out how to get objects from the Context, but I'm still killing the conversation, even though I thought I found a fix for that.

              Now, in my try block for getDocument(), I have this:

               DocumentSet docSet = (DocumentSet)Contexts.getConversationContext().get("currentReferral");
               if(attachments != null)
               attachments = docSet.getAttachments();
              


              My endConversation() now looks like this:
               Manager.instance().leaveConversation();
               Manager.instance().endRequest(ContextAdaptor.getRequest(request));
               Lifecycle.endRequest();
              


              However, I'm still having the same problem with killing the Conversation.
              The second time I try to access one of these links, the Conversation ID has changed, even though the URL has not. The commend for Manager.leaveConversation() says "Leave the scope of the current conversation, leaving it completely intact", but it doesn't work for me.

              ... Time Passes ...

              OK, I've tried another tactic. I've tried NOT being a part of the initial conversation. My URL does NOT have "cid' in it. Instead, I use "tid". I look that value up from the SessionMap and call Manager.restoreConversation() with it. Now I get some really strange results, though.

              restoreConversation() always returns true, but I do NOT always end up in the requested conversation! Every other time, I end up with a DIFFERENT conversationID (as returned by Manager.instance().getCurrentConversationId()), and my Contexts.getConversationContext().get() fails on those occasions.

              So, I tried resetting it back to adding cid and clr to the original URL, and almost the same thing happened, except that restoreConversation actually returned false when it returned with a different conversation.

              INFO [DocumentResource] pathInfo=/src/0
              INFO [DocumentResource] restoreConversation to 4=true
              INFO [DocumentResource] cid=4
              INFO [DocumentResource] pathInfo=/src/0
              INFO [DocumentResource] restoreConversation to 4=false
              INFO [DocumentResource] cid=8
              INFO [DocumentResource] pathInfo=/src/0
              INFO [DocumentResource] restoreConversation to 4=true
              INFO [DocumentResource] cid=4
              INFO [DocumentResource] pathInfo=/src/0
              INFO [DocumentResource] restoreConversation to 4=false
              INFO [DocumentResource] cid=10
              INFO [DocumentResource] pathInfo=/src/0
              INFO [DocumentResource] restoreConversation to 4=true
              INFO [DocumentResource] cid=4
              INFO [DocumentResource] pathInfo=/src/0
              INFO [DocumentResource] restoreConversation to 4=false
              INFO [DocumentResource] cid=12
              


              This was done by doing +Refresh in the browser (IE 6) once I got an error page. On the occasion that it succeeded, it didn't prompt me to save a file, but actually returned the binary data in the browser.

              I could really use some help on preserving the Conversation Context. :-)