This is version 8.
It is not the current version, and thus it cannot be edited.
[Back to current version]
[Restore this version]
A HowTo for generating PDF documents using Apache FOP in the AppFuse architecture.
About this Tutorial
This tutorial will show you how to create a Servlet that can generate a PDF document with data retrieved from our UserDAO object, and a JUnit Test (using StrutsTestCase) that will demonstrate the different stages of the PDF generation process.
The FOP process I am going to describe is producing XML from a POJO entity, passing that XML through an XSL stylesheet to produce a FO XML document, then passing the FO document through an XSLT processor to produce the PDF document.
Please note, the resulting PDF document does not look very pretty. This is because the XSL stylesheet is very basic and does not really apply much FO formatting tags/attributes. It would be very easy to modify the XSL to include images, bold, grey background cells etc.. but this is best left for the FOP documentation to describe.
Table of Contents
- Create an InputSource for your User POJO
- Create XMLReader for your User POJO
- Create XSL stylesheet to produce FO
- Modify User Service Junit test
- Run User Service Junit test
- Create a servlet to handle the requests
- Modify JSP to call the servlet
- Environment changes needed
- Troubleshooting
Now let's write our UserInputSource object. This class is used to wrap a SAXSource around our User object, which is then used for generating the XML document in Stage .
In src/service/**/fop, create a UserInputSource.java file with the following contents:
package org.appfuse.fop;
import org.appfuse.model.User;
import org.xml.sax.InputSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.Source;
public class UserInputSource extends InputSource {
private User user;
/**
* Constructor for the UserInputSource
* @param user The User object to use
*/
public UserInputSource(User user) {
this.user = user;
}
/**
* Returns the user.
* @return User
*/
public User getUser() {
return user;
}
/**
* Sets the user.
* @param user The user to set
*/
public void setUser(User user) {
this.user = user;
}
/**
* Resturns a Source object for this object so it can be used as input for
* a JAXP transformation.
* @return Source The Source object
*/
public Source getSource() {
return new SAXSource(new UserXMLReader(), this);
}
}
|
Create XMLReader for your User POJO
Now we need to create the XMLReader which will generate SAX events from our User object. This object should write any data from the User object, we might want to publish in our PDF document.
This class depends on two helper classes, AbstractObjectReader and EasyGenerationContentHandlerProxy that need to be added to src/service/**/util. These files are downloadable at the end of this tutorial.
package org.appfuse.fop;
//Java
import java.io.IOException;
import java.util.Iterator;
import java.util.Set;
//SAX
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.apache.log4j.Logger;
// Appfuse imports
import org.appfuse.model.User;
import org.appfuse.model.Role;
import org.appfuse.util.AbstractObjectReader;
/**
* XMLReader implementation for the User class. This class is used to
* generate SAX events from the User class.
*/
public class UserXMLReader extends AbstractObjectReader {
// User elements
public static final String USER = "user";
public static final String FIRST_NAME = "firstName";
public static final String LAST_NAME = "lastName";
// Role elements
public static final String ROLE = "role";
public static final String ROLE_NAME = "roleName";
protected Logger log = Logger.getLogger(getClass());
/**
* @see org.xml.sax.XMLReader#parse(InputSource)
*/
public void parse(InputSource input) throws IOException, SAXException {
if (input instanceof UserInputSource) {
parse(((UserInputSource)input).getUser());
} else {
throw new SAXException("Unsupported InputSource specified. Must be a UserInputSource");
}
}
/**
* Starts parsing the User object.
* @param user The object to parse
* @throws SAXException In case of a problem during SAX event generation
*/
public void parse(User user) throws SAXException {
if (user == null) {
throw new NullPointerException("Parameter user must not be null");
}
if (handler == null) {
throw new IllegalStateException("ContentHandler not set");
}
//Start the document
handler.startDocument();
//Generate SAX events for the User
processUser(user);
//End the document
handler.endDocument();
}
/**
* Generates SAX events for a User object.
* @param user User object to use
* @throws SAXException In case of a problem during SAX event generation
*/
protected void processUser(User user) throws SAXException {
if (user == null) {
throw new NullPointerException("Parameter user must not be null");
}
if (handler == null) {
throw new IllegalStateException("ContentHandler not set");
}
handler.startElement(USER);
processUserDetails(user);
processRoles(user);
handler.endElement(USER);
}
protected void processUserDetails(User user) throws SAXException {
handler.element(FIRST_NAME, user.getFirstName());
handler.element(LAST_NAME, user.getLastName());
}
protected void processRoles(User user) throws SAXException {
Set roles = user.getRoles();
if (roles!=null) {
Iterator iter = roles.iterator();
while (iter.hasNext()) {
Role role = (Role)iter.next();
handler.startElement(ROLE);
handler.element(ROLE_NAME, role.getName());
handler.endElement(ROLE);
}
}
}
}
|
Create XSL stylesheet to produce FO
The xsl stylesheet will process the XML generated by the UserXMLReader and produce a FO document. The FO document is processed by Apache FOP to produce the PDF document. Therefore, the XSL stylesheet is where we can modify the FO formatting and style the PDF document.
Place the user2fo.xsl stylesheet in src/web/**/fop/user2fo.xsl.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format"
exclude-result-prefixes="fo">
<xsl:output method="xml" version="1.0" omit-xml-declaration="no" indent="yes"/>
<!-- ================== -->
<!-- root element: user -->
<!-- ================== -->
<xsl:template name="user" match="user">
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
<fo:layout-master-set>
<fo:simple-page-master master-name="simpleA4" page-height="29.7cm"
page-width="21cm" margin-top="2cm" margin-bottom="2cm" margin-left="2cm" margin-right="2cm">
<fo:region-body/>
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="simpleA4">
<!-- START MAIN BODY -->
<fo:flow flow-name="xsl-region-body">
<!-- Title Text -->
<fo:block font-weight="bold" text-align="start" space-after="12pt">
User Report
</fo:block>
<!-- START MAIN TABLE -->
<fo:block>
<fo:table table-layout="fixed" border="solid black 1px">
<fo:table-column column-width="4.3cm" column-number="1"/>
<fo:table-column column-width="4.3cm" column-number="2"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell font-weight="bold" padding="2px" border="solid black 1px">
<fo:block>
First name
</fo:block>
</fo:table-cell>
<fo:table-cell padding="2px" border="solid black 1px">
<fo:block>
<xsl:value-of select="firstName"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell font-weight="bold" padding="2px" border="solid black 1px">
<fo:block>
Last name
</fo:block>
</fo:table-cell>
<fo:table-cell padding="2px" border="solid black 1px">
<fo:block>
<xsl:value-of select="lastName"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
<!-- When the apply-templates is executed by the XSLT processor
all matching child nodes (from the XML document are processed).
So, in our example, any role elements nested within a User
will be processed by the xsl:template below that matches on
role -->
<xsl:apply-templates/>
</fo:table-body>
</fo:table>
</fo:block>
<!-- END MAIN TABLE -->
</fo:flow>
<!-- END MAIN BODY -->
</fo:page-sequence>
</fo:root>
</xsl:template>
<!-- =================== -->
<!-- child element: role -->
<!-- =================== -->
<!-- If there are multiple role elements nested within the User then
a tale-row per role end up in the PDF doc -->
<xsl:template match="role">
<fo:table-row>
<fo:table-cell font-weight="bold" padding="2px" border="solid black 1px">
<fo:block>
Role name
</fo:block>
</fo:table-cell>
<fo:table-cell padding="2px" border="solid black 1px">
<fo:block>
<xsl:value-of select="roleName"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
</xsl:stylesheet>
- You could place your xsl stylesheet in your service layer if you wanted. We deploy the stylesheet to the WEB-INF/classes dir inside your webapp and use ServletContextResource to load it, so as long as it is visible to your webapp it does not really matter where you put it. But if you move it, you will need to modify the path used in our servlet we write in a later stage.
NOTE: If you want a section for role names output in your final PDF and we had a user that does not have any role names (for example). Your UserXMLReader should write some empty elements for the role names. Else when the apply templates is called, there will be no matching elements entitled 'role' and therefore, no role name content will be displayed in the resulting PDF document.
Modify User Junit test
To support the execution of the User service level test, we need another helper file. This file (attached at the end) is called FOPHelper.java and lives in src/service/**/utils.
Add the following imports and method to UserManagerTest.java:
import java.io.File;
import java.io.OutputStream;
import java.io.IOException;
//JAXP
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.Source;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.sax.SAXResult;
//Avalon
import org.apache.avalon.framework.ExceptionUtil;
import org.apache.avalon.framework.logger.ConsoleLogger;
import org.apache.avalon.framework.logger.Logger;
//FOP
import org.apache.fop.apps.Driver;
import org.apache.fop.apps.FOPException;
import org.apache.fop.messaging.MessageHandler;
//Appfuse
import org.appfuse.fop.UserInputSource;
import org.appfuse.utils.FOPHelper;
public void testGeneratePDF() throws Exception {
try {
User testData = new User();
testData.setUsername("tomcat");
testData.getRoles().add(new Role("user"));
// set expected behavior on dao
userDAO.expects(once()).method("getUser")
.with(eq("tomcat")).will(returnValue(testData));
user = userManager.getUser("tomcat");
File baseDir = new File(".");
File xslFile = new File(baseDir, "./src/web/org/appfuse/webapp/fop/user2fo.xsl");
if (xslFile.exists()) {
log.info("xslFile exists");
}
File pdfFile = new File(baseDir, "user.pdf");
File xmlFile = new File(baseDir, "user.xml");
log.debug("Input: a User object");
log.debug("Stylesheet: " + xslFile);
log.debug("Output: PDF (" + pdfFile + ")");
log.debug("Transforming...");
FOPHelper fopHelper = new FOPHelper();
fopHelper.convertUser2PDF(user, xslFile, pdfFile);
fopHelper.convertUser2XML(user, xmlFile);
}
catch (Throwable e) {
log.error("Error [" + e.getMessage() + "]", e);
}
}
|
Run User Junit test
Now execute ant test-service. The testGeneratePDF method will execute and generate the 3 files involved in the PDF generation
process. user.xml, user.fo and user.pdf (all three attached at the end of this tutorial).
You should see output:
[junit] [appfuse] INFO [main] UserManagerTest.testGeneratePDF(83) | xslFile exists
[junit] [appfuse] DEBUG [main] UserManagerTest.testGeneratePDF(88) | Input: a User object
[junit] [appfuse] DEBUG [main] UserManagerTest.testGeneratePDF(89) | Stylesheet: ././src/web/org/appfuse/webapp/fop/user2fo.xsl
[junit] [appfuse] DEBUG [main] UserManagerTest.testGeneratePDF(90) | Output: PDF (./user.pdf)
[junit] [appfuse] DEBUG [main] UserManagerTest.testGeneratePDF(91) | Transforming...
Create a servlet to handle the requests
This servlet will take a userName from the HttpRequest object, call the UserDAO to load a User. It then calls UserXmlReader to generate the XML, does the XSLT transformation, and generates the PDF document. During this process, no files are saved to disk.
Add this servlet so src/web/**/action/
package org.appfuse.webapp.action;
import org.appfuse.model.User;
import org.appfuse.dao.UserDAO;
import org.appfuse.fop.UserInputSource;
import org.apache.avalon.framework.logger.ConsoleLogger;
import org.apache.avalon.framework.logger.Logger;
import org.apache.fop.apps.Driver;
import org.apache.fop.apps.XSLTInputHandler;
import org.apache.fop.apps.TraxInputHandler;
import org.apache.fop.messaging.MessageHandler;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.ServletContextResource;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
/**
* Implementation of <strong>HttpServlet</strong> that is used
* to autogenerate a PDF document.
*
* <p><a href="FopServlet.java.html"><i>View Source</i></a></p>
*
* @author <a href="mailto:mailto:[email protected]">Ben Gill</a>
* @version $Revision: 1.1 $ $Date: 2005/03/12 17:46:09 $
*
* @web.servlet
* display-name="Fop Servlet"
* load-on-startup="1"
* name="fop"
*
* @web.servlet-mapping
* url-pattern="/fop/*"
*/
public class FopServlet extends HttpServlet {
private Logger logger = null;
private ApplicationContext ctx = null;
private ServletContext servletContext = null;
public void init() throws ServletException {
this.servletContext = getServletContext();
this.ctx =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
}
/**
* Route the user to the execute method
*
* @param request The HTTP request we are processing
* @param response The HTTP response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet exception occurs
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
execute(request, response);
}
/**
* Route the user to the execute method
*
* @param request The HTTP request we are processing
* @param response The HTTP response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet exception occurs
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
execute(request, response);
}
/**
* Process the specified HTTP request, and create the corresponding HTTP
* response (or forward to another web component that will create it).
*
* @param request The HTTP request we are processing
* @param response The HTTP response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet exception occurs
*/
public void execute(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
ByteArrayOutputStream out = null;
try {
String userName = request.getParameter("userName");
UserDAO dao = (UserDAO)ctx.getBean("userDAO");
User user = dao.getUser(userName);
Driver driver = new Driver();
Logger logger = new ConsoleLogger(ConsoleLogger.LEVEL_INFO);
driver.setLogger(logger);
MessageHandler.setScreenLogger(logger);
driver.setRenderer(Driver.RENDER_PDF);
// Start with a bigger buffer to avoid too many buffer reallocations
out = new ByteArrayOutputStream(16384);
driver.setOutputStream(out);
ServletContextResource resource =
new ServletContextResource(servletContext,
"/WEB-INF/classes/org/appfuse/webapp/fop/user2fo.xsl");
File xslFile = resource.getFile();
//Setup XSLT
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer(new StreamSource(xslFile));
//Setup input for XSLT transformation
Source src = new UserInputSource(user).getSource();
//Resulting SAX events (the generated FO) must be piped through to FOP
Result res = new SAXResult(driver.getContentHandler());
//Start XSLT transformation and FOP processing
transformer.transform(src, res);
byte[] content = out.toByteArray();
response.setContentType("application/pdf");
response.setContentLength(content.length);
response.getOutputStream().write(content);
response.getOutputStream().flush();
} catch (Exception e) {
throw new ServletException(e);
} finally {
if (out!=null) {
out.close();
}
}
}
}
|
Modify JSP to call the servlet
To link to the servlet from the main app. Add the following column to the table in src/web/pages/userList.jsp:
<display:column sort="false" media="html">
<c:url value="/fop" var="fopURL">
<c:param name="userName" value="${users.username}"/>
</c:url>
<a href="<c:out value='${fopURL}'/>">
PDF
</a>
</display:column>
test the call to the servlet
Environment changes needed
Lib dir / lib.properties file changes
You need to download and install fop-0.20.5 into the appfuse lib structure. Ensure these jar files are in your lib/fop-0.20.5 dir:
- avalon-framework-cvs-20020806.jar
- batik.jar
- fop.jar
- xalan-2.4.1.jar
- xercesImpl-2.2.1.jar
- xml-apis.jar
NOTE: The reason I have listed the jar files above is because I just copied *.jar from the lib directory within the fop distribution. But fop.jar is not actually in the lib directory, it resides in the base directory of the distribution. I missed this, and got compilation errors.
body#pageName element.class { background-color: blue }
In the lib.properties file you should have the following entry:
#
# Fop - http://xml.apache.org/fop/
#
fop.version = 0.20.5
fop.dir=${lib.dir}/fop-${fop.version}
fop.jar=${fop.dir}/fop.jar
properties.xml changes
You need to add to the webapp and service classpath's:
Add this entry:
<fileset dir="${fop.dir}" includes="*.jar"/>
to paths:
<path id="service.compile.classpath">
<path id="web.compile.classpath">
To ensure the fop jar files are in the classpath.
Also, add
<pathelement location="${log4j.jar}"/>
To path
<path id="service.compile.classpath">
build.xml changes
Add this line to the war target inside package-web target:
<lib dir="${fop.dir}" includes="*.jar"/>
Also inside the package-web target add this line:
<include name="**/*.xsl"/>
So you end up with block
<!-- Copy .properties files in src tree to build/web/classes -->
<copy todir="${build.dir}/web/classes">
<fileset dir="src/web">
<include name="**/*.properties"/>
<include name="**/*.xml"/>
<include name="**/*.xsl"/>
</fileset>
</copy>
which ensures the xsl file is deployed.
To ensure the fop jar files are available at runtime.
New files/dir added to AppFuse
- src/web/org/appfuse/webapp/fop/user2fo.xsl
- src/web/org/appfuse/webapp/action/FopServlet.java
- src/service/org/appfuse/fop/UserInputSource.java
- src/service/org/appfuse/fop/UserXMLReader.java
- src/service/org/appfuse/util/AbstractObjectReader.java
- src/service/org/appfuse/util/EasyGenerationContentHandlerProxy.java
Troubleshooting
If you follow these instructions, things should work fine. But here are things to check if
it is not working for you:
- Ensure the xsl file is within your webapps/appfuse/WEB-INF/classes/** runtime directory somewhere
- Ensure the path to load the user2fo.xsl file, specified in FopServlet, is correct (in case you moved it)
- Check the webapps/appfuse/WEB-INF/lib directory to ensure the fop.jar file exist there
Attachments:
|