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