JasperReport as a report option?

I’ve seen in another post (https://www.cuba-platform.com/discuss/t/jasperreports-in-cuba) that JasperReports integration is planned. Any status on this? I’m hitting some limitations in YARG and I don’t want to re-invent the wheel if you are close to having this done.

My vision is that the jrxml file would be loaded as a template, just as you do for Excel, and a JasperReport formatter would execute the jrxml.

What are the plans?

2 Likes

No plans at the moment. I’ve created a YouTrack issue, so we’ll consider it when planning the next release.

Of course, if you have any implementation to share - you are very welcome.

I am working on one as we speak. The general idea is to use your bands as the data sources and the JasperReport fields will be referenced as band.field. This is totally untested yet and may not pan out, but I’m hopeful. I’m using XLSFormatter as a reference.

Current issue I’m running into is a conflict between slf4j implementations. JasperReports needs olap4j, which I’ve added as a dependency. In order to build my code. I also added your reports-core as a dependency, which gives me access to some of the reports internals so I can integrate things. I believe this is what causes the conflict, as it appears that olap4j uses log4j but your reports-core uses logback-classic and these don’t play well together. When I try to run the app, I get this in the Tomcat logs:


SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/home/eraskin/studio-projects/rade/build/tomcat/shared/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/home/eraskin/studio-projects/rade/build/tomcat/shared/lib/logback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]

I found a post on the web (java - ClassCastException: org.slf4j.impl.Log4jLoggerAdapter cannot be cast to ch.qos.logback.classic.Logger - Stack Overflow) that seems to indicate a solution, but it is for Maven and not Gradle. I don’t know enough to translate it.

Any suggestions on how to resolve this?

Try to exclude slf4j from your new dependency:


compile('...') {
     exclude(group: 'org.slf4j', module: 'slf4j-log4j12')
}

I found that I had to delete the log4j-1.2.17.jar and the slf4j-log4j12-1.7.5.jar from tomcat/shared/lib directory. They were still there and getting loaded in the classpath, even though my project didn’t require them any more. :-/

I looked through all the gradle dependency graphs and could NOT find anything that loaded them to begin with, so deleting them seems OK. I guess something I had tried to include earlier in my development cycle caused the problem.

As an aside, I also discovered that bringing in com.haulmont.reports:reports-core:6.4.1 is sufficient. I originally had brought in YARG as well, but reports-core brings in YARG. Seems obvious when I think about it. :slight_smile:

Good to hear you have a progress.

I found that I had to delete the log4j-1.2.17.jar and the slf4j-log4j12-1.7.5.jar from tomcat/shared/lib directory.

It can be a result of your previous deployments. The deployment procedure doesn’t clean the shared/lib and webapps, it only resolve version conflicts for jars with same names. In order to make a clean deployment after changes in dependencies execute undeploy task: Run > Undeploy in Studio menu.

AHA! Learn something new every day. :slight_smile:

Next part of the equation. I’ve build a FormatterFactory like this:


package com.paslists.reporting;

import com.haulmont.reports.libintegration.CubaFormatterFactory;
import com.haulmont.yarg.formatters.ReportFormatter;
import com.haulmont.yarg.formatters.factory.FormatterFactoryInput;

/**
 * Created by eraskin on 3/6/17.
 */
public class PASFormatterFactory extends CubaFormatterFactory {

    public PASFormatterFactory() {
        super();
        FormatterCreator jrxmlCreator = new FormatterCreator() {
            @Override
            public ReportFormatter create(FormatterFactoryInput factoryInput) {
                JRXMLFormatter jrxmlFormatter = new JRXMLFormatter(factoryInput);
                return jrxmlFormatter;
            }
        };

        formattersMap.put("jrxml", jrxmlCreator);
    }
}

and I’ve modified the spring.xml file in the core module like this (as described in https://www.cuba-platform.com/discuss/t/jasperreports-in-cuba):


   <bean id="reporting_lib_FormatterFactory"
          class="com.paslists.reporting.PASFormatterFactory">
        <property name="useOfficeForDocxPdfConversion" value="${cuba.reporting.openoffice.docx.useOfficeForPdfConversion?:false}"></property>
        <property name="officeIntegration" ref="reporting_lib_OfficeIntegration"></property>
        <property name="defaultFormatProvider" ref="reporting_lib_CubaFieldFormatProvider"></property>
    </bean>

So, now that I’ve got this built, how do I actually ACCESS my Formatter? I’m using the reports gui and I can’t seem to figure out how to build a report. How do I tell the system that the template is jrxml and to use the JRXMLFormatter class that I built?

Hi Eric,

One of the solutions is to create a custom report defined by java class.
You can create report template with custom output type. Add JRXML as a file for Template File field and specify java class that creates a document in JasperReport by JRXML file.

You can find template configuration in the attached image. I’ve built a sample java class that reads template and returns it to the user. You can add print report functionality by JasperReport to that class.


import com.haulmont.yarg.formatters.CustomReport;
import com.haulmont.yarg.structure.BandData;
import com.haulmont.yarg.structure.ReportTemplate;
import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.util.Map;

public class JasperReportFormatter implements CustomReport {

    @Override
    public byte[] createReport(com.haulmont.yarg.structure.Report report, BandData rootBand, Map<String, Object> params) {
        //Find report template with code JRXML
        ReportTemplate template = report.getReportTemplates().get("JRXML");
        try {
            byte[] array = IOUtils.toByteArray(template.getDocumentContent());
            //Execute loaded JRXML in JasperReports.....
            //Get result from JasperReports as byte array and return it...
            return array;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

reportTemplate

Sorry to overload another issue, but it is all related to the JasperReports extension…

As far as I can tell from the code, just loading a template that ends with the letters “.jrxml” seems to be enough to invoke the code. Is this correct?

The issue I run into now is that none of my output formats (such as XLSX) are “legal” for jrxml. There is a class ReportPrintHelper in the gui that contains a map called inputOutputTypesMapping. I can’t figure out how to add an entry to that table to tell the gui what outputs are legal for jrxml. Do you have any suggestions for a good way to do this?

Looks like we cross-posted. I was trying to build it into the system so that we wouldn’t need a custom class, but maybe the custom class is a better solution.

I can give it a shot unless you think my original attempt is better in the long run (since I am considering contributing it to the project).

One question, though. Which module does the custom report class belong in?

Custom report class must be in the core module.

I’ve gotten pretty far. I can execute a Jasper Report where the SQL is defined in the report template as Bands. Each field is referenced in the JRXML file as “band.alias” (and every field in the band must have an alias). The only downside is that you have to manually declare each and every field in the Jasper Studio report builder.

I’m now trying to allow for SQL directly in the JRXML file via JNDI. In order for that to work, I need to be able to access an XML file that contains the JNDI specification. I can’t figure out where to put that XML file so it is accessible. The JasperReports library documentation seems to imply that the file must be accessible to the web browser (as opposed to being on the classpath).

What’s the best way to make this accessible? In which module and directory/package should I place this XML file?

Am I right that you want to connect to the database directly from JasperReports? If so, the JDBC datasource is defined in modules/core/web/META-INF/context.xml and you can obtain it from JNDI in the “app-core” web application using java:comp/env/jdbc/CubaDS name.

Understood, I could use the JNDI definition if I create a connection myself in the code and pass that to the report. That may be an option in that I think I can pass the connection into the executing report.

When you use Jasper Studio to build the report and define a JDBC connection there, you get a datasource xml file referenced in the jrxml file. It is very possible that the JDBC connection the user creates is different from the JNDI CubaDS data source, so I’m not sure that automatically creating the JNDI connection is the right thing to do. I’m also not sure if the resulting jrxml file will work if the user doesn’t remove that property referencing the connection xml file before uploading the template. I can test that.

One small disadvantage of using your report template and bands, or defining the connection externally, is that I have to create all the report fields by hand. To avoid that, I have to use a connection created in Jasper Studio. But we can’t mix the approaches, since the fields defined by the SQL don’t have “band” names in them. It’s a lot of manual effort or embed the SQL query in the jrxml file itself. And then I get the datasource xml file, etc…

An alternative would be to extend your report schema to allow us to upload the connection xml file. If we did that, I could extract it in the same way as I extract the jrxml template from the database and create the connection in the code. But that requires changes to your schema.

My only solution so far has been to put the xml file in the main directory of app-core (build/tomcat/webapps/app-core) manually. I can then access it, but it has a plain text password and that would be accessible on the web. NOT a good solution yet. :-/ Jasper Reports has a Jasper Server that I’m trying NOT to require since I don’t think it will work with your report band definitions. It would require SQL defined in the report. If you use Jasper Server, then it provides the data source to the report by storing the definition in a database and hiding the password. It also appears to encrypt the password in xml files.

All in all, still a work in progress. No good solution presents itself yet. I am open to suggestions.

An update for you. I wrote an SQL version of my code as a separate CustomReport class. As long as I remove the data provider description in the jrxml file, it successfully connects using the JNDI definition. So, I can use SQL embedded in the report. If I leave the data provider in the jrxml, then that overrides the connection I provide and we are back to the original problem. In a perfect world, I would be able to override the jrxml definition with my connection, but it doesn’t work that way.

I guess the next option is to allow for a parameter named “conn” that contains the JNDI definition that the user wants to connect to. Then they can connect to any database they choose and not just the CubaDS connection. The user would enter this in the Root band of the definition. Does this seem like a good compromise or do you have a better way to do this?

Hi Eric,

An alternative would be to extend your report schema to allow us to upload the connection xml file. If we did >>that, I could extract it in the same way as I extract the jrxml template from the database and create the >>connection in the code. But that requires changes to your schema.

You can create a separate entity “Connection” and store XML-definition of connection on it. You can put connection entity as a parameter of a report and create the connection from the entity in the code.

I went the “conn” parameter route instead. Seems less invasive than creating yet another Entity. In any event, the basics now seem to work. The requirements are:

Option 1) Use the Custom class com.paslists.reporting.JasperReportFormatter if you want to create a report based on bands declared in the report template. Make sure all fields have an alias defined if you are using SQL in the bands. Manually declare all fields in the JRXML report with “band.alias”. Upload the JRXML file as the template with an ID of “JRXML”.

Option 2) Use the Custom class com.paslists.reporting.JasperSQLReportFormatter if you want to create a report based on SQL embedded in the report via a JNDI connection. By default, the code will connect to the CubaDS jndi connection pool " java:comp/env/jdbc/CubaDS". Optionally, create a parameter named “conn” in the Root band and define your jndi connection info (same format as the default). Upload the JRXML file as the template with an ID of “JRXML”. Make sure that the JRXML does NOT include any net.sf.jasperreports.data.adapter property definition, as that will override the connection we create.

All normal Jasper Reports output formats are supported.

I have NOT tested sub-reports yet. Sub-Reports require passing connection information from the main to the sub-report, which should be OK. They also require a path to the sub-report JRXML code, which must be accessible. I’m not quite sure how to achieve that yet. Open to suggestions…

:ticket: See the following issue in our bug tracker:

https://youtrack.cuba-platform.com/issue/PL-8765