7 Replies Latest reply: Sep 30, 2010 6:54 PM by Bernhard Hurzeler RSS

pdf generation without redirect

Lukasz Rozek Newbie

hi guys,


I use seam feature for pdf generation. Everything works fine. Our requirements don't allow case, when user bookmarks redirected url i.e.

seam/docstore/document.seam?docId=1&cid=7

and when accesses it one more time gets exception for missing resource, or page with explanation why the resource is missing (errorPage attribute of documentStore). I set attribute sendRedirect to false which doesn't work as I expect (I expected that sent response will be pdf document).


The code below (UIDocument) shows what happens when sendRedirect is set to false, so I nested document within outputText (implementator of ValueHolder). I got page with result of toString() on object kept by outputText.value. I have no idea what intention had author while writing this piece of code. Can you give me any hint how to make usage of this ??


      {
         UIComponent parent = getParent();

         if (parent instanceof ValueHolder)
         {
            ValueHolder holder = (ValueHolder) parent;
            holder.setValue(documentData);
         }
      }




In order to achieve my requirements I extended UIDocument and reimplemented encodeEnd() and changed  else clause for sendRedirect to code below, which is a copy of DocumentStorePhaseListener


        {
            HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
            response.setContentType(documentData.getDocumentType().getMimeType());

            response.setHeader("Content-Disposition", documentData.getDisposition() + "; filename=\""
                    + documentData.getFileName() + "\"");

            response.getWriter();
            documentData.writeDataToStream(response.getOutputStream());
            context.responseComplete();
        }



Unfortunately I got following exception


java.lang.IllegalStateException: Servlet response already use Writer, OutputStream not possible
     at org.ajax4jsf.webapp.FilterServletResponseWrapper.getOutputStream(FilterServletResponseWrapper.java:224)
     at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:102)
     at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:102)
     at org.jboss.seam.pdf.ui.PatchedUIDocument.encodeEnd(PatchedUIDocument.java:63)
     at javax.faces.component.UIComponent.encodeAll(UIComponent.java:946)
     at javax.faces.component.UIComponent.encodeAll(UIComponent.java:942)
     at com.sun.facelets.FaceletViewHandler.renderView(FaceletViewHandler.java:592)
     at org.ajax4jsf.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:100)
     at org.ajax4jsf.application.AjaxViewHandler.renderView(AjaxViewHandler.java:176)
     at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:109)
     at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:100)
     at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:139)
     at javax.faces.webapp.FacesServlet.service(FacesServlet.java:266)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:83)
     at org.jboss.seam.web.RewriteFilter.doFilter(RewriteFilter.java:63)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.web.IdentityFilter.doFilter(IdentityFilter.java:40)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.web.MultipartFilter.doFilter(MultipartFilter.java:90)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.web.ExceptionFilter.doFilter(ExceptionFilter.java:64)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.web.RedirectFilter.doFilter(RedirectFilter.java:45)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:73)
     at org.ajax4jsf.webapp.BaseFilter.doFilter(BaseFilter.java:532)
     at org.jboss.seam.web.Ajax4jsfFilter.doFilter(Ajax4jsfFilter.java:56)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.web.LoggingFilter.doFilter(LoggingFilter.java:60)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.web.HotDeployFilter.doFilter(HotDeployFilter.java:53)
     at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
     at org.jboss.seam.servlet.SeamFilter.doFilter(SeamFilter.java:158)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
     at org.ajax4jsf.webapp.BaseXMLFilter.doXmlFilter(BaseXMLFilter.java:178)
     at org.ajax4jsf.webapp.BaseFilter.handleRequest(BaseFilter.java:290)
     at org.ajax4jsf.webapp.BaseFilter.processUploadsAndHandleRequest(BaseFilter.java:390)
     at org.ajax4jsf.webapp.BaseFilter.doFilter(BaseFilter.java:517)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
     at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
     at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
     at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
     at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:182)
     at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:432)
     at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
     at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
     at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
     at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
     at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
     at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:262)
     at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
     at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
     at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:446)
     at java.lang.Thread.run(Thread.java:619)



This is my show stopper and can't find any solution / workaround


I have couple of questions:
1) why this exception is thrown?
2) when DocumentStorePhaseListener executes the same code it works?
3) how to make usage of sendRedirect when set to false (what was author's idea)?
4) any ideas how to achieve my goal?


thx for any help

  • 1. Re: pdf generation without redirect
    Norman Richards Expert

    The sendRedirect attribute is not for your use case.  That attribute is used when you are embedding the UIDocument into some other component rather than sending it via HTTP.  I believe the only time we ever do this is for email attachments.

  • 2. Re: pdf generation without redirect
    Norman Richards Expert

    Oh, as you can see, we cannot send the PDF binary directly due to a limitation of facelets.  It's been so long, I don't remember the exact issue, but we spent quite a bit of time trying to work around this issue and eventually settled on the redirect.


    As for your particular issue, I'm not sure I have a good solution.  I think I would try to find a way to capture the document store exception and redirect to the appropriate URL that causes the PDF to be re-rendered. 

  • 3. Re: pdf generation without redirect
    Lukasz Rozek Newbie

    Norman Richards wrote on Aug 14, 2009 03:19:


    Oh, as you can see, we cannot send the PDF binary directly due to a limitation of facelets.  It's been so long, I don't remember the exact issue, but we spent quite a bit of time trying to work around this issue and eventually settled on the redirect.

    As for your particular issue, I'm not sure I have a good solution.  I think I would try to find a way to capture the document store exception and redirect to the appropriate URL that causes the PDF to be re-rendered. 


    thx for you reply.


    If you have a quick look at DocumentStorePhaseListener especially at sendContent(), you will notice that the same code is executed and response.getOutputStream() doesn't throw any exception.


    The differnce is:


    - in my case (I execute that code in encodeEnd() of overridden UIDocument) which is in render response phase, and state of org.ajax4jsf.webapp.FilterServletResponseWrapper has changed at this point ( useWireter is set to true)
    



    - in normal case, when documentStore saves binary file and redirects browser to other url. Then DocumentStorePhaseListener gets to action, which is before restore view and org.ajax4jsf.webapp.FilterServletResponseWrapper's state hasn't changed (useWriter is set to false) and exception isn't thrown.
    



    I haven't had time to investigate why between restore view and render response phase state of org.ajax4jsf.webapp.FilterServletResponseWrapper is changed, I quess it is inner implementation of ajax4jsf. Do you have any knowledge about this issue ??


    I will try your solution and let you know if it works, but I've noticed one problem while reading your reply. Lets say I have a couple of templates for pdf generation i.e invoice.pdf, billing.pdf. When user
    request resource as follows (bookmarked url), exception will be thrown


    seam/docstore/document.seam?docId=1&cid=7
    



    On exception how can I decide where to redirect user, to invoice.pdf or billing.pdf. It would work if I had only one template, but in my case I have many. Any ideas how to solve this ?

  • 4. Re: pdf generation without redirect
    Norman Richards Expert

    That is because with the phase listener, we can intercept the request much earlier, before facelets has gotten a hold of things and restricted us from sending binary conent.


    I don't have a good answer off the top off my head on how to redirect the way you want.  I might have time to take a look next week and make a specific suggestion.  In the meantime, if you play with it some and have specific ideas for improving seam to better serve your usecase, I'm all ears.

  • 5. Re: pdf generation without redirect
    Lukasz Rozek Newbie

    I gave up idea with redirection, and trying resolve where to redirect user on exception. While working on this idea I encountered a couple of ideas and problems :


    - I would have to append another query parameter to redirected url, (defining source of redirection), i.e. docName, than I would end up with link as follows. I can't think of any other solution, how I  could make decision where to redirect


    seam/docstore/document.seam?docId=1&docName=invoice&cid=7
    



    - if I required some other query parameters to generate document, I would also have to append them, i.e. let's say user can decide for which month to generate document. This requirement gives me links like follows:


    /pdf/invoice.pdf?month=august
    



    and redirected url


    seam/docstore/document.seam?docId=1&docName=invoice&month=august&cid=7
    



    - I end up with ugly url



    I found workaround / solution and achieved my requirement without redirection.


    1) I added exportKey for UIDocument, here are details: https://jira.jboss.org/jira/browse/JBSEAM-2613


    2) added some classes to catch documentDate, here are details: http://seamframework.org/Community/SavingSEAMPDFAsAFileOnTheServer, http://seamframework.org/Community/PDFdocumentStore


    3) created component for pdfGeneration, code below


    @BypassInterceptors
    @Name("pdfGenerator")
    public class PdfGenerator {
    
        @Logger
        private Log logger;
    
        public DocumentData generate(String viewId, String exportKey) {
            if (StringUtils.isBlank(viewId)) {
                throw new IllegalArgumentException("viewId can't be null");
            }
            if (StringUtils.isBlank(exportKey)) {
                throw new IllegalArgumentException("exportKey can't be null");
            }
            EmptyFacesContext emptyFacesContext = new EmptyFacesContext();
            try {
                Renderer.instance().render(viewId);
            } finally {
                emptyFacesContext.restore();
            }
            DocumentData documentData = (DocumentData) Contexts.getEventContext().get(exportKey);
            if (null == documentData) {
                logger.warn("pdf document not found for in request for key " + exportKey);
                return null;
            }
            return documentData;
        }
    }
    



    4) Last step is only a proof of concept, but it works. I sticked with idea of intercepting request before facelets restricts me from sending binary content. I created phaseListener, which is executed before renderRespone, just like DocumentStorePhaseListener, so I can make call to response.getOutputSream and exception isn't thrown. Here is code of my phaseListener:


    public class PdfGeneratorPhaseListener implements PhaseListener {
    
        private static final long serialVersionUID = -8562030687738960410L;
    
        public void afterPhase(PhaseEvent event) {
            //...
        }
    
        public void beforePhase(PhaseEvent event) {
            String viewId = Pages.getViewId(event.getFacesContext());
            try {
                if (viewId.startsWith("/pdf/")) {
                    PdfGenerator pdfGenerator = (PdfGenerator) Component.getInstance("pdfGenerator");
                    DocumentData documentData = pdfGenerator.generate(viewId, Pages.getCurrentBaseName());
                    HttpServletResponse response = (HttpServletResponse) event.getFacesContext().getExternalContext()
                            .getResponse();
                    response.setContentType(documentData.getDocumentType().getMimeType());
                    response.setHeader("Content-Disposition", documentData.getDisposition() + "; filename=\""
                            + documentData.getFileName() + "\"");
                    documentData.writeDataToStream(response.getOutputStream());
                    event.getFacesContext().responseComplete();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public PhaseId getPhaseId() {
            return PhaseId.RENDER_RESPONSE;
        }
    }
    



    I tested it for a couple of templates with params and patterns. It works and it satisfies my requirements


    Conclusions:


    - pretty urls :


    /pdf/august/invoice, or /pdf/invoice.pdf?month=august
    


    - bookmarkable urls, user can access as many times as he want and exception won't be thrown


    - resource is accessed with one trip to the server (no redirection)


    I look forward for your feedback

  • 6. Re: pdf generation without redirect
    Ondrej Sevce Newbie

    Thank you Lukasz, your post was very helpful for me!


    Ondrej

  • 7. Re: pdf generation without redirect
    Bernhard Hurzeler Novice

    I also need to generate PDFs with out redirect.


    I have two questions:




    1. Do I have to change the UIDocument class directly or is there a way to extend it?

    2. Do I have to configure the PdfGeneratorPhaseListener in the web.xml file somehow?



    Thanks for you help.


    Bernhard