jasperreports
jasperreports copied to clipboard
Avoid requiring a DataSource for subreports if printWhen expression evaluates to false
If I define a sub report with a printWhen expression then chances are that the sub report datasource only works correctly if the printWhen expression returns true. However JasperReports still asks for a datasource instance even if the printWhen expression evaluates to false. This makes it difficult to write DataSource implementations that check their input parameters.
Consider the example:
public class PersonDataSource implements JRDataSource {
private final Person person;
public PersonDataSource(Person person) {
this.person = Objects.requireNonNull(person);
}
public Object getFieldValue(JRField field) {
var name = field.getName();
return switch(name) {
case "name" -> person.getName();
default -> null;
}
}
}
Now if I have a parent report with
<subreport>
<reportElement>
<printWhenExpression><![CDATA[$F{person} != null]]></printWhenExpression>
</reportElement>
<dataSourceExpression><![CDATA[new PersonDataSource($F{person})]]></dataSourceExpression>
....
</subreport>
it fails at runtime because the datasource expression is evaluated without considering the printWhen expression. Thus a PersonDataSource with null parameter may be created.
I am wondering if it would be possible to avoid that and only ask for datasources if they are really needed. That would save java instances and make the code cleaner. Currently custom datasources like above would always need to expect @Nullable parameters and check them in their next() method to eventually return false. That is quite some code noise. Ideally it should be possible to say "A PersonDataSource requires a person as input and if it doesn't receive one an exception will be thrown".
What I can do is changing the parent report XML to something like
<subreport>
<reportElement>
<printWhenExpression><![CDATA[$F{person} != null]]></printWhenExpression>
</reportElement>
<dataSourceExpression><![CDATA[$F{DATASOURCE_PERSON}]]></dataSourceExpression>
....
</subreport>
and have a custom report datasource for the parent report that looks like
public Object getFieldValue(JRField field) {
var name = field.getName();
return switch(name) {
case "DATASOURCE_PERSON" -> {
if (person != null) {
yield new PersonDataSource(person);
}
yield null;
}
default -> null;
}
}
But now I have the same condition in XML (printWhen) and code (DataSource) which isn't ideal if changes need to be done.
I'm not able to reproduce the behaviour you're describing. Also at code level there's a check that skips subreport data source expression evaluation when the print when expression evaluates to false, see https://github.com/TIBCOSoftware/jasperreports/blob/f8fd9a694dc01c3044a35a3ff88921f59d671ffc/jasperreports/src/net/sf/jasperreports/engine/fill/JRFillSubreport.java#L355
Can you post the stacktrace of the exception that you get, to confirm where it comes from? Also, what JasperReports version are you using?
@dadza Thanks for looking into it. Because of your answer I created an example report in my application and tested both cases:
- Detail 1 band without printWhen expression containing a sub report having a printWhen expression.
- printWhen expression on Detail 2 band containing a sub report without printWhen expression
Both cases fail at runtime if the printWhen expression evaluates to false because the datasource expression is evaluated.
I am using the javaflow variant of 6.20.5 currently and in the stack trace I noticed that JRFillSubreport does not appear at all. The stack trace is
at com.example.ExampleReportDataSource.getFieldValueCustom(ExampleReportDataSource.java:58)
at com.example.ReportDataSourceBase.getFieldValue(ReportDataSourceBase.java:116)
at net.sf.jasperreports.engine.fill.JRFillDataset.setOldValues(JRFillDataset.java:1533)
at net.sf.jasperreports.engine.fill.JRFillDataset.next(JRFillDataset.java:1434)
at net.sf.jasperreports.engine.fill.JRFillDataset.next(JRFillDataset.java:1410)
at net.sf.jasperreports.engine.fill.JRBaseFiller.next(JRBaseFiller.java:1210)
at net.sf.jasperreports.engine.fill.JRVerticalFiller.fillReport(JRVerticalFiller.java:117)
at net.sf.jasperreports.engine.fill.JRBaseFiller.fill(JRBaseFiller.java:631)
at net.sf.jasperreports.engine.fill.BaseReportFiller.fill(BaseReportFiller.java:434)
at net.sf.jasperreports.engine.fill.JRFiller.fill(JRFiller.java:162)
at net.sf.jasperreports.engine.fill.JRFiller.fill(JRFiller.java:145)
at net.sf.jasperreports.engine.JasperFillManager.fill(JasperFillManager.java:758)
at net.sf.jasperreports.engine.JasperFillManager.fillReport(JasperFillManager.java:1074)
The XML of the main report looks like:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.20.5.final using JasperReports Library version 6.20.5-3efcf2e67f959db3888d79f73dde2dbd7acb4f8e -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="ExampleReport" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="3a2cbd1b-9f27-40e1-a4ce-7de1b91e9a85">
<field name="DS_SUBREPORT_DETAIL_1" class="net.sf.jasperreports.engine.JRDataSource"/>
<field name="DS_SUBREPORT_DETAIL_2" class="net.sf.jasperreports.engine.JRDataSource"/>
<field name="MESSAGE" class="java.lang.String"/>
<background>
<band splitType="Stretch"/>
</background>
<title>
<band splitType="Stretch"/>
</title>
<pageHeader>
<band splitType="Stretch"/>
</pageHeader>
<columnHeader>
<band splitType="Stretch"/>
</columnHeader>
<detail>
<band height="120" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<subreport>
<reportElement x="0" y="0" width="555" height="120" uuid="a95de6c7-00ce-49e6-8e35-601309a9eb48">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<printWhenExpression><![CDATA[$F{MESSAGE} != null]]></printWhenExpression>
</reportElement>
<dataSourceExpression><![CDATA[$F{DS_SUBREPORT_DETAIL_1}]]></dataSourceExpression>
<subreportExpression><![CDATA["ExampleSubReport"]]></subreportExpression>
</subreport>
</band>
<band height="120">
<printWhenExpression><![CDATA[$F{MESSAGE} != null]]></printWhenExpression>
<subreport>
<reportElement x="0" y="0" width="555" height="120" uuid="cdad5e2b-367a-4db4-8987-28b966578c55">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<dataSourceExpression><![CDATA[$F{DS_SUBREPORT_DETAIL_2}]]></dataSourceExpression>
<subreportExpression><![CDATA["ExampleSubReport"]]></subreportExpression>
</subreport>
</band>
</detail>
<columnFooter>
<band splitType="Stretch"/>
</columnFooter>
<pageFooter>
<band splitType="Stretch"/>
</pageFooter>
<summary>
<band splitType="Stretch"/>
</summary>
</jasperReport>
The corresponding data source does something like:
protected Object getFieldValueCustom(JRField field) throws JRException {
return switch (field.getName()) {
case "DS_SUBREPORT_DETAIL_1" -> exampleSubReportDataSourceFactory.create(getParameters(), message);
case "DS_SUBREPORT_DETAIL_2" -> exampleSubReportDataSourceFactory.create(getParameters(), message);
case "MESSAGE" -> message;
default -> throw new IllegalArgumentException();
};
}
In the above code variable message is kept null so that the printWhen expression evaluates to false. Still JasperReports asks for DS_SUBREPORT_DETAIL_1 and DS_SUBREPORT_DETAIL_2 field. I have tested that by providing a literal string in the factory calls. If I put a literal string in the factory call for DS_SUBREPORT_DETAIL_1 then DS_SUBREPORT_DETAIL_2 fails and vice versa.
Seems like JRFillDataset does not respect printWhen expressions during evaluation?
Field values are always fetched from the data source. That's irrespective of whether the fields are used in expressions or not.
Therefore the only way to have the subreport data source created only when the print when expression evaluates to true is to put the data source creation in the subreport expression and not directly in the field value.
I.e. something like what you had in your original post:
<dataSourceExpression><![CDATA[new PersonDataSource($F{person})]]></dataSourceExpression>
Here the expression is only evaluated when the print when expression is true. But if you have something like $F{PersonDataSource} and the new PersonDataSource(..) code is in the main data source, it will be evaluated when the field values are fetched from data source.
Thanks for your answer. So my simplified example was a bit too simple.
It is quite unfortunate that always all fields are fetched from the data source no matter what. I feel like constructing a data source in the datasource expression more or less only works for simple cases.
I try to keep as much code/sql out of jasper design so I can refactor code without breaking reports unintentionally. Anything in the report design is basically hidden from code refactoring so the most natural thing to do was using just a field as data source expression and provide the instance via the parent data source.
In addition data sources often have dependencies so creating the data source in the data source expression can be complex, especially if dependencies are things like daos/repositories or classes that calculate something and have dependencies themselves. That is why my data sources usually have a factory that allows for providing dependencies manually as well as injecting them via a dependency injection framework like Guice.
So in my situation, if I want to avoid data source creation, the next best thing I could do is using a field that provides the factory and then in jasper design I do something like
<dataSourceExpression>
<![CDATA[ $F{PERSON_DS_FACTORY}.create($P{REPORT_PARAMETERS_MAP}, $F{person}) ]]>
</dataSourceExpression>
But then if someone wants to rename the create method it is difficult to find all locations as you not necessarily know the factory field name to search for.
We are not going to change existing behaviour as there are ways to achieve what you need.
Thank you, Teodor