JasperReports and iReports tutorial for JAVA using subreports with different datasources

Here at RationalPlan we are working on a way to generate reports. We chose JasperReports for Java and iReports as the graphical interface for a visual creation of the report layout. It is a daunting task because there are no tutorials which describe in detail how to create a master report with many subreports each  with their different datasource and if you want to read the documentation you have to pay for it. After accomplishing what we proposed we thought that is a good idea to share our knowledge and experience.

1. Creating the main report layout

First let’s start by opening iReport and creating the main report as shown in the figure (in this tutorial there were used the latest versions of  iReport(3.7.6) and JasperReports(3.7.6)).

main report layout

If you don’t know how to make the main report to look like this search about iReports tutorial on different search engines or post here and we shall try to help you as much as we can. After finishing it press the “Preview” button and if there are no errors in the iReport console then you can compile your report by clicking “Compile report” icon. A new file with the .jasper extension will be created in the same folder as the .jrxml file.

2. Creating the main report custom datasource

After successfully completing the first step you must create the custom datasource in your java application.

Create a class that implements JRRewindableDataSource and then complete getFieldValue(JRField jrField) and next() methods with your own code.

public class ProjectInfoMainReportDS implements JRRewindableDataSource {

	private Task project;
	private int index = -1;
	private DateFormat dateFormat;

	public ProjectInfoMainReportDS(Task project) {
		this.project = project;
		dateFormat = new SimpleDateFormat(UIManager.getInstance()
				.getDateFormat());
	}

	public Object getFieldValue(JRField jrField) throws JRException {
		String fieldName = jrField.getName();
		if (fieldName.equals("LOGO")) {
			if (loadLogo() != null) {
				return loadLogo();
			} else
				return null;
		} else if (fieldName.equals("REPORT_DATE")) {
			return dateFormat.format(new Date());
		} else if (fieldName.equals("PROJECT_NAME")) {
			return project.getName();
		} else if (fieldName.equals("PROJECT_CODE")) {
			return project.getCode();
		} else if (fieldName.equals("PROJECT_MANAGER")) {
			return project.getManager();
		} else if (fieldName.equals("CLIENT")) {
			if (project.getClient() == null) {
				return null;
			} else {
				return project.getClient().getName();
			}
		} else if (fieldName.equals("BUDGET")) {
			return project.getCostBudget();
		} else if (fieldName.equals("START_DATE")) {
			return dateFormat
					.format(((SNET) project.getConstraint()).getDate());
		} else if (fieldName.equals("END_DATE")) {
			return dateFormat.format(project.getEndDate());
		} else if (fieldName.equals("PROJECT_SCOPE")) {
			return project.getDescription();
		}
		return null;
	}

	public boolean next() throws JRException {
		index++;
		return (index < 1);
	}

	public void moveFirst() throws JRException {
	}
}

In the getFieldValue() method you have to check every field from the main report and return the corresponding value from your application. In the next() method you have to tell how many times the report fields are evaluated. In our case only once but if we modify the return condition like this: index < 2 then each field is evaluated twice and there will be duplicate data in your report. In the above code snippet you can see how these methods are implemented in RationalPlan Project Management Software.

The method loadLogo() returns an image from the application in order to print it in the report.

3. Creating the subreport layout and adding it to the main report

Now  go back to the iReport application and open your .jrxml file which contains the main report layout. Add a subreport with an empty datasource to the main report. After completing the subreport wizard a new tab will appear in the application. This is the subreport’s layout and has the same structure as the master report because in JasperReports every subreport is treated as a master report. In the following picture you will see the structure of the subreport.

As seen in this picture the subreport contains one field and one table. In order to fill both the field and the table with the same datasource you must right click on the table then click “Edit table datasource”  and in the appearing dialog you must write  $P{REPORT_DATA_SOURCE}.  After pressing Ok preview the subreport, compile it and save the .jasper file in the same place as the master’s .jasper file.

Now go back to main report and create two parameters. Let’s name them NOTES_LINKS_SUBREPORT and NOTES_LINKS_DATASOURCE. For the first parameter go to properties window, click on the “Parameter Class” row and in the dialog write net.sf.jasperreports.engine.JasperReport.

For the second parameter repeat the same steps as for NOTES_LINKS_SUBREPORT and from the drop down list of  “Parameter Class” field choose java.lang.Object. Now the parameters are set and ready to be used.

These parameters are used to link the subreport with the master report. In order to achieve this switch the application’s view from Designer to XML and locate your subreport in the xml file. It should look something like this:

	<subreport>
			<reportElement x="185" y="613" width="200" height="100"/>
			<dataSourceExpression><![CDATA[new net.sf.jasperreports.engine.JREmptyDataSource()]]></dataSourceExpression>
			<subreportExpression class="java.lang.String"><![CDATA[$P{SUBREPORT_DIR} + "ProjectInfo_subreport3.jasper"]]></subreportExpression>
	</subreport>

Leaving it like this the subreport would not be assigned to the main report so modify it according to the next code snippet:

<subreport>
        <reportElement positionType="Float" x="0" y="277" width="555" height="100" isRemoveLineWhenBlank="true"/>
	<dataSourceExpression><![CDATA[$P{NOTES_LINKS_DATASOURCE}]]></dataSourceExpression>
	<subreportExpression class="net.sf.jasperreports.engine.JasperReport"><![CDATA[$P{NOTES_LINKS_SUBREPORT}]]></subreportExpression>
</subreport>

As you can see in these expressions we use the parameters created and configured before to link each subreport to its custom datasource and also to the master report. In case you have many subreports just create two parameters for each subreport and repeat the steps described above.

4. Creating the subreport’s custom datasource

In order to create the subreport’s datasource repeat the indications in step 2 but be careful with the initialization of index. In this case it is -2 because before the table we have only one field. Let us  suppose that we have three fields before the table then the index should be -4.

The LINK and DESCRIPTION fields are the fields of the table and you can’t see them in the subreport snapshot.

public class ProjectInfoNotesLinksDS implements JRRewindableDataSource {

	private Task project;
	private int index = -2;
	private List<Link> links;

	public ProjectInfoNotesLinksDS(Task project) {
		this.project = project;
		links = project.getLinks();
	}

	public Object getFieldValue(JRField jrField) throws JRException {
		String fieldName = jrField.getName();
		if (fieldName.equals("PROJECT_NOTES")) {
			return project.getNotes();
		} else {
			Link currentLink = links.get(index);
			if (fieldName.equals("LINK")) {
				return currentLink.getName();
			} else if (fieldName.equals("DESCRIPTION")) {
				return currentLink.getDescription();
			}
		}
		return null;
	}

	public boolean next() throws JRException {
		index++;
		System.out.println("next link" + index);
		return index < links.size();
	}

	public void moveFirst() throws JRException {

	}
}

5. Creating the main class of the report

After creating the custom datasource classes for the main report and for the subreport we have to create the report’s main class which will actually create the report and print it to PDF.

public class ProjectInfoMainReport {
	private HashMap<String, Object> hm = new HashMap<String, Object>();
	private Task project;
	private ProjectInfoMainReportDS mainReportDS;
	private InputStream projectInfoStream;

	public ProjectInfoMainReport(Company company, boolean notesLinks,
			boolean assumpConstr, boolean risks) {

		project = company.getCurrentTask();
		mainReportDS = new ProjectInfoMainReportDS(project);
		projectInfoStream = getClass().getResourceAsStream(
				"/com/sbs/jpm/reports/jasper/ProjectInfo.jasper");
		fillNotesLinks();
	}

	private void fillNotesLinks() {
		try {
			InputStream notesLinksStream = getClass()
					.getResourceAsStream(
							"/com/sbs/jpm/reports/jasper/ProjectInfo_Notes_Links.jasper");
			JasperReport subreport = (JasperReport) JRLoader
					.loadObject(notesLinksStream);
			hm.put("NOTES_LINKS_DATASOURCE", new ProjectInfoNotesLinksDS(
					project));
			hm.put("NOTES_LINKS_SUBREPORT", subreport);
		} catch (JRException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void print(File file) {
		try {
			JasperPrint print = JasperFillManager.fillReport(projectInfoStream,
					hm, mainReportDS);
			JRExporter exporter = new JRPdfExporter();
			exporter.setParameter(JRExporterParameter.OUTPUT_FILE_NAME, file
					.getAbsolutePath());
			exporter.setParameter(JRExporterParameter.JASPER_PRINT, print);
			exporter.exportReport();
		} catch (JRException e) {
			e.printStackTrace();
			JOptionPane.showMessageDialog(null,
					"Failed to generate the report!", I18nTexts.I18N_MESSAGES
							.getString("file.failedToSave"),
					JOptionPane.ERROR_MESSAGE);
		} catch (Exception e) {
			e.printStackTrace();
		}

	}
}

As shown in this code snippet in the class constructor we declare and initiate the main’s report custom datasource, we load into a stream the compiled version of the main report (the .jasper file) and call the fillNotesLinks() method. Be careful to place all the .jasper files inside the source files of your project.

The fillNotesLinks() method creates a stream based on the subreport ‘s .jasper file, fills the hash map with the parameters created in step 3 and assigns them the corresponding values.  The NOTES_LINKS_DATASOURCE parameter gets the subreport’s custom datasource and NOTES_LINKS_SUBREPORT gets the subreport created from the stream.

The print() method prints the jasper file to PDF using the stream mentioned before, the hash map that contains all the parameters and the main’s report datasource.

After compiling and running your application without errors the PDF file should be generated in the location pointed by the file parameter passed to the print() method.

In the end you should have a perfectly working java application that generates reports. If not try harder. Good luck!