Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid requiring a DataSource for subreports if printWhen expression evaluates to false #403

Open
jnehlmeier opened this issue Nov 28, 2023 · 5 comments

Comments

@jnehlmeier
Copy link

jnehlmeier commented Nov 28, 2023

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".

@jnehlmeier
Copy link
Author

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.

@dadza
Copy link
Collaborator

dadza commented Nov 29, 2023

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

if (isPrintWhenExpressionNull() || isPrintWhenTrue())

Can you post the stacktrace of the exception that you get, to confirm where it comes from? Also, what JasperReports version are you using?

@jnehlmeier
Copy link
Author

jnehlmeier commented Nov 30, 2023

@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?

@dadza
Copy link
Collaborator

dadza commented Dec 2, 2023

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.

@jnehlmeier
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants