JasperReports: Handling a Large Detail Band

iReport and the JasperReports framework will not let you design a page that is greater than one page in length. Why this limitation exists is a little opaque to me: there’s no reason that an extremely large detail needs to be on one page. On a recent project I was using JasperReports to print a report that consisted of many fields of one record – that is, there would ever only be one detail record on the report, and that detail band would stretch over 3 or 4 pages.

This is such a common requirement that it is addressed in the FAQ:

Well, we could do that by splitting the detail band content on multiple bands, each one of them smaller that one page, so that the report design remains valid. And what other report bands could we use? Group headers and footers of course.

Here’s the trouble with the proffered solution: it doesn’t work. The group headers and footers are included in the page size calculation, so even if you split the detail band over the header and footers as described, the report would not compile. I’m not entirely sure how that answer got included in the FAQ, but if you read it closely it appears that it was written as a hypothetical.

I did manage to solve the problem, though. But first we have to talk about subreports.

Subreports

Subreports are intended to handle collections of objects that are children of the main detail. For example, so you have a Person object that has a collection of Address objects associated with it. We’d like the report to look like this:

Name: PersonName    Age:45

Address: 44 My Street, Mycity, etc
Address: 55 Your Street, Yourcity, etc
Address: 66 Someother Ave, Whereever, etc

for an arbitrary number of Addresses. So we create a subreport named Contacts – it’s just a normal report with a datasource that feeds addresses to it. It’s easiest if the margins and non-detail bands of the subreport are zeroed out so that it can be placed on the main report more easily.

In the main report, define a subreport element. The size of the element is unimportant, the report will start at the top of the subreport element and fill space as needed. In the subreport properties, define a datasource that feeds Addresses to the subreport. For a JDBC-based report, the easy way to do that is to pass the conenction to the subreport and have that subreport run a query like select * from address where person_id = $P{PERSON_ID}, and pass PERSON_ID to the subreport via a parameter. For a bean-based report (like the one I’m actually creating), it’s easy enough to specify a datasource like new JRBeanCollectionDataSource((Collection)$F{addresses}).

Using subreports to create a large detail

Imagine that the Person object has hundreds of attributes. We can’t fit everything about it on to one page of detail. So instead we break the Person report into multiple subreports – one subreport takes care of name and age, one takes care of educational history, one takes care of physical characteristics, etc.

Unlike the Address subreport, these subreports still want to use the primary Person datasource to populate themselves. We can’t use the same datasource as the main report since it will have been pre-iterated before the subreport is evaluated, so we need another way to access the Person data. That’s easy enough to do with a JDBC connection – just use a query like select * from person where person_id = $P{PERSON_ID} in the subreport. Using a bean-based datasource is trickier. I ended up creating a class like this:

package ca.intelliware.jasper;

import java.util.ArrayList;
import java.util.List;

import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import ca.intelliware.model.Person;

public class PersonCollectionDataSource extends JRBeanCollectionDataSource {

    private final List<Person> persons;

    public PersonCollectionDataSource(List<Person> persons) {
        super(persons);
        this.persons= persons;
    }

    public PersonCollectionDataSource getSubDataSource() {
        List<Person> newList = new ArrayList<Person>();

        for (Person person : this.persons) {
            newList.add(person);
        }

        return new PersonCollectionDataSource(newList);
    }
}

Now the main report has a datasource of type PersonCollectionDataSource, and it can pass ((ca.intelliware.jasper.PersonCollectionDataSource)$P{REPORT_DATA_SOURCE}).getSubDataSource()to the subreports.

There is of course a huge drawback with this approach – it only works for lists of one Person at a time. If multiple Persons are printed at once, the subreports would list all the details for the entire list.

Ensuring the subreport datasource only contains the current bean

The JRBeanCollectionDataSource keeps track of the current bean in a private variable named currentBean. I simply access that via reflection and make sure that the current bean is the only added to the subreport datasource.

package ca.intelliware.jasper;

import java.util.ArrayList;
import java.util.List;

import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import ca.intelliware.model.Person;

public class PersonCollectionDataSource extends JRBeanCollectionDataSource {

    public PersonCollectionDataSource(List<Person> persons) {
        super(persons);
    }

    public PersonCollectionDataSource getSubDataSource() {
        List<Person> newList = new ArrayList<Person>();
        newList.add(getCurrentPerson());
        return new PersonCollectionDataSource(newList);
    }

    private Person getCurrentPerson() {
        try {
            Field currentBeanField = JRBeanCollectionDataSource.class.getDeclaredField("currentBean");
            currentBeanField.setAccessible(true);
            return (Person) currentBeanField.get(this);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException("Programmer error: PersonCollectionDataSource should have a currentBean");
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Programmer error: PersonCollectionDataSource should have a currentBean");
        }
    }
}

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply