Java : Reporting in Java using XSL-FO

This page last changed on Feb 22, 2006 by Kees de Kooter

Not really satisfied with the available reporting solution for Java I decided to brew my own solution. It consists of the following layers:

  1. Domain objects (populated using Hibernate)
  2. A Velocity template for generating the xml
  3. An XSL template for generating the XSL-FO
  4. The Apache FOP renderer for generating the PDF

The domain objects are java beans with getter methods for all properties. Velocity can interpret the standard ${} markup . All variable placeholders are replaced with bean property values. The variables can contain members of members. This is the Velocity template:

<?xml version="1.0" encoding="UTF-8"?>
<report>
   <!-- Template for rendering timesheet xml report -->
   <title>${timeSheet.description}</title>
   <customer>${timeSheet.customer.name}</customer>
   employee>${timeSheet.resource.userFullName}</employee>
   <activities>
   #foreach($activity in ${timeSheet.activities})
      <activity>
         <project>${activity.assignment.project.name}</project>
         <date>${activity.activityDate}</date>
         <formattedDate>${dateFormatter.formatDate($activity.activityDate)}</formattedDate>
         <hours>${activity.hours}</hours>
         <issue>${activity.issueId}</issue>
         <description>${activity.description}</description>
         <percentage>${activity.billablePercentage}</percentage>
      <billablehours>${activity.billableHours}</billablehours>
   </activity>
   #end
   </activities>
</report>

The java code triggering Velocity looks like this (exception handling omitted):

Velocity.init();
VelocityContext velocityContext = new VelocityContext();
Template template = Velocity
        .getTemplate("WEB-INF/templates/timesheet.vm");
StringWriter stringWriter = new StringWriter();
velocityContext.put("timeSheet", dataSource);
velocityContext.put("dateFormatter", new Dates());
template.merge(velocityContext, stringWriter);

Here is a snippet of the XSL file for transforming this xml report to the XSL-FO:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" 
   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
   xmlns:fo="http://www.w3.org/1999/XSL/Format">
   
   <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   
   <xsl:decimal-format decimal-separator="," grouping-separator="." name="nl"/>
   
   <xsl:template match="/">
      <fo:root>
         <fo:layout-master-set>
            <fo:simple-page-master margin="1.5cm" page-width="21cm" page-height="29.7cm" master-name="first">
               <fo:region-before>
               
               </fo:region-before>
               <fo:region-body/>
            </fo:simple-page-master>
         </fo:layout-master-set>
         <fo:page-sequence master-reference="first">
            <fo:flow flow-name="xsl-region-body" font-family="Helvetica" font-size="10pt">
            
               <fo:block>
                  <fo:external-graphic src="http://www.boplicity.nl/images/weblogo.png"/>
               </fo:block>
               
               <fo:block font-weight="bold" font-size="11pt" padding-before="1cm" padding-after="0.2cm">
                  <xsl:value-of select="/report/customer"/>
               </fo:block>
               <fo:block font-weight="bold" font-size="11pt" padding-before="0.2cm" padding-after="0.2cm">
                  <xsl:value-of select="/report/title"/>
               </fo:block>
               
               <fo:block padding-before="0.6cm" padding-after="0.2cm">
                  <xsl:text>Medewerker: </xsl:text><xsl:value-of select="/report/employee"/>
               </fo:block>               
               
               <xsl:apply-templates select="/report/activities"/>
            </fo:flow>
         </fo:page-sequence>
      </fo:root>
   </xsl:template>

Next the the xml is transformed to XSL-FO and rendered by FOP. The resulting byte array can either be piped to a file or sent to the browser.

private byte[] renderTimeSheet(Integer id) {
    String xml = timeSheetDao.findByIdAsXml(id);
      
    // Transform to fo and subsequently pdf
    Driver driver = new Driver();
    driver.setRenderer(Driver.RENDER_PDF);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    driver.setOutputStream(outputStream);
      
    try {
        // Setup JAXP using identity transformer
        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer transformer = factory.newTransformer(new StreamSource(
                getClass().getResourceAsStream(
                        "/com/bop/metronome/reports/timesheet.xsl")));
            
        // Setup input stream
        Source source = new StreamSource(new StringReader(xml));
            
        // Resulting SAX events (the generated FO) must be piped through to
        // FOP
        Result result = new SAXResult(driver.getContentHandler());
            
        // Start XSLT transformation and FOP processing
        transformer.transform(source, result);
    } catch (Exception e) {
        log.error(e.toString());
    }
    return outputStream.toByteArray();
}

And all of this coded by hand, no graphical tools used whatsoever. The toughest job was coding the FO. There is a very extensive W3C specification available (http://www.w3.org/TR/xsl/), but no comprehensive 'cookbook' manual, nor GUI tools. So every tiny little layout aspect that I wanted to render took a lot of research time.

Another issue is the performance of FOP. It is quite slow. There is a version coming up however, promising to be much faster: http://xmlgraphics.apache.org/fop/0.91/index.html.