Building Dynamic Columns in JasperReports

Written by Jeff Ortman
on March 28, 2014

JasperReports

The JasperReports Library is a very powerful and full-featured open source reporting engine. The jaspersoft.com web site states, “The JasperReports Library is the world’s most popular open source reporting engine. It is entirely written in Java and it is able to use data coming from any kind of data source and produce pixel-perfect documents that can be viewed, printed or exported in a variety of document formats including HTML, PDF, Excel, OpenOffice and Word.”

For each JasperReport, a template is built that is populated with data from a database, CSV file or other data source. If tabular data is displayed, the number of columns in the table must be set at the time the template is built. If more columns need to be displayed, typically a new template would be built specific to the new output requirements. So what if you want to use only one template but display data with a varying number of columns? This is not trivial to do and there are many approaches to solving this problem. This article describes one such approach that can be used to dynamically build tabular output in JasperReports with a varying number of columns.

To understand the approach of dynamically building tabular output, first you need to understand the workflow involved in creating a JasperReport. There are four steps or phases in creating a JasperReport. They are shown in the diagram below, which was taken from the very helpful JasperReports Quick Guide.

First, the JasperReport is designed and a JRXML report template is created. JRXML templates are standard XML files that have the extension .jrxml. The XML can be created by hand in a text editor but typically iReport Designer, a visual report designer for JasperReports from the same company that developed JasperReports, is used to visually create the report layout.

Next, the JRXML is compiled into a Jasper template (which has a .jasper extension). In the execution phase, the Jasper template is filled with data that is provided from a SQL query, an XML file, a CSV file or from any number of other types of data sources to create a .jprint file. The JasperFillManager is the class that performs this task.

Finally, the report can be exported to multiple output formats in the export phase.

There are a couple of approaches that can be used to modify a template dynamically. One popular approach that is described in various blogs or articles on the Web is to modify the JRXML that describes the Report and recompile the JRXML into a Jasper template. Depending on the type and scope of changes, Velocity templates or XSLT could be used in modifying the JRXML. While this approach works, it requires an understanding of the underlying JRXML format and could break if the format changes in future releases of JasperReports. Another approach that might be viable is to create all possible columns and then hide columns dynamically based upon the “Print When Expression.” This expression can be defined for each column in the report template. This approach still requires the template designer to know the maximum number of columns that can be displayed.

A third approach is to use the JasperReports crosstab element to execute a crosstab query. Crosstab queries are similar to Pivot Tables in Excel where the data is aggregated or grouped so that the data in the original table becomes the column headers and rows in the Pivot table (such as aggregating all sales data and determining average sales by product and region). The JasperReports crosstab component handles the display of dynamic data. Not all data, however, is structured so that crosstab queries make sense. For example, if we are reporting on data that originates from free-form text fields on a Web page it might be difficult to apply a crosstab query.

Finally, a library called Dynamic Jasper could be used. The promise of Dynamic Jasper is that it can simplify much of the code needed to write dynamic reports compared to using the built-in JasperReports capabilities. Depending on the complexity of what you are trying to build, Dynamic Jasper could be worth investigating.

The approach described below uses the JasperReports Java API to dynamically build rows and columns of tabular data and uses a concrete subclass of JRAbstractBeanDataSource to provide data to the JasperFillManager. Therefore, it’s an implementation approach that depends only on the JasperReport libraries that are already present in any project that is using JasperReports.

JasperReports ships with an API that can be used to create and manipulate JasperDesign objects. Here is the code that uses the JasperReports API to build columns dynamically:

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.engine.type.HorizontalAlignEnum;

/**
* Uses the Jasper Report API to dynamically add columns to the Report
*/
public class DynamicReportBuilder {
//The prefix used in defining the field name that is later used by the
JasperFillManager
public static final String COL_EXPR_PREFIX = "col";

// The prefix used in defining the column header name that is later
used by the JasperFillManager
public static final String COL_HEADER_EXPR_PREFIX = "header";

// The page width for a page in portrait mode with 10 pixel margins
private final static int TOTAL_PAGE_WIDTH = 545;

// The whitespace between columns in pixels
private final static int SPACE_BETWEEN_COLS = 5;

// The height in pixels of an element in a row and column
private final static int COLUMN_HEIGHT = 12;

// The total height of the column header or detail band
private final static int BAND_HEIGHT = 15;

// The left and right margin in pixels
private final static int MARGIN = 10;

// The JasperDesign object is the internal representation of a report
private JasperDesign jasperDesign;

// The number of columns that are to be displayed
private int numColumns;

public DynamicReportBuilder(JasperDesign jasperDesign, int numColumns) {
this.jasperDesign = jasperDesign;
this.numColumns = numColumns;
}

public void addDynamicColumns() throws JRException {

JRDesignBand detailBand = new JRDesignBand();
JRDesignBand headerBand = new JRDesignBand();

JRDesignStyle normalStyle = getNormalStyle();
JRDesignStyle columnHeaderStyle = getColumnHeaderStyle();
jasperDesign.addStyle(normalStyle);
jasperDesign.addStyle(columnHeaderStyle);

int xPos = MARGIN;
int columnWidth = (TOTAL_PAGE_WIDTH - (SPACE_BETWEEN_COLS * (numColumns - 1))) / numColumns;

for (int i = 0; i < numColumns; i++) {

// Create a Column Field
JRDesignField field = new JRDesignField();
field.setName(COL_EXPR_PREFIX + i);
field.setValueClass(java.lang.String.class);
jasperDesign.addField(field);

// Create a Header Field
JRDesignField headerField = new JRDesignField();
headerField.setName(COL_HEADER_EXPR_PREFIX + i);
headerField.setValueClass(java.lang.String.class);
jasperDesign.addField(headerField);

// Add a Header Field to the headerBand
headerBand.setHeight(BAND_HEIGHT);
JRDesignTextField colHeaderField = new JRDesignTextField();
colHeaderField.setX(xPos);
colHeaderField.setY(2);
colHeaderField.setWidth(columnWidth);
colHeaderField.setHeight(COLUMN_HEIGHT);
colHeaderField.setHorizontalAlignment(HorizontalAlignEnum.LEFT);
colHeaderField.setStyle(columnHeaderStyle);
JRDesignExpression headerExpression = new JRDesignExpression();
headerExpression.setValueClass(java.lang.String.class);
headerExpression.setText("$F{" + COL_HEADER_EXPR_PREFIX + i + "}");
colHeaderField.setExpression(headerExpression);
headerBand.addElement(colHeaderField);

// Add text field to the detailBand
detailBand.setHeight(BAND_HEIGHT);
JRDesignTextField textField = new JRDesignTextField();
textField.setX(xPos);
textField.setY(2);
textField.setWidth(columnWidth);
textField.setHeight(COLUMN_HEIGHT);
textField.setHorizontalAlignment(HorizontalAlignEnum.LEFT);
textField.setStyle(normalStyle);
JRDesignExpression expression = new JRDesignExpression();
expression.setValueClass(java.lang.String.class);
expression.setText("$F{" + COL_EXPR_PREFIX + i + "}");
textField.setExpression(expression);
detailBand.addElement(textField);

xPos = xPos + columnWidth + SPACE_BETWEEN_COLS;
}

jasperDesign.setColumnHeader(headerBand);
((JRDesignSection)jasperDesign.getDetailSection()).addBand(detailBand);
}

private JRDesignStyle getNormalStyle() {
JRDesignStyle normalStyle = new JRDesignStyle();
normalStyle.setName("Sans_Normal");
normalStyle.setDefault(true);
normalStyle.setFontName("SansSerif");
normalStyle.setFontSize(8);
normalStyle.setPdfFontName("Helvetica");
normalStyle.setPdfEncoding("Cp1252");
normalStyle.setPdfEmbedded(false);
return normalStyle;
}

private JRDesignStyle getColumnHeaderStyle() {
JRDesignStyle columnHeaderStyle = new JRDesignStyle();
columnHeaderStyle.setName("Sans_Header");
columnHeaderStyle.setDefault(false);
columnHeaderStyle.setFontName("SansSerif");
columnHeaderStyle.setFontSize(10);
columnHeaderStyle.setBold(true);
columnHeaderStyle.setPdfFontName("Helvetica");
columnHeaderStyle.setPdfEncoding("Cp1252");
columnHeaderStyle.setPdfEmbedded(false);
return columnHeaderStyle;
}

}

It is difficult to explain all that is going on in the above code sample, but I will highlight a few items. The code will make more sense if you have first used iReport Designer to design a report, as the terminology used in iReport Designer matches the terminology used in the JasperReports API. A “band” is a section of a JasperReport. In a standard JasperReport, there is a Title Band, a Page Header Band, a Column Header Band, a Detail Band, a Column Footer Band, a Page Footer Band and a Summary Band. Anything added to the Detail Band is understood to be repeating. The Detail Band is where the data in the report is printed as a table. To get the code sample to work, a JasperReport template should be created with no Column Header Band and no Detail Band. These two “bands” are created dynamically by the above code.

For each column, JRDesignField objects are created for the column and for the column header. These JRDesignField objects are then added to the JasperDesign object. JRDesignTextField objects for each column and column header are then added to the appropriate band. There is code to control where the field is placed on the x and y axis, to set the height, to set the horizontal alignment and to set the font style.

This code sample takes in a parameter for the number of columns and computes the column width for each column based upon the number of columns. Obviously there is only so much horizontal space on the page. Once over a certain number of columns is displayed (depending on the data this could be 5-15 columns), each column will be so small that the truncated data will not be useful anymore. Each column is also the same width.This code could be made more flexible if there is knowledge of the data to be displayed.

Next let’s look at how the data in the report template that we have just designed gets populated. Each column in the report has an expression of the following form: (“$F{“col”+ i + “}”. In JasperReports terminology, these are field expressions that are evaluated at runtime. In a report with four columns, the four columns will have the following four field expressions: $F{col1}, $F{col2}, $F{col3}, $F{col4}.

The JasperFillManager takes as a parameter a JRDataSource. JasperReports defines a number of implementations of this interface including JRCsvDataSource, JRResultSetDataSource and JRXmlDataSource among others. However, you can also subclass JRAbstractBeanDataSource and provide your own implementation by just overriding the next(), getFieldValue() and moveFirst() methods.

Here is an implementation of JRAbstractBeanDataSource that can provide data to the JasperFillManager from a simple row and column structure.

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.data.JRAbstractBeanDataSource;

import java.util.Iterator;
import java.util.List;

/**
* Implementation of a Jasper Reports data source that can contain a dynamic number of columns
*/
public class DynamicColumnDataSource extends JRAbstractBeanDataSource
{
private List<string> columnHeaders;
private List<list<string>> rows;
private Iterator<list<string>> iterator;
private List<string> currentRow;

public DynamicColumnDataSource(List<string> columnHeaders, List<list<string>> rows)
{
super(true);

this.rows = rows;
this.columnHeaders = columnHeaders;

if (this.rows != null && this.rows != null)
{
this.iterator = this.rows.iterator();
}
}

@Override
public boolean next()
{
boolean hasNext = false;

if (iterator != null)
{
hasNext = iterator.hasNext();

if (hasNext)
{
this.currentRow = iterator.next();
}
}

return hasNext;
}

@Override
public Object getFieldValue(JRField field) throws JRException
{
// The name of the field in dynamic columns that were created by DynamicReportBulder is also the index into the list of columns.
// For example, if the field is named 'col1', this is the second (because it's zero-based) column in the currentRow.
String fieldName = field.getName();
if (fieldName.startsWith(DynamicReportBuilder.COL_EXPR_PREFIX)) {
String indexValue = fieldName.substring(DynamicReportBuilder.COL_EXPR_PREFIX.length());
String column = currentRow.get(Integer.parseInt(indexValue));
return column;
}
else if (fieldName.startsWith(DynamicReportBuilder.COL_HEADER_EXPR_PREFIX)) {
int indexValue = Integer.parseInt(fieldName.substring(DynamicReportBuilder.COL_HEADER_EXPR_PREFIX.length()));
String columnHeader = columnHeaders.get(indexValue);
return columnHeader;
}
else {
throw new RuntimeException("The field name '" + fieldName + "' in the Jasper Report is not valid");
}
}

@Override
public void moveFirst()
{
if (rows != null)
{
iterator = rows.iterator();
}
}
}
</list<string></string></string></list<string></list<string></string>

The DynamicColumnDataSource is constructed from a List of Column Header Strings and a List of Lists of Strings representing each value in the table to be printed. The JasperFillManager will call the overridden methods when populating the report.

To put it all together, here is the code that loads a report template called DynamicColumns.jrxml, adds the dynamic columns and fills the report using the DynamicColumnDataSource.

public void runReport(List<string> columnHeaders, List<list<string>> rows) throws JRException {

InputStream is = getClass().getResourceAsStream("../../../DynamicColumns.jrxml");
JasperDesign jasperReportDesign = JRXmlLoader.load(is);

DynamicReportBuilder reportBuilder = new DynamicReportBuilder(jasperReportDesign, columnHeaders.size());
reportBuilder.addDynamicColumns();

JasperReport jasperReport = JasperCompileManager.compileReport(jasperReportDesign);

Map<string, object=""> params = new HashMap<string, object="">();
params.put("REPORT_TITLE", "Sample Dynamic Columns Report");
DynamicColumnDataSource pdfDataSource = new DynamicColumnDataSource(columnHeaders, rows);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, params, pdfDataSource);

JasperExportManager.exportReportToPdfFile(jasperPrint, "/tmp/DynamicColumns.pdf");
}

</string,></string,></list<string></string>

As you can see, the JasperReports API provides a lot of functionality to create dynamic reports. By using the approach described above, you can display different result sets using the same JasperReport template. Since JasperReports can be exported in many different formats, this can provide a powerful framework to provide users of your application the ability to run queries with varying result set sizes and export this data to whatever format is desired.

The full source code for this article can be found on our git repository under the JasperDynamicReports directory.