Search code examples
javascriptnode.jselectroncrystal-reports

How to use SAP Crystal Reports in a Node.js application


I am trying to use SAP Crystal Reports in an Electron application (a desktop Node.js application). I have a few *.rpt files that should read data from an SQLite database and then print some reports.

But it looks like there is no JavaScript library that would enable me to do that. The only thing I have found is SAP Crystal Reports JavaScript API which is just a client library for SAP Business Intelligence Platform that I don't use. I simply need to print some reports without relying on any external server.

Has anybody managed to make printing work with Crystal Reports in a Node.js application?


Solution

  • It looks like there is no way to interact with Crystal Reports directly from a Node.js application. So I have decided to write a simple command-line Java program which makes use of Crystal Reports Java SDK. I run this Java program from my Node.js application.

    Crystal Reports CLI (Java)

    I have a single cr-cli/CrystalReport.java file in my project:

    import com.crystaldecisions.sdk.occa.report.application.*;
    import com.crystaldecisions.sdk.occa.report.data.*;
    import com.crystaldecisions.sdk.occa.report.document.*;
    import com.crystaldecisions.sdk.occa.report.lib.*;
    import com.crystaldecisions.sdk.occa.report.exportoptions.*;
    import java.io.*;
    import java.nio.file.*;
    import org.apache.commons.cli.*;
    
    public class CrystalReport {
    
      public static void main(String args[]) {
        Options options = new Options();
    
        Option databaseOption = new Option("d", "database", true, "database file path");
        databaseOption.setRequired(true);
        options.addOption(databaseOption);
    
        Option exportOption = new Option("e", "export", true, "output PDF file path");
        options.addOption(exportOption);
    
        Option reportOption = new Option("r", "report", true, "report file path");
        reportOption.setRequired(true);
        options.addOption(reportOption);
    
        Option selectionOption = new Option("s", "selection", true, "record selection formula");
        selectionOption.setRequired(true);
        options.addOption(selectionOption);
    
        CommandLine cmd = null;
    
        try {
          CommandLineParser parser = new DefaultParser();
          cmd = parser.parse(options, args);
        } catch (ParseException e) {
          System.out.println(e.getMessage());
    
          HelpFormatter formatter = new HelpFormatter();
          formatter.printHelp("cr-cli", options);
    
          System.exit(1);
        }
    
        String databasePath = cmd.getOptionValue("database");
        String exportPath = cmd.getOptionValue("export");
        String reportPath = cmd.getOptionValue("report");
        String selectionFormula = cmd.getOptionValue("selection");
    
        try {
          CrystalReport report = new CrystalReport(reportPath, databasePath, selectionFormula);
    
          if (exportPath != null) {
            Files.createDirectories(Paths.get(exportPath).getParent());
            report.export(exportPath);
          } else {
            report.print();
          }
        } catch (Exception ex) {
          ex.printStackTrace();
          System.exit(1);
        }
      }
    
      private ReportClientDocument report;
    
      public CrystalReport(String reportPath, String databasePath, String selectionFormula) throws ReportSDKException {
        this.report = new ReportClientDocument();
        this.report.open(reportPath, 0);
    
        this.report.setRecordSelectionFormula(selectionFormula);
    
        CrystalReport.replaceDatabaseConnection(this.report, databasePath);
        SubreportController subreportController = report.getSubreportController();
        for (String subreportName : subreportController.getSubreportNames()) {
          ISubreportClientDocument subreport = subreportController.getSubreport(subreportName);
          CrystalReport.replaceDatabaseConnection(subreport, databasePath);
        }
      }
    
      private static void replaceDatabaseConnection(IReportClientDocument report, String databasePath) throws ReportSDKException {
        DatabaseController databaseController = report.getDatabaseController();
        IConnectionInfo oldConnectionInfo = databaseController.getConnectionInfos(null).getConnectionInfo(0);
    
        PropertyBag attributes = new PropertyBag(oldConnectionInfo.getAttributes());
        attributes.put("Connection URL", "jdbc:sqlite:" + databasePath);
    
        IConnectionInfo newConnectionInfo = new ConnectionInfo(oldConnectionInfo);
        newConnectionInfo.setAttributes(attributes);
    
        int replaceParams = DBOptions._ignoreCurrentTableQualifiers + DBOptions._doNotVerifyDB;
        databaseController.replaceConnection(oldConnectionInfo, newConnectionInfo, null, replaceParams);
      }
    
      public void export(String exportPath) throws IOException, FileNotFoundException, ReportSDKException {
        ByteArrayInputStream byteArrayInputStream = (ByteArrayInputStream) this.report.getPrintOutputController().export(ReportExportFormat.PDF);
        byte byteArray[] = new byte[byteArrayInputStream.available()];
    
        File file = new File(exportPath);
        FileOutputStream fileOutputStream = new FileOutputStream(file);
    
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(byteArrayInputStream.available());
        int x = byteArrayInputStream.read(byteArray, 0, byteArrayInputStream.available());
        byteArrayOutputStream.write(byteArray, 0, x);
        byteArrayOutputStream.writeTo(fileOutputStream);
      }
    
      public void print() throws ReportSDKException {
        this.report.getPrintOutputController().printReport(new PrintReportOptions());
      }
    }
    

    With these files in cr-cli/lib folder:

    com.azalea.ufl.barcode.1.0.jar
    commons-configuration-1.2.jar
    CrystalCommon2.jar
    DatabaseConnectors.jar
    JDBInterface.jar
    log4j-api.jar
    pfjgraphics.jar
    sqlite-jdbc-3.41.0.0.jar
    XMLConnector.jar
    commons-cli-1.5.0.jar
    commons-lang-2.1.jar
    CrystalReportsRuntime.jar
    icu4j.jar
    jrcerom.jar
    log4j-core.jar
    QueryBuilder.jar
    webreporting.jar
    xpp3.jar
    commons-collections-3.2.2.jar
    commons-logging.jar
    cvom.jar
    jai_imageio.jar
    keycodeDecoder.jar
    logging.jar
    sap.com~tc~sec~csi.jar
    webreporting-jsf.jar
    

    I compile it using the following command:

    javac -classpath './cr-cli/lib/*' -source 8 -target 8 ./cr-cli/CrystalReport.java
    

    And then I run it from my Electron (Node.js) application this way:

    import {exec} from 'child_process';
    import {app} from 'electron';
    import path from 'path';
    
    interface CrystalReportsCliOptions {
      databasePath: string;
      exportPath?: string;
      reportPath: string;
      selectionFormula: string;
    }
    
    const cliPath = path.join(app.isPackaged ? process.resourcesPath : process.cwd(), 'cr-cli');
    
    export async function runCrystalReportsCli(options: CrystalReportsCliOptions) {
      const javaVersion = await detectJavaVersion();
      if (javaVersion < 8) {
        return;
      }
    
      const command = ['java'];
      if (javaVersion >= 9) {
        command.push('--add-exports', 'java.base/sun.security.action=ALL-UNNAMED');
      }
      command.push('-classpath', `"${cliPath + path.delimiter + path.join(cliPath, 'lib/*')}"`);
    
      command.push('CrystalReport');
      command.push('--database', options.databasePath);
      command.push('--report', options.reportPath);
      command.push('--selection', `"${options.selectionFormula}"`);
      if (options.exportPath) {
        command.push('--export', options.exportPath);
      }
    
      return new Promise((resolve, reject) => {
        exec(command.join(' '), (error, stdout) => (error ? reject(error) : resolve(stdout)));
      });
    }
    
    function detectJavaVersion(): Promise<number> {
      return new Promise(resolve => {
        exec('java -version', (error, _, stderr) => {
          if (error) {
            resolve(0);
          } else {
            const [versionLine] = stderr.split('\n');
            const version = versionLine.split('"')[1];
            const majorVersion = Number(version.split('.')[version.startsWith('1.') ? 1 : 0]);
    
            resolve(majorVersion);
          }
        });
      });
    }