XprHelper.java

/*
** Module   : XprHelper.java
** Abstract : Helper class to provide report generation from xpr input files.
**
** Copyright (c) 2018-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 EVL 20180509 Created initial version.
**     ECF 20180518 Added single parameter variants of printFile.
**     EVL 20180520 Small fix for help string content.
**     ECF 20180521 Renamed single parameter printFile methods to printPdf; added streaming and
**                  deletion of PDF.
**     EVL 20180521 Changed the input object creation approach removing static XprEntity.create().
** 002 EVL 20180523 Adding flag to set up silent image error mode.
** 003 EVL 20180527 Adding derectory based configuration options reading.  Changed MIME resource
**                  opening call to work with Swing client.
**     SBI 20180528 Fixed schema prefix for local file resources.
**     EVL 20180528 Adding DPI option set up via directory.  Adding 3-rd optional parameter for
**                  simple mode - output page format.
** 004 EVL 20180531 Fixes for line/rectangle X coord.
** 005 EVL 20180622 Adding only log method mode without error message box.
** 006 EVL 20180703 Adding method to log info level message to stdout.
** 007 EVL 20180725 Adding failure info log about XprEntity creation failure.  Adding try...catch
**                  protection to XPR processing block and to Jasper based worker.
** 008 EVL 20190607 Making PDF mime resource related constants as public.
** 009 VVT 20200820 Added the setSimpleUseMode() method.
** 010 EVL 20220513 Adding static method to change the current page size at runtime.
** 011 EVL 20220519 Changing default value flag to TRUE to display icon for missed images.
** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 013 DDF 20230620 Replaced static initialization of values from the directory configuration with
**                  a method called at server bootstrap.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.reporting;

import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.client.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;

import java.awt.print.*;
import java.io.*;
import java.util.*;
import java.util.logging.Level;

import static com.goldencode.p2j.reporting.ReportFactory.showError;

/**
 * The frontend for converted code to make different reports from xpr files generated by 4GL code
 * to print via xPrint library.  This replacement is a platform neutral.
 */
public class XprHelper
{
   /** Standard suffix of PDF file name. */
   public static final String PDF_SUFFIX = ".pdf";
   
   /** MIME type for PDF files. */
   public static final String PDF_MIME = "application/pdf";
   
   /** URL protocol for local file */
   public static final String FILE_PROTOCOL = "file:///";
   
   /** Flag indicating we use the class locally via main() method. */
   private static boolean simpleUse = false;
   
   /** Flag indicating to use ICON image loading error mode. */
   private static boolean imageOnErrorIconMode = true;
   
   /** The width in pixels of the generagted PDF file. */
   private static int pdfPageWidth = -1;
   
   /** The height in pixels of the generagted PDF file. */
   private static int pdfPageHeight = -1;
   
   /** Current DPI value for PDF file to render. */
   private static int pdfDPI = 2 * XprEntity.DPI_PDF_DEFAULT;

   /** Page size in pixels map. */
   private static Map<PrintOptions.PageSize, NativeDimension> pageSizes =
                                            new HashMap<PrintOptions.PageSize, NativeDimension>();
   /** Logger. */
   private static final CentralLogger LOG =
                          CentralLogger.get(XprHelper.class.getName());

   // one time globals init
   static
   {
      // construct page size map for single default
      pageSizes.put(PrintOptions.PageSize.LETTER,
                    new NativeDimension((int)Math.round(8.5*Xpr2PdfWorker.DPI_DEFAULT),
                                        (int)Math.round(11.0*Xpr2PdfWorker.DPI_DEFAULT)));
      // construct the rest of the page size map
      pageSizes.put(PrintOptions.PageSize.LEGAL,
                    new NativeDimension((int)Math.round(8.5*Xpr2PdfWorker.DPI_DEFAULT),
                                        (int)Math.round(14.0*Xpr2PdfWorker.DPI_DEFAULT)));
      pageSizes.put(PrintOptions.PageSize.A0,
                    new NativeDimension((int)Math.round(33.1*Xpr2PdfWorker.DPI_DEFAULT),
                                        (int)Math.round(46.8*Xpr2PdfWorker.DPI_DEFAULT)));
      pageSizes.put(PrintOptions.PageSize.A1,
                    new NativeDimension((int)Math.round(23.4*Xpr2PdfWorker.DPI_DEFAULT),
                                        (int)Math.round(33.1*Xpr2PdfWorker.DPI_DEFAULT)));
      pageSizes.put(PrintOptions.PageSize.A2,
                    new NativeDimension((int)Math.round(16.5*Xpr2PdfWorker.DPI_DEFAULT),
                                        (int)Math.round(23.4*Xpr2PdfWorker.DPI_DEFAULT)));
      pageSizes.put(PrintOptions.PageSize.A3,
                    new NativeDimension((int)Math.round(11.7*Xpr2PdfWorker.DPI_DEFAULT),
                                        (int)Math.round(16.5*Xpr2PdfWorker.DPI_DEFAULT)));
      pageSizes.put(PrintOptions.PageSize.A4,
                    new NativeDimension((int)Math.round(8.3*Xpr2PdfWorker.DPI_DEFAULT),
                                        (int)Math.round(11.7*Xpr2PdfWorker.DPI_DEFAULT)));
   }
   
   /**
    * Method called at server bootstrap that initializes values from the directory
    * configuration. Until this method is called, default values are used.
    */
   public static void bootstrap()
   {
      // default page size defined in JVM for currently selected printer
      PageFormat pgFmt = PrinterJob.getPrinterJob().defaultPage();
      // size in pixels from Java
      // the page margins will be 0 in this case
      if (pgFmt != null)
      {
         pdfPageWidth = (int)pgFmt.getWidth();
         pdfPageHeight = (int)pgFmt.getHeight();
      }
      else
      {
         // this must always exist
         NativeDimension pageSizeNative = pageSizes.get(PrintOptions.PageSize.LETTER);
         pdfPageWidth = pageSizeNative.width;
         pdfPageHeight = pageSizeNative.height;
      }
      // clien-server mode can override several options
      if (!simpleUse)
      {
         imageOnErrorIconMode = Utils.getDirectoryNodeBoolean(null,
                                                              "xpr-image-loading-error-silent",
                                                              imageOnErrorIconMode);
         pdfDPI = Utils.getDirectoryNodeInt(null, "xpr-dpi", pdfDPI);
         // gets the page options overridden in directory
         PrintOptions prnOpt = PrintingService.getPrintOptions();
         if (prnOpt != null)
         {
            NativeDimension pageSizeNative = pageSizes.get(prnOpt.pageSize);
            if (pageSizeNative != null)
            {
               if (prnOpt.pageOrientation == PrintOptions.PageOrientation.PORTRAIT)
               {
                  pdfPageWidth = pageSizeNative.width;
                  pdfPageHeight = pageSizeNative.height;
               }
               else
               {
                  pdfPageWidth = pageSizeNative.height;
                  pdfPageHeight = pageSizeNative.width;
               }
               // need to adjust page size depending on current margins
               // prnOpts defines margins in mm
               pdfPageWidth -= (int)Math.round((double)(prnOpt.marginLeft + prnOpt.marginRight) *
                                               (double)Xpr2PdfWorker.DPI_DEFAULT /
                                               XprEntity.INCH_2_MM);
               pdfPageHeight -= (int)Math.round((double)(prnOpt.marginTop + prnOpt.marginBottom) *
                                                (double)Xpr2PdfWorker.DPI_DEFAULT /
                                                XprEntity.INCH_2_MM);
            }
         }
      }
   }
   
   /**
    * Convert the given XPR file to a temporary PDF file with a unique name. Stream the PDF file
    * to the client and then delete it.
    * 
    * @param   xprFileName
    *          The name of the XPR file to process.
    */
   public static void printPdf(character xprFileName)
   {
      if (xprFileName.isUnknown())
      {
         return;
      }
      
      // call method accepting string parameter
      printPdf(xprFileName.toStringMessage());
   }

   /**
    * Convert the given XPR file to a temporary PDF file with a unique name. Stream the PDF file
    * to the client and then delete it.
    * 
    * @param   xprFileName
    *          The name of the XPR file to process.
    */
   public static void printPdf(String xprFileName)
   {
      // generate a random name for the temporary PDF file, including a path at the same location
      // as the XPR file
      String path = xprFileName.replace('\\', '/');
      int pos = path.lastIndexOf('/');
      path = (pos >= 0 ? path.substring(0, pos + 1) : "");
      String pdfFileName = path + RandomWordGenerator.create(32, 8, 0) + PDF_SUFFIX;
      
      // generate the PDF file
      printFile(xprFileName, pdfFileName);
      
      // stream it to the client
      WebBrowserManager.openMimeResource(PDF_MIME, FILE_PROTOCOL + pdfFileName, false);
      
      // delete the temporary file
      // TODO: enable once fix for #3571 is available
      //FileSystemOps.delete(new String[] { pdfFileName }, false);
   }

   /**
    * Convert the given XPR file to PDF.
    * 
    * @param   xprFileName
    *          The name of the XPR file to process.
    * @param   pdfFileName
    *          The name of the PDF file to create.
    */
   public static void printFile(character xprFileName, character pdfFileName)
   {
      if (xprFileName.isUnknown() || pdfFileName.isUnknown())
      {
         return;
      }
      
      // call method accepting string parameters
      printFile(xprFileName.toStringMessage(), pdfFileName.toStringMessage());
   }

   /**
    * Convert the given XPR file to PDF.
    * 
    * @param   xprFileName
    *          The name of the XPR file to process.
    * @param   pdfFileName
    *          The name of the PDF file to create.
    */
   public static void printFile(String xprFileName, character pdfFileName)
   {
      if (pdfFileName.isUnknown())
      {
         return;
      }

      // call method accepting string parameters
      printFile(xprFileName, pdfFileName.toStringMessage());
   }

   /**
    * Convert the given XPR file to PDF.
    * 
    * @param   xprFileName
    *          The name of the XPR file to process.
    * @param   pdfFileName
    *          The name of the PDF file to create.
    */
   public static void printFile(character xprFileName, String pdfFileName)
   {
      if (xprFileName.isUnknown())
      {
         return;
      }
      
      // call method accepting string parameters
      printFile(xprFileName.toStringMessage(), pdfFileName);
   }

   /**
    * Convert the given XPR file to PDF.
    * 
    * @param   xprFileName
    *          The name of the XPR file to process.
    * @param   pdfFileName
    *          The name of the PDF file to create.
    */
   public static void printFile(String xprFileName, String pdfFileName)
   {
      printFile(xprFileName, pdfFileName, false);
   }
   
   /**
    * Conputes the value of row to pixel factor.
    *
    * @param   dpi
    *          The device dependent DPI.
    * @param   lpi
    *          The lines per inch value.
    *
    * @return  pixels per row for given device.
    */
   public static double getRow2Pix(int dpi, double lpi)
   {
      return (double)dpi / lpi;
   }

   /**
    * Conputes the value of column to pixel factor.
    *
    * @param   dpi
    *          The device dependent DPI.
    * @param   cpi
    *          The columns per inch value.
    *
    * @return  pixels per column for given device.
    */
   public static double getCol2Pix(int dpi, double cpi)
   {
      return (double)dpi / cpi;
   }
   
   /**
    * Returns <code>TRUE</code> if class was executed in standalone command line mode without
    * server and client.
    *
    * @return  flag indicating standalone mode usage.
    */
   public static boolean isSimpleUseMode()
   {
      return simpleUse;
   }

   /**
    * Set the simple use flag.
    * 
    * @param mode the new simple use flag value
    */
   public static void setSimpleUseMode(boolean mode)
   {
      simpleUse = mode;
   }

   /**
    * Returns <code>TRUE</code> if PDF file should ignore missing image file displaying icon
    * instead of missing file.
    *
    * @return  flag indicating to use ICON mode for image loading error.
    */
   public static boolean onErrorImageSilentMode()
   {
      return imageOnErrorIconMode;
   }
   
   /**
    * Returns the with the output PDF file expects to have.
    *
    * @return  Page width in pixels.
    */
   public static int getPdfPageWidth()
   {
      return pdfPageWidth;
   }
   
   /**
    * Returns the height the output PDF file expects to have.
    *
    * @return  Page height in pixels.
    */
   public static int getPdfPageHeight()
   {
      return pdfPageHeight;
   }
   
   /**
    * Set up new page size for current PDF based report generation.  Should be in a list of the supported
    * sizes.  LETTER, LEGAL, A0-A4.  Otherwise the method does nothing.
    * 
    * @param   newPageSize
    *          The new page size.
    */
   public static void setPdfPageSize(PrintOptions.PageSize newPageSize)
   {
      NativeDimension newPageSizeNative = pageSizes.get(newPageSize);
      if (newPageSizeNative != null)
      {
         // some options can be overridden
         PrintOptions prnOpt = PrintingService.getPrintOptions();
         if (prnOpt != null)
         {
            if (prnOpt.pageOrientation == PrintOptions.PageOrientation.PORTRAIT)
            {
               pdfPageWidth = newPageSizeNative.width;
               pdfPageHeight = newPageSizeNative.height;
            }
            else
            {
               pdfPageWidth = newPageSizeNative.height;
               pdfPageHeight = newPageSizeNative.width;
            }
            // need to adjust page size depending on current margins
            // prnOpts defines margins in mm
            pdfPageWidth -= (int)Math.round((double)(prnOpt.marginLeft + prnOpt.marginRight) *
                                            (double)Xpr2PdfWorker.DPI_DEFAULT /
                                            XprEntity.INCH_2_MM);
            pdfPageHeight -= (int)Math.round((double)(prnOpt.marginTop + prnOpt.marginBottom) *
                                             (double)Xpr2PdfWorker.DPI_DEFAULT /
                                             XprEntity.INCH_2_MM);
         }
         else
         {
            // this is the default for given size
            pdfPageWidth = newPageSizeNative.width;
            pdfPageHeight = newPageSizeNative.height;
         }
      }
      else
      {
         logInfo("XprHelper does not suppport the requested page size: " + newPageSize);
      }
   }
   
   /**
    * Returns the current DPI value for device independent operations.
    *
    * @return  Page current DPI value.
    */
   public static int getPdfDPI()
   {
      return pdfDPI;
   }
   
   /**
    * Obtains input stream object for given filename depending on run time mode.
    * 
    * @param   fileName
    *          The name of the file to open.
    *
    * @return  The input stream object or <code>NULL</code> in case of failure.
    */
   public static InputStream getInputStream(String fileName)
   {
      InputStream isRes = null;
      // simple error checking for input parameters
      if (fileName == null || fileName.isEmpty())
      {
         return isRes;
      }
      // depends on run time mode
      if (simpleUse)
      {
         File inputFile = new File(fileName);
         if (inputFile.exists())
         {
            try
            {
               isRes = new FileInputStream(inputFile);
            }
            catch (Exception e)
            {
               isRes = null;
            }
         }
      }
      else
      {
         // open stream for reading
         Stream inpStream = StreamFactory.openFileStream(fileName, false, false);
         if (inpStream != null)
         {
            isRes = new InputStreamWrapper(inpStream);
         }
      }
      return isRes;
   }

   /**
    * Convert the given XPR file to PDF.
    * 
    * @param   xprFileName
    *          The name of the XPR file to process.
    * @param   pdfFileName
    *          The name of the PDF file to create.
    * @param   simpleUse
    *          The flag indicating simple call via main without client/server running.
    */
   private static void printFile(String xprFileName, String pdfFileName, boolean simpleUse)
   {
      // simple error checking for input parameters
      if (xprFileName == null || xprFileName.isEmpty())
      {
         return;
      }
      
      // we can differentiate this case using same name but replacing file extension
      // for example file.xpr to become file.pdf, currently just return as invalid condition
      if (pdfFileName == null || pdfFileName.isEmpty())
      {
         return;
      }
      
      // simple use requires alternative reader to create
      XprEntity xprInput = null;
      if (simpleUse)
      {
         File inputXpr = new File(xprFileName);
         if (inputXpr.exists())
         {
            try
            {
               xprInput = new XprEntity(new FileReader(inputXpr));
            }
            catch (Exception e)
            {
               xprInput = null;
            }
         }
      }
      else
      {
         // open stream for reading
         Stream inpStream = StreamFactory.openFileStream(xprFileName, false, false);
         if (inpStream != null)
         {
            InputStreamWrapper isw = new InputStreamWrapper(inpStream);
            try
            {
               xprInput = new XprEntity(new InputStreamReader(isw));
            }
            catch (Exception e)
            {
               displayOrLogError(String.format("XprHelper has failed to create parser for file %s.",
                                               xprFileName), e);
               xprInput = null;
            }
         }
      }
      // continue if input entity is OK
      if (xprInput == null)
      {
         // unable to get XPR file to handle
         return;
      }
      else
      {
         // intercept parser errors
         try
         {
            // start handling XPR file
            xprInput.init();
         }
         catch (Exception e)
         {
            displayOrLogError(String.format(
               "XprHelper has faced unexpected problem with handling XPR file %s." +
               " Report generation will be ignored.", xprFileName), e);
            
            // skip report generation to avoid Jasper failures
            return;
         }
         
         // intercept Jasper related issues tp avoid client abend too
         try
         {
            // time to create jasper related worker
            Xpr2PdfWorker pdfOutput = new Xpr2PdfWorker(xprInput, pdfFileName);
            // export to PDF
            if (!pdfOutput.export(simpleUse))
            {
               displayOrLogError(String.format("XprHelper export failed for %s.", pdfFileName));
            }
         }
         catch (Exception e)
         {
            displayOrLogError(String.format(
               "XprHelper has faced unexpected problem with report generation for PDF file %s.",
                pdfFileName), e);
         }
      }
   }
   
   /**
    * Log the error and display in case of simple usage when no server and clients are running.
    * 
    * @param     msg
    *            The message to log or display.
    */
   public static void displayOrLogError(String msg)
   {
      displayOrLogError(msg, null, false);
   }
   
   /**
    * Log the error and optionally display in case of simple usage when no server and clients are
    * running.
    * 
    * @param     msg
    *            The message to log or display.
    * @param     showError
    *            When <code>TRUE</code> display error message, <code>FALSE</code> - only logging.
    */
   public static void displayOrLogError(String msg, boolean showError)
   {
      displayOrLogError(msg, null, showError);
   }
   
   /**
    * Log the error and display in case of simple usage when no server and clients are running.
    * 
    * @param     msg
    *            The message to log or display.
    * @param     excpt
    *            Optional exception to get issue details.
    */
   public static void displayOrLogError(String msg, Exception excpt)
   {
      displayOrLogError(msg, excpt, false);
   }
   
   /**
    * Log the error and optionally display in case of simple usage when no server and clients are
    * running.
    * 
    * @param     msg
    *            The message to log or display.
    * @param     excpt
    *            Optional exception to get issue details.
    * @param     showError
    *            When <code>TRUE</code> display error message, <code>FALSE</code> - only logging.
    */
   public static void displayOrLogError(String msg, Exception excpt, boolean showError)
   {
      // if running from command line - perform only logging
      // sometimes it is better to ignore message box display even not in simle mode
      StringBuilder sbMsg = new StringBuilder();
      boolean needNL = false;
      // adding possible message prefix
      if (msg != null && !msg.isEmpty())
      {
         needNL = true;
         sbMsg.append(msg);
      }
      // adding exception details
      if (excpt != null)
      {
         if (needNL)
         {
            sbMsg.append(".\n");
         }
         sbMsg.append(excpt.getMessage());
      }
      // make final string
      String errMsgStr = sbMsg.toString();
      if (errMsgStr == null || errMsgStr.isEmpty())
      {
         errMsgStr = "General XPR Helper failure.";
      }
      // let's log or show error, do not throw the exception to interrupt the client in any case
      if (simpleUse || !showError)
      {
         LOG.log(Level.SEVERE, errMsgStr);
      }
      else
      {
         showError(errMsgStr);
      }
   }
   
   /**
    * Log the information message to the stadard error stream.  The failure is not severe but log
    * entry can be useful.
    * 
    * @param     msg
    *            The message to log.
    */
   public static void logInfo(String msg)
   {
      if (msg == null || msg.isEmpty())
      {
         msg = "Info logger call requested but the message is null or empty. Check the source.";
      }
      // log the final message
      LOG.log(Level.INFO, msg);
   }
   
   /**
    * The command line entry point.
    * 
    * @param     args
    *            The array of command-line parameters.
    */
   public static void main(String[] args)
   {
      simpleUse = true;
      
      System.out.println("Press ENTER to start...");
      try
      {
         System.in.read();
      }
      catch (Exception e)
      {
         return;
      }
      
      if (args.length != 2 && args.length != 3 || (args.length == 1 && args[0].equals("-?")))
      {
         System.out.println("XPR to PDF Jasper based converter. Valid usage:");
         System.out.println("java XprHelper xprFilename pdfFilename [pageFormat]");
         System.out.println();
         return;
      }
      
      // override the page size in simple mode 
      if (args.length == 3)
      {
         // new page name string
         String newPageName = args[2].toUpperCase();
         // evaluate the requested page size
         NativeDimension pageSizeNative = pageSizes.get(
                                             PrintOptions.PageSize.valueOf(newPageName));
         if (pageSizeNative != null)
         {
            pdfPageWidth = pageSizeNative.width;
            pdfPageHeight = pageSizeNative.height;
            System.out.println("Changed page format to: "+newPageName);
         }
      }
      
      // calling worker
      printFile(args[0], args[1], simpleUse);
   }
}