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.

Tuesday, March 12, 2013

Timed redirect without the php, javascript fuzz

Having worked on a few MVC projects in different languages like PHP, C# and Java, there always that "thank you" or "success" page you show after successful operation like an insert or update. Often these pages are automatically redirected to some URL after a set amount of time, normally 10 - 15 seconds. Normally, I use javascript; Something like this:

 <script type="text/JavaScript">  
 <!--  
 setTimeout("location.href = 'http://google.com';",1000);  
 -->  
 </script>  

It works but then I found out there's an easier way to do this via a HTML meta tag. Wikipedia explains it here. This one just automatically redirects you to google.com after 10 seconds.

<meta http-equiv="refresh" content="10;URL='http://google.com/'">  

It does have drawbacks but it works and I don't have to really concern myself if the browser's javascript is on or off.

Tuesday, March 5, 2013

Tapestry 5.3 and the case of Twitter-Bootstrap disabled attributes

Making Tapestry5 use Twitter-Bootstrap as its style is quite easy but not everything is as it seems.

Bootstrap as some nice form style rules. It's quite easy to add them into normal tapestry5 common components. Like if you need some a textfield to look like a "search-query" field then you would write something like:

// Rounded corners
<input t:type="textfield" t:id="keyword" class="search-query" placeholder="Callslip ID"/>  

or if you need a page or event link to look like a button:

// Tapestry5 eventlink styled as a button via Bootstrap
<a t:type="eventlink" t:event="add" class="btn brn-primary" href="#">Add 1</a>  

But I have recently came across a scenario where I need to display a textfield with data that's by default is uneditable but under certain conditions can be edited. Reading the bootstrap documentation, you have to do something like this:

// As shown in the Bootstrap documentation
<input class="input-xlarge" id="disabledInput" type="text" placeholder="Disabled input here..." disabled>

OK, that should be easy to convert into tapestry5 component and use javascript to enable or disable the field. When done via jquery, removeAttr() or prop(). There's an equivalent to add an attribute.

// Tapestry5 is not happy about disabled attribute
<input class="input-xlarge" id="disabledInput" t:type="textfield" placeholder="Disabled input here..." disabled>

And that's this is where the problem crops up. Tapestry5 doesn't like any unpaired attributes. I'm talking about the disabled attribute at the end of the input element above. Fortunately, Peter Wendorff in the Tapestry mailing-list pointed me to a html microsyntax spec document explaining this unique nuance of html. You have to do a disabled="true" attribute to makeTapestry5 happy.

// How Tapestry5 wants it done
<input disabled="true" class="input-xlarge" t:id="name" t:type="textfield" placeholder="${name}" />
Other ways to write this doesn't work, even if the spec doc says its OK.

  • disabled="" -> doesn't work
  • disabled="disabled" or disabled='disabled' or disabled=disalbed -> also doesn't work
  • disabled -> obviously doesn't work