Monday, March 18, 2013

Tapestry5 and JasperReports

Creating "reports" are a fact of life for developers. It's sucky work but necessary. With my Tapestry5 application, I decided to use JasperReports for the following reasons:
  1. It's free
  2. It's easy to use
  3. It has a nice plugin that works inside Netbeans: iReport
To get started, you have to add JasperReports dependency in your POM.xml:

 <dependency>  
   <groupId>net.sf.jasperreports</groupId>  
     <artifactId>jasperreports</artifactId>  
   <version>5.0.1</version>  
 </dependency>  
 <dependency>  
   <groupId>commons-collections</groupId>  
     <artifactId>commons-collections</artifactId>  
   <version>3.2.1</version>  
 </dependency>  

The commons-collections might be needed to fix a transitive dependency error that you might get with Tapestry5 and JasperReports.

The next part is the report template (jrxml) needs to be created. I just put it template in the WEB-INF folder so to access it is: /WEB-INF/reports/report1.jrxml

The report template is a rather rich XML file with a lot of moving parts but it shouldn't be that hard since we do have the iReport plugin to "visually" create the reports. Here is an example: test_jasper.jrxml. And here is a full working example.

The next piece of the puzzle is a custom Tapestry5 StreamResponse for PDF files. The idea is after we generate the report, we are going to send it to the client as a PDF file so they can print it.

 import java.io.IOException;  
 import java.io.InputStream;  
 import org.apache.tapestry5.StreamResponse;  
 import org.apache.tapestry5.services.Response;  
 /**  
  *  
  * @author jaypax  
  */  
 public class PDFStreamResponse implements StreamResponse {  
     private InputStream is;  
     private String filename="default";  
     /**  
      * Constructor  
      * @param is  
      * @param args (http://docs.oracle.com/javase/1.5.0/docs/guide/language/varargs.html  
      */  
     public PDFStreamResponse(InputStream is, String... args) {  
         this.is = is;  
         if (args != null) {  
             this.filename = args[0];  
         }  
     }  
     public String getContentType() {  
         return "application/pdf";  
     }  
     public InputStream getStream() throws IOException {  
         return is;  
     }  
     public void prepareResponse(Response arg0) {  
         arg0.setHeader("Content-Disposition", "attachment; filename="  
                 + filename + ".pdf");  
     }  
 }  

Then we can now add a event handler for that button when clicked sends the report. In my scenario, its inside a onSubmit event. This is on a page class that's populated with services injected via Tapestry's IOC. I also injected the report form as an asset.

    @Property
    private int id;

    @Inject
    @Path(value = "context:WEB-INF/reports/report1.jrxml")
    private Asset ReportForm;
    ....
    ....
    ....
    public StreamResponse onSubmitFromReportForm() throws IOException,JRException{
        InputStream is = ReportForm.getResource().openStream();
       
        JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(someDAO.findAllEntriesById(id));
        
        Map reportParams = new HashMap();
        reportParams.put("TITLE", "SOME REPORT TITLE");

        JasperDesign reportDesign = JRXmlLoader.load(is);
        JasperReport reportCompiled = JasperCompileManager.compileReport(reportDesign);
        JasperPrint reportPrinted = JasperFillManager.fillReport(reportCompiled, reportParams,ds);
           
        ByteArrayInputStream bais = new ByteArrayInputStream(JasperExportManager.exportReportToPdf(reportPrinted));
        
        return new PDFStreamResponse(bais, "Some Report");
    }

Notice the PDFStreamResponse as the return object, without that then browser will mishandle the return stream.

Turning this into a service shouldn't be that hard which I will probably do in a coming iteration.