Integrate JasperReports with your existing iBATIS implementation The core task of many Java applications is to retrieve data and display it, sometimes in sophisticated print- or Web-based reports. Luckily for Java developers, two popular open source solutions work especially well together to help you accomplish this task. The iBATIS Data Mapper framework provides a simple XML-based mechanism for linking Java objects to a data repository. JasperReports is a full featured Java reporting library that you can embed in your applications. Put the two together and you have a winning combination for producing scalable, easy-to-maintain reports.JasperReports is an open source Java reporting library that is quickly gaining popularity as a viable alternative to costly proprietary reporting solutions. With any reporting solution, getting the data to the reporting engine is the most basic implementation concern. Unfortunately, Jasper poses a small problem in this area.Most Java applications use some type of data-fetching framework for data mapping and dynamic SQL generation, such as the iBatis Data Mapper Framework. Jasper’s default mechanism for retrieving and managing data isn’t flexible enough to leverage existing data mapping frameworks, however. Instead, you pass the Jasper engine a connection to your database, and it uses SQL queries embedded in an XML-based report template to populate the report. Although simple to implement, this mechanism ties you to the Jasper template’s embedded SQL. Besides, who wants to add yet another moving piece to an already complex application? You would be better off leveraging the existing data framework and just letting Jasper handle report generation.In this article you’ll learn how to integrate JasperReports and the iBATIS Data Mapper Framework for just such a solution. I’ll walk through two simple scenarios where the goal is to integrate Jasper and iBATIS for report generation. The first scenario applies to iBATIS implementations that use iBATIS’s data capabilities to return a list of Java beans. This scenario doesn’t require you to write any custom code. The Jasper framework contains supporting classes that allow the data returned from iBATIS to fill a Jasper report.For the second scenario — a more basic uses of iBATIS that returns a list of java.util.Map objects — you’ll create a custom Jasper data source to feed a Jasper report. In addition to working with the Jasper framework classes, for both exercises you’ll use the iReport report designer, which eases and accelerates the process of creating template files in Jasper. Running the examplesThis article’s example code generates a simple monthly sales report for each type of implementation I cover. The data for the reports is retrieved from an embedded Apache Derby database via the iBATIS Data Mapper framework. The examples are built into a JSF/Spring-based Web application that runs in the same JVM as Derby. I’ve provided an Ant script for building that WAR file — just execute the buildWar task to compile content and build it. You’ll need Tomcat 5.5x to deploy and run the examples. You’ll also need the Abode Acrobat Reader Web browser plug-in to view the report output.Getting the iBATIS data into JasperUsing iBATIS to return a list of a specific type of Java beans (I’ll call this a return list) is much tidier than using the framework to return a list of java.util.Map objects. Most developers using iBATIS take this approach to data mapping, and it happens to make integration with Jasper a snap.The Jasper framework provides a JRDataSource implementation that your application can use to fill a report template with data from an iBATIS return list. The JRBeanCollectionDataSource class is constructed from a collection of Java beans and knows how to loop through the collection and access the beans’ properties. Listing 1 shows how you can pass an instance of a JRBeanCollectionDataSource when calling on the Jasper engine to populate a report. Listing 1. Populating a report with JRBeanCollectionDataSource/* Helper method to create a fully populated JasperPrint object from an list of Java beans */ private JasperPrint fillReport (List dataList) throws JRException { // this map could be filled with parameters defined in the report Map parameters = new HashMap(); // make sure the .jasper file (a compiled version of the .jrxml template file) exists String localPath = this.servlet.getServletContext().getRealPath("/"); File reportFile = new File(localPath + "WEB-INF" + File.separator + "monthySales.jasper"); if (!reportFile.exists()) { throw new JRRuntimeException("monthySales.jasper file not found."); } // load up the report JasperReport jasperReport = (JasperReport)JRLoader.loadObject(reportFile); // pass JRBeanCollectionDataSource (which is populated with iBATIS list) to fillReport method return JasperFillManager.fillReport (jasperReport, parameters, new JRBeanCollectionDataSource (dataList)); } In Listing 1, you first define the parameters map, which is the mechanism for passing parameter values to the report at runtime. For example, you could define a parameter named REPORT_TITLE in the report template and pass the value for this parameter to the report by simply adding the key/value pair to the map (e.g., Key=REPORT_TITLE, Value=Sale Report). The parameters map is passed to the fillReport method. The next portion of code loads a compiled Jasper template (.jasper) file. Finally, the static fillReport method is called. It does the actual work of building the report and returns a JasperPrint object, which is passed to a specific type of Jasper exporter to write out the report. The example code for this article uses a JRPdfExporter to write the report to PDF format (see the PdfServlet.java class).Although this mechanism lets the Jasper framework link with iBATIS, you might need to modify the Java beans that iBATIS populates, depending on your report’s requirements. Jasper’s field objects know how to work with the common JDBC mapping types. For example, Jasper stores an Oracle numeric field type as a java.math.BigDecimal object. Any of the iBATIS bean properties that you plan to use in a report must map to one of Jasper’s defined field types. You should select your report field types carefully, because the formatting and expression capabilities are better in some types than in others. For example, a BigDecimal type is more convenient to work with than a String when you’re trying to apply a currency format.Creating a bean-driven reportThe simplest way to create a Jasper report is to use the iReport designer, which is part of the JasperSoft Business Intelligence Suite. iReport offers a full-featured GUI for creating a report layout and shields you from having to hand-code Jasper report templates (.jrxml files). If you are new to iReport, I suggest you work through the iReport “Getting Started” section and possibly a few of the tutorials before proceeding with this article’s examples. Add a helper methodBefore you start creating the report in iReport, you need to add a helper method to the iBATIS-populated bean to return a collection of populated beans, as shown in Listing 2. You’ll use this helper method when setting up the iReport data source, which you’ll use to test the report during construction. By using the iBATIS bean directly in iReport, you avoid the need for an active database connection and ensure that the field types are mapped correctly between the bean and the report.Listing 2. createBeanCollection() helper method used to retrieve a populated bean collectionpublic static List createBeanCollection () { // simulated collection returned from iBATIS List list = new ArrayList (); // Java bean populated with row data by iBATIS MonthlySalesBean msb = new MonthlySalesBean (); msb.setEmployeeID(1); msb.setFirst("John"); msb.setLast("Doe"); msb.setTotal(new BigDecimal ("1600.50")); list.add (msb); return list; } Make sure that the bean containing the createBeanCollection method compiles. Then create a JAR file with just the single bean .class file. Copy the JAR into the lib folder under the iReport install directory and restart iReport (if you happen to have it open).With the bean class containing the createBeanCollection method now in iReport’s classpath, open a new Jasper report (iReport menu > File > New Document). Then select the Data > Connection / Datasources menu. Create a new data source of type “JavaBeans set data source” and populate the necessary information, as shown in the example screen sequence in Figure 1. You might need to specify a package structure if you did not use the default package as in the downloadable example code. Figure 1. Screens used for creating a Java-bean-driven data source in iReport (click for larger image)iReport has a feature that makes working with a Java bean data source very convenient: you can tell it to inspect the bean’s properties and expose them as data fields available to the report. This saves you from manually creating the fields and helps to ensure the correct mapping between your bean property types and the types available in iReport.Select the Data > Report query menu and then the JavaBean Data Source tab (see Figure 2). You must set the “Class name” field here to the bean containing createBeanCollection that you designated as the “Factory class” when you set the data source (see the third screen in Figure 1). You should be careful not to select properties that map to non-JDBC types, such as a java.util.List (as shown in Figure 2). Although iReport will try to map complex types as report fields, when you try to add those fields to the report you’ll get an error stating that it can’t cast to the appropriate type. For example, Figure 2 shows that iReport will expose the sales property (java.util.List) from the MonthlySalesBean, but adding this field to the report will either result in an exception when you compile the report template or cause unpredictable output.Figure 2. Generating Jasper fields from a Java bean in iReportThe Jasper engine accesses bean properties much like the JavaServer Pages Standard Tag Library (JSTL) or other frameworks that have text-to-bean property-mapping abilities. The get is dropped off the getter method’s name, and the first following letter (which is normally uppercase if you’re following the Java standard) is changed to lowercase. For example, the getLastName getter method would change to lastName when referenced in the Jasper report template. You can even use the same convention used in other frameworks (such as JSTL or Struts) to access nested properties. Notice in Figure 2 how the amount property is nested in the LastSale bean and accessed in the report as lastSale.amount. Figure 3 shows the generated PDF output for the report.Figure 3. The generated reportHandling basic iBATIS return typesIf iBATIS doesn’t use Java beans as return objects in your application, then it’s most likely returning lists of java.util.Map objects. Performing Jasper integration by working with maps is possible but requires a few additional steps and considerations. Listing 3 shows an iBATIS SELECT statement that returns a list of java.util.Map objects that represent returned records.Listing 3. iBATIS SELECT statement configured to return a list of java.util.Map objects<select id="salesReportSQL" parameterClass="java.util.Map" resultClass="java.util.HashMap"> SELECT E.EMPLOYEE_ID "ID", E.FIRST_NAME "FIRST", E.LAST_NAME "LAST", MS.TOTAL_SALES "TOTAL" FROM EMPLOYEE E, MONTHLY_SALES MS WHERE E.EMPLOYEE_ID = MS.EMPLOYEE_ID AND MS.MONTH = #selectedMonth# </select> The resultClass attribute in Listing 3 tells iBATIS to return the list of map objects. It’s important to understand that the key value in the return map comes directly from the SELECT statement, so a key value of TO_CHAR(MS.TOTAL_SALES) might not be easy to work with in the report. Aliasing the field names is not necessary but might help in creating the report if your field names aren’t exactly human readable, or use a function, or concatenate several fields. It’s also a good idea to decide on your return field names’ case, because a map’s key values are stored as a java.lang.Object, and case-insensitive comparison isn’t possible without looping through all the keys (which is inefficient). Custom JRDataSource implementationWhen you deal with maps as return objects, the glue between iBATIS and Jasper is a custom implementation of the JRDataSource interface. The custom implementation is then passed to JasperFillManager‘s fillReport method and used by the Jasper engine to populate the report fields. The code for filling the report is almost identical to Listing 1. The only difference is that you pass your custom JRDataSource implementation — instead of the JRBeanCollectionDataSource class — in the FillManager‘s fillReport method.The custom JRDataSource‘s job is to loop through the iBATIS result list and feed the Jasper engine the correct field values when they’re requested. The JRDataSource interface consists of two abstract methods: getFieldValue and next. The getFieldValue method is responsible for retrieving a field value. Its only parameter is a JRField object that contains the properties of the field that the Jasper engine is actively trying to populate. The next method is responsible for forwarding some type of container object storing the rows of report data. Listing 4 shows a custom JRDataSource implementation that loops through a list of map objects.Listing 4. Implementation of custom JRDataSourceimport java.util.ArrayList; import java.util.List; import java.util.Map; import net.sf.jasperreports.engine.JRDataSource; import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.JRField; public class CustomJRDataSource implements JRDataSource { // counter for keeping track of current row private int index = 0; // list of map objects returned from iBATIS private List dataList = null; // map representing current row of data private Map current = null; public CustomJRDataSource (List dataList) { this.dataList = dataList; } public Object getFieldValue(JRField jRField) throws JRException { Object value = null; if (current.get(jRField.getName()) != null) { value = current.get(jRField.getName()); } return value; } public boolean next() throws JRException { if (dataList.size() > 0) { if (index < dataList.size()) { current = (Map) dataList.get(index); index++; return true; } } return false; } } In Listing 4, the link between iBATIS and Jasper occurs in the getFieldValue method when you use the JRField parameter’s getName method to find the associated value in the current map object. The field names returned from the iBATIS SELECT statement are the key values in the current map object and must match the name of the Jasper report’s fields exactly. It would be helpful if the current map’s keys were string-based, so the case sensitivity issue could be avoided. You could always create your own string-based key map and teach iBATIS to use it on request (but that’s a bit out of this article’s scope). Creating a map object driven reportTo work with a list of map objects in iReport, you need to create a data source that knows how to use a custom JRDataSource implementation. Before you set up the data source, though, you need to add a static method to return an instance of the custom JRDataSource populated with a list of map objects that can be used to run sample reports in the iReport environment. Listing 5 shows the code for this method.Listing 5. Method added to JRDataSource implementation to return an instance of JRDataSourcepublic static JRDataSource createDataSource() { List list = new ArrayList(); Map record = new HashMap(); record.put("ID", new BigDecimal("1")); record.put("FIRST", "John"); record.put("LAST", "Doe"); record.put("TOTAL", new BigDecimal("1600.50")); record.put("LAST_SALE", new BigDecimal("32.50")); list.add(record); return new CustomJRDS (list); } After you add the method in Listing 5, compile the JRDataSource implementation, JAR it up, and place it in iReport’s lib directory. Restart iReport and open a new report (iReport menu > File > New Document).Creating a data source to use JRDataSource is much like the process of creating a Java bean powered data source shown in Figure 1, except that you select “Custom JRDataSource” as the data source type instead of “JavaBeans set data source.” A map object doesn’t have properties that iReport can introspect to create fields, so you must use the iBATIS SQL statement to generate the Jasper fields. Do this by setting the “Report query” in iReport (see Figure 4). Notice that the Jasper fields that are created are the fields from the SELECT statement. For this reason, it’s important always to match the case of the field names exactly between the iBATIS SQL and the Jasper report. Don’t use a different SQL in iReport from what you deploy for use with iBATIS. A different field name in the report and the iBATIS SQL would mean the comparison in the getFieldValue method would never yield the expected value, and the report would not get filled.Figure 4. Generating Jasper fields from Java bean in iReportThe only caveat with using the SQL statement to create the Jasper fields is that you need a live connection to the database. This means that prior to creating the Jasper fields you must set up a database-driven iReport data source and set the data source to the default. When the field creation is complete, you can change the default data source back to the JRDataSource (to power the report from the JRDataSource implementation class). The alternative to using an active database connection is to create the fields one at a time using the View > Fields menu in iReport.In conclusionThis article has demonstrated the first steps toward a Jasper-based reporting solution powered by an existing iBATIS implementation. iBATIS’s easy maintenance and configuration features — such as externalized SQL, dynamic SQL generation, and data mapping — are all now available to your Jasper reports. You can also address your application’s future scalability concerns more effectively, because the data-retrieval function is separate from the report-generation function. All the good reasons you selected the iBATIS framework to begin with are extended to your new JasperReports implementation, giving you another arena for effective code reuse. Scott Monahan is a senior software engineer for the Chase Paymentech Solutions Internet Gateway group and works primarily in Java and C++ technologies. He successfully designed and implemented an iBATIS/Jasper reporting solution that handles all of the reporting needs for the Chase Paymentech Internet Gateway subsystems. Open SourceSoftware DevelopmentBuild AutomationJavaData ManagementDevelopment Tools