# 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);
       }
    }