BIRT in spring boot app

Henk V picture Henk V · Aug 18, 2015 · Viewed 11.7k times · Source

I need to create a report capability(function) in an existing spring boot web application. The suggestion was to use BIRT, which I could integrate with the spring boot web app.

I found the article below and was able to run the reports in a spring boot starter project (using http://start.spring.io/). This article, fairly old, did help me to get to a working example. https://spring.io/blog/2012/01/30/spring-framework-birt. This article is basically exactly what I want, but in a spring boot web app.

The challenge I have is to run the report using the BIRT viewer, which comes with nice additional features. (Print, Expoet data, PDF, pagination etc.)

I cannot find any spring boot example using BIRT the way this article describes.

My questions are:

  1. Is there an alternative or other way to do reports in a spring boot web application? (obviously not want to re-inventing the wheel by creating BIRT like capability from scratch or run the report engine separate from the web application if at all possible)

  2. Does anyone today have BIRT working (with the viewer) in a spring boot web application and be willing to share or educate me on the best way to do this? (I tried to get JSP page working with spring boot, but unable to so successfully...more a lack of experience than anything else)

Can someone help me please.

Kind Regards, Henk

Answer

Kent Bull picture Kent Bull · Apr 20, 2016

Dependencies, an instance of the IReportEngine, and directories are the most challenging part of setting up BIRT in a spring boot web application. This example has a little bit of error handling and depends on environment variables for specifying paths to important directories.

One nice things about this setup is that it is a standalone ReST API for building and generating reports. As a side benefit it doesn't depend on JSP at all.

I run it in a Docker container.

Originally there was a plan to use some of the interfaces to try other reporting frameworks like Jasper reports or something else.

This isn't exactly what you are asking for though in my opinion it is better in certain contexts. You have a lot of flexibility with using, configuring, and deploying the BIRT report runner as an application like this. This does not use the pre-packaged BIRT Viewer provided by Actian.

I would make a data container for reports called birt-report-runner-data and then put all of the directories for logging and reports on that container which you can then mount onto your BIRT container.

Main Components

Main File (BirtReportRunnerApplication.java)

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BirtReportRunnerApplication {

    public static void main(String[] args) {
        SpringApplication.run(BirtReportRunnerApplication.class, args);
    }
}

Controller to receive report Requests (ReportController.java)

package com.example.core.web.controller;

import com.example.core.model.BIRTReport;
import com.example.core.service.ReportRunner;
import com.example.core.web.dto.ReportRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController("ReportController")
@RequestMapping("/reports")
public class ReportController {

    private Logger logger = LoggerFactory.getLogger(ReportController.class);

    @Autowired
    @Qualifier("birt")
    ReportRunner reportRunner;

    @RequestMapping(value = "/birt", method = RequestMethod.POST)
    public ResponseEntity<byte[]> getBIRTReport(@RequestBody ReportRequest reportRequest) {
        byte[] reportBytes;
        ResponseEntity<byte[]> responseEntity;
        try {
            logger.info("REPORT REQUEST NAME:   " + reportRequest.getReportName());
            reportBytes =
                new BIRTReport(
                    reportRequest.getReportName(),
                    reportRequest.getReportParameters(),
                    reportRunner)
                    .runReport().getReportContent().toByteArray();

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.parseMediaType("application/pdf"));
            String fileName = reportRequest.getReportName() + ".pdf";
            httpHeaders.setContentDispositionFormData(fileName, fileName);
            httpHeaders.setCacheControl("must-revalidate, post-check=0, pre-check=0");
            responseEntity = new ResponseEntity<byte[]>(reportBytes, httpHeaders, HttpStatus.OK);
        } catch (Exception e) {
            responseEntity = new ResponseEntity<byte[]>(HttpStatus.NOT_IMPLEMENTED);
            return responseEntity;
        }
        return responseEntity;
    }
}

Report Request DTO for specifying report to run (ReportRequest.java)

package com.example.core.web.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;

public class ReportRequest {
    private String reportName;
    private String reportParameters;

    public ReportRequest(@JsonProperty("reportName") String reportName,
                         @JsonProperty("reportParameters") String reportParameters) {
        this.reportName = reportName;
        this.reportParameters = reportParameters;
    }

    public String getReportName() {
        return reportName;
    }

    public String getReportParameters() {
        return reportParameters;
    }

    public void setReportParameters(String reportParameters) {
        this.reportParameters = reportParameters;
    }
}

Interface for report runner (ReportRunner.java)

package com.example.core.service;

import com.example.core.model.Report;

import java.io.ByteArrayOutputStream;

public interface ReportRunner {

    ByteArrayOutputStream runReport(Report report);
}

Largest class that does most of the work (BIRTReportRunner.java)

package com.example.core.service;

import com.example.core.model.Report;
import org.eclipse.birt.core.exception.BirtException;
import org.eclipse.birt.core.framework.Platform;
import org.eclipse.birt.report.engine.api.*;
import org.eclipse.core.internal.registry.RegistryProviderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

@Service
@Qualifier("birt")
public class BIRTReportRunner implements ReportRunner {
    private static final String DEFAULT_LOGGING_DIRECTORY = "defaultBirtLoggingDirectory/";
    private Logger logger = LoggerFactory.getLogger(BIRTReportRunner.class);

    private static String reportOutputDirectory;

    private IReportEngine birtReportEngine = null;

    @Autowired
    private Environment env;

    /**
     * Starts up and configures the BIRT Report Engine
     */
    @PostConstruct
    public void startUp() {
        if(env.getProperty("birt_report_input_dir") == null)
            throw new RuntimeException("Cannot start application since birt report input directory was not specified.");
        try {
            String birtLoggingDirectory = env.getProperty("birt_logging_directory") == null ? DEFAULT_LOGGING_DIRECTORY : env.getProperty("birt_logging_directory");
            Level birtLoggingLevel = env.getProperty("birt_logging_level") == null ? Level.SEVERE : Level.parse(env.getProperty("birt_logging_level"));
            EngineConfig engineConfig = new EngineConfig();
            logger.info("BIRT LOG DIRECTORY SET TO : {}", birtLoggingDirectory);
            logger.info("BIRT LOGGING LEVEL SET TO {}", birtLoggingLevel);
            engineConfig.setLogConfig(birtLoggingDirectory, birtLoggingLevel);

            // Required due to a bug in BIRT that occurs in calling Startup after the Platform has already been started up
            RegistryProviderFactory.releaseDefault();
            Platform.startup(engineConfig);
            IReportEngineFactory reportEngineFactory = (IReportEngineFactory) Platform.createFactoryObject(IReportEngineFactory.EXTENSION_REPORT_ENGINE_FACTORY);
            birtReportEngine = reportEngineFactory.createReportEngine(engineConfig);
        } catch (BirtException e) {
            // TODO add logging aspect and find out how to log a platform startup problem from this catch block, if possible, using the aspect.
            // Possibly rethrow the exception here and catch it in the aspect.
            logger.error("Birt Startup Error: {}", e.getMessage());
        }

        reportOutputDirectory = env.getProperty("birt_temp_file_output_dir");
    }

    /**
     * Shuts down the BIRT Report Engine
     */
    @PreDestroy
    public void shutdown() {
        birtReportEngine.destroy();
        RegistryProviderFactory.releaseDefault();
        Platform.shutdown();
    }

    public File getReportFromFilesystem(String reportName) throws RuntimeException {
        String reportDirectory = env.getProperty("birt_report_input_dir");
        Path birtReport = Paths.get(reportDirectory + File.separator + reportName + ".rptdesign");
        if(!Files.isReadable(birtReport))
            throw new RuntimeException("Report " + reportName + " either did not exist or was not writable.");

        return birtReport.toFile();
    }

    /**
     * This method creates and executes the report task, the main responsibility
     * of the entire Report Service.
     * This method is key to enabling pagination for the BIRT report. The IRunTask run task
     * is created and then used to generate an ".rptdocument" binary file.
     * This binary file is then read by the separately created IRenderTask render
     * task. The render task renders the binary document as a binary PDF output
     * stream which is then returned from the method.
     * <p>
     *
     * @param birtReport the report object created at the controller to hold the data of the report request.
     * @return Returns a ByteArrayOutputStream of the PDF bytes generated by the
     */
    @Override
    public ByteArrayOutputStream runReport(Report birtReport) {

        ByteArrayOutputStream byteArrayOutputStream;
        File rptDesignFile;

        // get the path to the report design file
        try {
            rptDesignFile = getReportFromFilesystem(birtReport.getName());
        } catch (Exception e) {
            logger.error("Error while loading rptdesign: {}.", e.getMessage());
            throw new RuntimeException("Could not find report");
        }

        // process any additional parameters
        Map<String, String> parsedParameters = parseParametersAsMap(birtReport.getParameters());

        byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            IReportRunnable reportDesign = birtReportEngine.openReportDesign(rptDesignFile.getPath());
            IRunTask runTask = birtReportEngine.createRunTask(reportDesign);

            if (parsedParameters.size() > 0) {
                for (Map.Entry<String, String> entry : parsedParameters.entrySet()) {
                    runTask.setParameterValue(entry.getKey(), entry.getValue());
                }
            }
            runTask.validateParameters();

            String rptdocument = reportOutputDirectory + File.separator
                    + "generated" + File.separator
                    + birtReport.getName() + ".rptdocument";
            runTask.run(rptdocument);

            IReportDocument reportDocument = birtReportEngine.openReportDocument(rptdocument);
            IRenderTask renderTask = birtReportEngine.createRenderTask(reportDocument);

            PDFRenderOption pdfRenderOption = new PDFRenderOption();
            pdfRenderOption.setOption(IPDFRenderOption.REPAGINATE_FOR_PDF, new Boolean(true));
            pdfRenderOption.setOutputFormat("pdf");
            pdfRenderOption.setOutputStream(byteArrayOutputStream);
            renderTask.setRenderOption(pdfRenderOption);

            renderTask.render();
            renderTask.close();
        } catch (EngineException e) {
            logger.error("Error while running report task: {}.", e.getMessage());
            // TODO add custom message to thrown exception
            throw new RuntimeException();
        }

        return byteArrayOutputStream;
    }

    /**
     * Takes a String of parameters started by '?', delimited by '&', and with
     * keys and values split by '=' and returnes a Map of the keys and values
     * in the String.
     *
     * @param reportParameters a String from a HTTP request URL
     * @return a map of parameters with Key,Value entries as strings
     */
    public Map<String, String> parseParametersAsMap(String reportParameters) {
        Map<String, String> parsedParameters = new HashMap<String, String>();
        String[] paramArray;
        if (reportParameters.isEmpty()) {
            throw new IllegalArgumentException("Report parameters cannot be empty");
        } else if (!reportParameters.startsWith("?") && !reportParameters.contains("?")) {
            throw new IllegalArgumentException("Report parameters must start with a question mark '?'!");
        } else {
            String noQuestionMark = reportParameters.substring(1, reportParameters.length());
            paramArray = noQuestionMark.split("&");
            for (String param : paramArray) {
                String[] paramGroup = param.split("=");
                if (paramGroup.length == 2) {
                    parsedParameters.put(paramGroup[0], paramGroup[1]);
                } else {
                    parsedParameters.put(paramGroup[0], "");
                }

            }
        }
        return parsedParameters;
    }
}

Report object class (Report.java)

package com.example.core.model;

import com.example.core.service.ReportRunner;

import java.io.ByteArrayOutputStream;
import java.util.List;

/**
 * A Report object has a byte representation of the report output that can be
 * used to write to any output stream. This class is designed around the concept
 * of using ByteArrayOutputStreams to write PDFs to an output stream.
 *
 *
 */
public abstract class Report {

    protected String name;
    protected String parameters;
    protected ByteArrayOutputStream reportContent;
    protected ReportRunner reportRunner;

    public Report(String name, String parameters, ReportRunner reportRunner) {
        this.name = name;
        this.parameters = parameters;
        this.reportRunner = reportRunner;
    }

    /**
     * This is the processing method for a Report. Once the report is ran it
     * populates an internal field with a ByteArrayOutputStream of the
     * report content generated during the run process.
     * @return Returns itself with the report content output stream created.
     */
    public abstract Report runReport();

    public ByteArrayOutputStream getReportContent() {
        return this.reportContent;
    }

    public String getName() {
        return name;
    }

    public String getParameters() {
        return parameters;
    }
}

BIRTReport object class (BIRTReport.java)

package com.example.core.model;

import com.example.core.service.ReportRunner;

public class BIRTReport extends Report {

    public BIRTReport(String name, String reportParameters, ReportRunner reportRunner) {
        super(name, reportParameters, reportRunner);
    }

    @Override
    public Report runReport() {
        this.reportContent = reportRunner.runReport(this);
        return this;
    }
}

Build file (build.gradle)

buildscript {
    ext {
        springBootVersion = '1.3.2.RELEASE'
    }
    repositories {
        jcenter()

    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("io.spring.gradle:dependency-management-plugin:0.5.2.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'com.example.simple-birt-runner'
    version = '1.0.0'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

ext {
    springBootVersion = '1.3.0.M5'
}

repositories {
    jcenter()
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}")
    compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")


    // BIRT Runtime and ReportEngine dependencies
    // These were pulled from the Actuate download site at http://download.eclipse.org/birt/downloads/ for version 4.5.0
    compile fileTree(dir: 'lib', include: '*.jar')
    /*compile("org.eclipse.birt.runtime:org.eclipse.birt.runtime:4.4.2") {
        exclude group: "org.eclipse.birt.runtime", module: "org.apache.poi"
        exclude group: "org.eclipse.birt.runtime", module: "org.eclipse.orbit.mongodb"
    }*/

    compile("org.springframework.boot:spring-boot-starter-tomcat")

    // include any runtime JDBC driver dependencies here

    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Dependencies

Get all of the dependencies in the Birt Viewer lib directory and put them on your classpath. Here is a full list of external dependencies:

com.ibm.icu_54.1.1.v201501272100.jar
com.lowagie.text_2.1.7.v201004222200.jar
derby.jar
flute.jar
javax.wsdl_1.5.1.v201012040544.jar
javax.xml.stream_1.0.1.v201004272200.jar
javax.xml_1.3.4.v201005080400.jar
net.sourceforge.lpg.lpgjavaruntime_1.1.0.v201004271650.jar
org.apache.batik.bridge_1.6.0.v201011041432.jar
org.apache.batik.css_1.6.0.v201011041432.jar
org.apache.batik.dom.svg_1.6.0.v201011041432.jar
org.apache.batik.dom_1.6.1.v201505192100.jar
org.apache.batik.ext.awt_1.6.0.v201011041432.jar
org.apache.batik.parser_1.6.0.v201011041432.jar
org.apache.batik.pdf_1.6.0.v201105071520.jar
org.apache.batik.svggen_1.6.0.v201011041432.jar
org.apache.batik.transcoder_1.6.0.v201011041432.jar
org.apache.batik.util.gui_1.6.0.v201011041432.jar
org.apache.batik.util_1.6.0.v201011041432.jar
org.apache.batik.xml_1.6.0.v201011041432.jar
org.apache.commons.codec_1.6.0.v201305230611.jar
org.apache.commons.logging_1.1.1.v201101211721.jar
org.apache.lucene.core_3.5.0.v20120725-1805.jar
org.apache.poi_3.9.0.v201405241750.jar
org.apache.xerces_2.9.0.v201101211617.jar
org.apache.xml.resolver_1.2.0.v201005080400.jar
org.apache.xml.serializer_2.7.1.v201005080400.jar
org.eclipse.birt.runtime_4.5.0.jar
org.eclipse.core.contenttype_3.5.0.v20150421-2214.jar
org.eclipse.core.expressions_3.5.0.v20150421-2214.jar
org.eclipse.core.filesystem_1.5.0.v20150421-0713.jar
org.eclipse.core.jobs_3.7.0.v20150330-2103.jar
org.eclipse.core.resources_3.10.0.v20150423-0755.jar
org.eclipse.core.runtime.compatibility_3.2.300.v20150423-0821.jar
org.eclipse.core.runtime_3.11.0.v20150405-1723.jar
org.eclipse.datatools.connectivity.apache.derby.dbdefinition_1.0.2.v201107221459.jar
org.eclipse.datatools.connectivity.apache.derby_1.0.103.v201212070447.jar
org.eclipse.datatools.connectivity.console.profile_1.0.10.v201109250955.jar
org.eclipse.datatools.connectivity.db.generic_1.0.1.v201107221459.jar
org.eclipse.datatools.connectivity.dbdefinition.genericJDBC_1.0.2.v201310181001.jar
org.eclipse.datatools.connectivity.oda.consumer_3.2.6.v201403131814.jar
org.eclipse.datatools.connectivity.oda.design_3.3.6.v201403131814.jar
org.eclipse.datatools.connectivity.oda.flatfile_3.1.8.v201403010906.jar
org.eclipse.datatools.connectivity.oda.profile_3.2.9.v201403131814.jar
org.eclipse.datatools.connectivity.oda_3.4.3.v201405301249.jar
org.eclipse.datatools.connectivity.sqm.core_1.2.8.v201401230755.jar
org.eclipse.datatools.connectivity_1.2.11.v201401230755.jar
org.eclipse.datatools.enablement.hsqldb.dbdefinition_1.0.0.v201107221502.jar
org.eclipse.datatools.enablement.hsqldb_1.0.0.v201107221502.jar
org.eclipse.datatools.enablement.ibm.db2.iseries.dbdefinition_1.0.3.v201107221502.jar
org.eclipse.datatools.enablement.ibm.db2.iseries_1.0.2.v201107221502.jar
org.eclipse.datatools.enablement.ibm.db2.luw.dbdefinition_1.0.7.v201405302027.jar
org.eclipse.datatools.enablement.ibm.db2.luw_1.0.3.v201401170830.jar
org.eclipse.datatools.enablement.ibm.db2.zseries.dbdefinition_1.0.4.v201107221502.jar
org.eclipse.datatools.enablement.ibm.db2.zseries_1.0.2.v201107221502.jar
org.eclipse.datatools.enablement.ibm.db2_1.0.0.v201401170830.jar
org.eclipse.datatools.enablement.ibm.informix.dbdefinition_1.0.4.v201107221502.jar
org.eclipse.datatools.enablement.ibm.informix_1.0.1.v201107221502.jar
org.eclipse.datatools.enablement.ibm_1.0.0.v201401170830.jar
org.eclipse.datatools.enablement.msft.sqlserver.dbdefinition_1.0.1.v201201240505.jar
org.eclipse.datatools.enablement.msft.sqlserver_1.0.3.v201308161009.jar
org.eclipse.datatools.enablement.mysql.dbdefinition_1.0.4.v201109022331.jar
org.eclipse.datatools.enablement.mysql_1.0.4.v201212120617.jar
org.eclipse.datatools.enablement.oda.ws_1.2.6.v201403131825.jar
org.eclipse.datatools.enablement.oda.xml_1.2.5.v201403131825.jar
org.eclipse.datatools.enablement.oracle.dbdefinition_1.0.103.v201206010214.jar
org.eclipse.datatools.enablement.oracle_1.0.0.v201107221506.jar
org.eclipse.datatools.enablement.postgresql.dbdefinition_1.0.2.v201110070445.jar
org.eclipse.datatools.enablement.postgresql_1.1.1.v201205252207.jar
org.eclipse.datatools.enablement.sap.maxdb.dbdefinition_1.0.0.v201107221507.jar
org.eclipse.datatools.enablement.sap.maxdb_1.0.0.v201107221507.jar
org.eclipse.datatools.modelbase.dbdefinition_1.0.2.v201107221519.jar
org.eclipse.datatools.modelbase.derby_1.0.0.v201107221519.jar
org.eclipse.datatools.modelbase.sql.query_1.1.4.v201212120619.jar
org.eclipse.datatools.modelbase.sql_1.0.6.v201208230744.jar
org.eclipse.datatools.sqltools.data.core_1.2.3.v201212120623.jar
org.eclipse.datatools.sqltools.parsers.sql.lexer_1.0.1.v201107221520.jar
org.eclipse.datatools.sqltools.parsers.sql.query_1.2.1.v201201250511.jar
org.eclipse.datatools.sqltools.parsers.sql_1.0.2.v201107221520.jar
org.eclipse.datatools.sqltools.result_1.1.6.v201402080246.jar
org.eclipse.emf.common_2.11.0.v20150512-0501.jar
org.eclipse.emf.ecore.change_2.11.0.v20150512-0501.jar
org.eclipse.emf.ecore.xmi_2.11.0.v20150512-0501.jar
org.eclipse.emf.ecore_2.11.0.v20150512-0501.jar
org.eclipse.equinox.app_1.3.300.v20150423-1356.jar
org.eclipse.equinox.common_3.7.0.v20150402-1709.jar
org.eclipse.equinox.preferences_3.5.300.v20150408-1437.jar
org.eclipse.equinox.registry_3.6.0.v20150318-1503.jar
org.eclipse.help_3.6.0.v20130326-1254.jar
org.eclipse.osgi.services_3.5.0.v20150519-2006.jar
org.eclipse.osgi_3.10.100.v20150529-1857.jar
org.eclipse.update.configurator_3.3.300.v20140518-1928.jar
org.mozilla.javascript_1.7.5.v201504281450.jar
org.w3c.css.sac_1.3.1.v200903091627.jar
org.w3c.dom.events_3.0.0.draft20060413_v201105210656.jar
org.w3c.dom.smil_1.0.1.v200903091627.jar
org.w3c.dom.svg_1.1.0.v201011041433.jar

Application properties (application.yml)

birt:
  report:
    output:
      dir: ${birt_temp_file_output_dir}
    input:
      dir: ${birt_report_input_dir}
  logging:
    level: ${birt_logging_level}


server:
  port: 8080
  context-path: /simple-birt-runner


birt:
  logging:
    level: SEVERE

logging:
  level:
    org:
      springframework:
        web: ERROR

Dockerfile for running (Dockerfile)

FROM java:openjdk-8u66-jre
MAINTAINER Kent O. Johnson <[email protected]>

COPY com.example.simple-birt-runner-*.jar /opt/soft/simple-birt-runner.jar

RUN mkdir -p /reports/input \
    && mkdir /reports/output \
    && mkdir -p /reports/log/engine

WORKDIR /opt/soft

CMD java -jar -Xms128M -Xmx4G simple-birt-runner.jar

EXPOSE 8080

Docker compose file for running image (docker-compose.yml)

simple-birt-runner:
  image: soft/simple-birt-runner-release
  ports:
  - "8090:8080"
  environment:
  - birt_temp_file_output_dir=/reports/output
  - birt_report_input_dir=/reports/input
  - birt_logging_directory=/reports/log/engine
  - birt_logging_level=SEVERE
  volumes_from:
  - birt-report-runner-data