I was not quite happy with lkdg's answer because I wanted to separate the concern of loading the correct file from the design as in my opinion I should not be forced to organize from where the reports are loaded at design time of the JRXML files.
Unfortunately the code of the Jasper Library is full of static references that make it hard to find the correct spot for the injection of a custom subreport loader and also some of the documentation sucks (e.g. the interface RepositoryService
is completely lacking a contract documentation so I needed to guess the contract by reading calling code), but it is possible:
private static void fillReport() throws IOException, JRException {
// The master report can be loaded just like that, because the
// subreports will not be loaded at this point, but later when
// report is filled.
final JasperReport report = loadReport("masterReport.jasper");
// The SimpleJasperReportsContext allows us to easily specify some
// own extensions that can be injected into the fill manager. This
// class will also delegate to the DefaultJasperReportsContext and
// combine results. Thus all the default extensions will still be available
SimpleJasperReportsContext jasperReportsContext = new SimpleJasperReportsContext();
jasperReportsContext.setExtensions(
RepositoryService.class, singletonList(new SubReportFindingRepository())
);
final byte[] pdf = JasperExportManager.exportReportToPdf(
JasperFillManager
.getInstance(jasperReportsContext)
// carefully select the correct `fill` method here and don't
// accidentally select one of the static ones!:
.fill(report, YOUR_PARAMS, YOUR_CONNECTION)
);
}
private static JasperReport loadReport(final String fileName) throws IOException, JRException {
try(InputStream in = loadReportAsStream(fileName)) {
return (JasperReport) JRLoader.loadObject(in);
}
}
private static InputStream loadReportAsStream(final String fileName) {
final String resourceName = "/package/path/to/reports/" + fileName;
final InputStream report = CurrentClass.class.getResourceAsStream(resourceName);
if (report == null) {
throw new RuntimeException("Report not found: " + resourceName);
}
return report;
}
private static class SubReportFindingRepository implements RepositoryService {
@Override
public Resource getResource(final String uri) {
return null; // Means "not found". The next RepositoryService will be tried
}
@Override
public void saveResource(final String uri, final Resource resource) {
throw new UnsupportedOperationException();
}
@Override
public <K extends Resource> K getResource(final String uri, final Class<K> resourceType) {
if (!isKnownSubReport(uri)) {
return null; // Means "not found". The next RepositoryService will be tried
}
final ReportResource reportResource = new ReportResource();
try {
reportResource.setReport(loadReport(uri));
} catch (IOException | JRException e) {
throw new Error(e);
}
return resourceType.cast(reportResource);
}
private static boolean isKnownSubReport(final String uri) {
return "subReport1.jasper".equals(uri) || "subReport2.jasper".equals(uri);
}
}
As an alternative to the local injection you can also write a global extension. As far as I got it (I did not try) this requires the creation of a jasperreports_extension.properties
file with class names that should be loaded which can include a custom repository to load the reports from. However in this case you completely loose the ability to work with conflicting configurations needed in different use cases.