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:
- Domain objects (populated using Hibernate)
- A Velocity template for generating the xml
- An XSL template for generating the XSL-FO
- 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.