Tuesday, April 24, 2012

Saving on the fly generated PDF to file or database (Seam 2.2 application in JBoss AS 5.1)

With Seam, you can create a pdf file on the fly using only xhtml pages, but if you want to get the PDF file’s binary data, that is much harder task. The basic problem, that this function was not designed to provide the binary data, rather than just generate the PDF file using JSF custom tags. You can read the solution in a lot of blogs and forums: just create a mock FacesContext, use that for render the page, then extract the binary data. I have tried the solutions described in various places, but for JBoss AS 5.1 and Seam 2.2.0 GA they didn’t worked. The main problem was to create the mock FacesContext, because you have to have the javax.faces.application.Application and javax.faces.render.RenderKitFactory instances which I had no luck to get at the time of the creation of the mock FacesContext. But by implementing a servlet context listener, I was able to capture both values. You need to register the DocumentServlet and the custom servlet context listener into the web.xml, create the /simple.xhtml page (see later) and put the xhtml code in paragraph #3 into an other page (the menu, for example). The other classes should be put into the ejb module of your application, into the org.example.pdf package. Let's see the steps in detail:

1.Servlet Context Listener and Document Servlet registration in web.xml

In order the PDF generation on the fly to be able to work, you must add the followings servlet mapping to the web.xml. The servlet context listener is used to capture the javax.faces.application.Application and javax.faces.render.RenderKitFactory instances when the servlet context initialized.
<servlet>
    <servlet-name>Document Store Servlet</servlet-name>
    <servlet-class>org.jboss.seam.document.DocumentStoreServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Document Store Servlet</servlet-name>
    <url-pattern>*.pdf</url-pattern>
</servlet-mapping>

  <listener>
    <listener-class>
        org.example.pdf.MockServletContextListener
    </listener-class>
  </listener>

2. Page that generates PDF

You must create a an xhtml page ("/simple.xhtml"), to test the export function:
<p:document xmlns:p="http://jboss.com/products/seam/pdf">                                                      
    <p:chapter>Hello PDF!</p:chapter>
</p:document>

3. Testing xhtml code

In order to test the PDF export function, the following link could be placed somewhere (e.g,: into the menu). This will call the mockPDFFileSaver’s savePDF(fileName, viewName) method, which simply generated the pdf then saves the results into the given file. This example will save the pdf into "d:\simple.pdf"
<s:link 
       includePageParams="false" 
       propagation="none" 
       value="Simple PDF test" 
       action="#{mockPDFFileSaver.
         savePDF('d:\\simple.pdf','/simple.xhtml')}">
</s:link>

4.Servlet Context Listener

The listener can capture the „Application” and „Render Kit Factory” instances. It then saves into the Seam component „factoryData”. Lifecycle.beginCall() initializes Seam.
package org.example.pdf;

import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.render.RenderKitFactory;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.jboss.seam.Component;
import org.jboss.seam.contexts.Lifecycle;

/**
 * ServletContextListener implementation to capture the JSF Application and
 * renderKitFactory. Should be registered into web.xml to work
 * 
 * @author anemeth
 * 
 */
public class MockServletContextListener implements ServletContextListener {

 public void contextDestroyed(ServletContextEvent event) {
 }

 public void contextInitialized(ServletContextEvent event) {

  Lifecycle.beginCall();

  Application application = ((ApplicationFactory) FactoryFinder
    .getFactory(FactoryFinder.APPLICATION_FACTORY)).getApplication();

  RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder
    .getFactory(FactoryFinder.RENDER_KIT_FACTORY);

  FactoryData factoryData = (FactoryData) Component.getInstance("factoryData", true);
  factoryData.setApplication(application);
  factoryData.setRenderKitFactory(renderKitFactory);

 }

}

5. Mock Servlet Context

The MockServletContext from Seam 2.2 was modified in order to work under JBoss AS 5.1
package org.example.pdf;

import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.render.RenderKitFactory;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.jboss.seam.Component;
import org.jboss.seam.contexts.Lifecycle;

/**
 * ServletContextListener implementation to capture the JSF Application and
 * renderKitFactory. Should be registered into web.xml to work
 * 
 * @author anemeth
 * 
 */
public class MockServletContextListener implements ServletContextListener {

 public void contextDestroyed(ServletContextEvent event) {
 }

 public void contextInitialized(ServletContextEvent event) {

  Lifecycle.beginCall();

  Application application = ((ApplicationFactory) FactoryFinder
    .getFactory(FactoryFinder.APPLICATION_FACTORY)).getApplication();

  RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder
    .getFactory(FactoryFinder.RENDER_KIT_FACTORY);

  FactoryData factoryData = (FactoryData) Component.getInstance("factoryData", true);
  factoryData.setApplication(application);
  factoryData.setRenderKitFactory(renderKitFactory);

 }

}

6. FactoryData

This is a simple Seam component for holding the „javax.faces.application.Application” and „javax.faces.render.RenderKitFactory”
package org.example.pdf;

import javax.faces.application.Application;
import javax.faces.render.RenderKitFactory;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;

/**
 * Class for holding the JSF "Application" instance and the renderKitFactory.
 * @author anemeth
 *
 */
@Name("factoryData")
@Scope(ScopeType.APPLICATION)
public class FactoryData {
 
 private Application application;

 private RenderKitFactory renderKitFactory;
 
 /**
  * @return the application
  */
 public Application getApplication() {
  return this.application;
 }

 /**
  * @return the renderKitFactory
  */
 public RenderKitFactory getRenderKitFactory() {
  return this.renderKitFactory;
 }

 /**
  * @param application
  *            the application to set
  */
 public void setApplication(Application application) {
  this.application = application;
 }

 /**
  * @param renderKitFactory
  *            the renderKitFactory to set
  */
 public void setRenderKitFactory(RenderKitFactory renderKitFactory) {
  this.renderKitFactory = renderKitFactory;
 }

 /**
  * @see java.lang.Object#toString()
  */
 @Override
 public String toString() {
  StringBuilder builder = new StringBuilder();
  builder.append("FactoryData [application=");
  builder.append(this.application);
  builder.append(", renderKitFactory=");
  builder.append(this.renderKitFactory);
  builder.append("]");
  return builder.toString();
 }
}

7. MockPDFFileSaver

This is a simple Seam component to demonstrate the PDF export function
package org.example.pdf;

import java.io.FileOutputStream;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;

@Name("mockPDFFileSaver")
public class MockPDFFileSaver {

 @In(create = true)
 PdfExporter pdfExporter;

 /**
  * Saves the PDF generated by the pageName to the file denoted by the
  * fileName
  * 
  * @param fileName
  *            the file name
  * @param pageName
  *            the page name
  */
 public void savePDF(String fileName, String pageName) {
  byte[] pdfBytes = pdfExporter.pdfExport(pageName);
  try {
   FileOutputStream fos = new FileOutputStream(fileName);

   fos.write(pdfBytes);

   fos.close();
  } catch (Exception e) {

   e.printStackTrace();
  }
 }

}

8. PDFExporter

The pdf export functionality is implemented in this Seam component. This works by rendering the given view into a mock context. The DocumentStore then contains the rendered pdf. The real question is what the id of the generated pdf. The DocumentStrore is a Conversation scoped component so basically the Id will be started from 1 when a new conversation starts. But for sure, the Id can be queried, in the case more document is generated per conversation.
package org.example.pdf;

import java.io.ByteArrayOutputStream;

import org.jboss.seam.annotations.Name;
import org.jboss.seam.document.DocumentData;
import org.jboss.seam.document.DocumentStore;
import org.jboss.seam.faces.Renderer;

@Name("pdfExporter")
public class PdfExporter {
 
 /**
  * returns the byte array of the PDF file generated using the page xhtml 
  * @param page the page name of the xhtml for the pdf
  * @return the byte array containing the PDF file data
  */
 public byte[] pdfExport(String page) {

  byte[] pdfBytes = null;

  EmptyFacesContext emptyFacesContext = null;

  try {

   emptyFacesContext = new EmptyFacesContext();

   try {

    Renderer renderer = Renderer.instance();
    renderer.render(page);
    
    DocumentStore store = DocumentStore.instance();

    String nextId = store.newId();
    long docId = Long.parseLong(nextId) - 1;

    DocumentData data = store.getDocumentData("" + docId);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    data.writeDataToStream(baos);

    pdfBytes = baos.toByteArray();

   } catch (Exception e) {
    e.printStackTrace();
   }
  } catch (Exception e) {
   e.printStackTrace();

  } finally {
   emptyFacesContext.restore();
  }

  return pdfBytes;
 }

}

1 comment:

  1. I'm really enjoying the theme/design of your site. Do you ever run into any web browser compatibility problems? A handful of my blog readers have complained about my website not operating correctly in Explorer but looks great in Chrome. Do you have any tips to help fix this problem?
    Also see my webpage - Scott Tucker

    ReplyDelete