Monday, May 16, 2011

Filling a table in Jasper Reports

You wouldn't think it, but figuring out how to fill a table with data in Jasper Reports (JR) was actually more difficult than it sounds. Poor documentation, bad/incorrect/plain stupid examples and forum posts that have been left for years with no answer! Due to library constraints on this project, this example is with Jasper Report/iReport 3.4.7 and YMMV with other versions.

Pictorial example of a table in a JR report


Say that you're producing a report with a table of Customers that is embedded within a report with other data (see image above). JR treats the table as a subreport (but with different XML tags) which means that data you fill the parent/master report with isn't instantly available to the table. This is a caveat that isn't intuitive to find out/understand until you find out that the table is a subreport. To make matters worse iReport assumes that you're getting your data either from a straight JDBC connection, or populates your table's <dataSourceExpression> with a JREmptyDataSource which will populate your table fields with null.

^How helpful^.

If you're in any sort of Enterprise system you'll no doubt have DAOs, and different models (domain, DTOs, etc) to feed into your reporting code, so you'll need to strip out the empty data source iReport sticks in your template.

Fortunately there is a JRBeanCollectionDataSource class that maps field names in the report template to properties in the data using the Java Beans naming convention. The last step is actually make your data available to the table. This is a combination of fixing the template and writing a reporting DTO class.

Firstly a field in the report will need to be a Java Collection type. I didn't have much success with non JSE collections, and it's better to code to interfaces anyway.
<field name="customers" class="java.util.List"/>
The DTO will need to provide an instance of that collection type with a getter that will match the name of the field in the template. Using the example in the image
public class CustomerList  {
public List<Customer> getCustomers() { ... }
}
Then for each field placeholder in the table, if it maps to a property getter in the Customer object then that property value will get substituted into the report.

The final configuration is to tell the table to source its data from the collection.
<dataSourceExpression><![CDATA[new net.sf.jasperreports.engine.
data.JRBeanCollectionDataSource($F{customers})]]>
</dataSourceExpression>
Then when the report is filled with an instance of CustomerList that you've prepared earlier, the report engine will iterate over the Collection and fill each row of the table.

Once you've done some digging/filtering and realised how JR does it's tables it's actually pretty easy/plain obvious. However because of the afore mentioned reasons (incorrect examples sending one down the garden path) it can be time consuming and frustrating. Given that a table is a core requirement of most reports wanted by a business it would make sense to me to make putting a table in a report to be so dead simple.

8 comments:

  1. Thats a great explanation. It would be more helpful if you could explain with a sample report.

    ReplyDelete
  2. Great help. But you didn't mention how to get the CustomerList class into the report.
    I've done it by putting into a List as well and passing this as a JRBeanCollectionDataSource.

    Second problem I had, was that I couldn't access the Customer's fields in the table. I also had to register them:



    ReplyDelete
  3. <subDataset name="Table Dataset 3" uuid="110be11e-dd1f-422b-b9b3-d9cb5dd7a31e">
    <field name="content" class="java.lang.String"/>
    </subDataset>

    ReplyDelete
  4. Hi

    I added the table as instructed above but I am getting this exception.

    This is because we are using JRBeanCollectionDataSource while table uses subreport evalution which has the sql connection object.

    ---------------------------------------------
    java.lang.ClassCastException: net.sf.jasperreports.engine.data.JRBeanCollectionDataSource cannot be cast to java.sql.Connection
    at net.sf.jasperreports.engine.fill.JRFillSubreport.evaluateSubreport(JRFillSubreport.java:369)
    ----------------------------------------

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  5. Please, could you provide the complete code? I'm lost in where i should put the commands you showed us.

    ReplyDelete
  6. Hi. Great explanation, but I too am stuck as to how to pass the list of customers into the report using the JasperFillManager:

    JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, dataSource);

    How do I set-up the datasource, as it only seems to accept collections as opposed to a List object. Thanks!

    ReplyDelete
  7. This explanation helped me really a lot. And usability of iReport tool did not make things easier. Thanks!

    ReplyDelete