# JasperReportWrapper.java

    /*
    ** Module   : JasperReportWrapper.java
    ** Abstract : Jasper reporting facility implemented as 4GL syntax extension.
    **
    ** Copyright (c) 2017-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description----------------------------------
    ** 001 SVL 20171030 Created initial version.
    ** 002 SVL 20180205 Switched to data sources concept.
    ** 003 HC  20180410 Renamed ReportOutputFormat to MediaType.
    ** 004 EVL 20180423 Renamed Report to FwdReport.  Adding support for decimal to Integer mapping.
    **                  Also added case when we will bypass data mapping.  Fixes jasperreport file
    **                  handling exceptions that causing application to end.
    ** 005 OM  20181003 Text in cells is cut only when constrained by page layout (PDF).
    ** 006 CA  20191005 Added export-report-html for FWD report.
    ** 006 EVL 20210212 Adding methods to handle chart related reports.
    **     EVL 20210305 Adding methods to handle rtf/docx related reports.  Fixed output stream issue for RTF
    **                  exporter.
    ** 007 RNC 20250131 When there are too many columns in a PDF report, split the report into multiple pages
    *                   rather than shrinking the cells to fit.
    */

    /*
    ** 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.persist.*;
    import com.goldencode.p2j.ui.BrowseWidget;
    import com.goldencode.p2j.util.*;
    import net.sf.jasperreports.engine.*;
    import net.sf.jasperreports.engine.base.JRBasePrintPage;
    import net.sf.jasperreports.engine.export.*;
    import net.sf.jasperreports.engine.export.ooxml.JRDocxExporter;
    import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
    import net.sf.jasperreports.engine.fill.JRTemplatePrintText;
    import net.sf.jasperreports.export.*;

    import java.io.*;
    import java.math.*;
    import java.util.*;

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

    /**
     * Jasper reporting facility implemented as 4GL syntax extension.
     */
    class JasperReportWrapper
    implements FwdReport
    {
       /** Buffer size for exporting report to a file. */
       private final static int OUTPUT_BUFFER_SIZE = 64000;

       /** Report parameters (the ones which were set in 4GL code). */
       private Map<String, BaseDataType> parameters;

       /** Data source used by this report. */
       private JasperDataSource dataSource;

       /** File name of the report design file. */
       private String designFileName;

       /** The resource ID. */
       private Long id;

       /**
        * Get Jasper-compatible value from the 4GL-compatible value. In most cases Jasper-compatible
        * value has Java primitive data type.
        *
        * @param   value
        *          4GL-compatible value
        * @param   jasperValueClass
        *          Expected Jasper-compatible data type.
        *
        * @return  Jasper-compatible value.
        *
        * @throws IllegalArgumentException
        *         If 4GL-compatible data type is not supported or doesn't match expected
        *         Jasper-compatible data type.
        */
       static Object getJavaValue(BaseDataType value, Class<?> jasperValueClass)
       {
          // first handle case when no need to make data transformation
          if (value.getClass().equals(jasperValueClass))
          {
             return value;
          }
          else if (value instanceof Text && String.class.equals(jasperValueClass))
          {
             return ((character) value).getValue();
          }
          else if (value instanceof logical && Boolean.class.equals(jasperValueClass))
          {
             return !value.isUnknown() ? ((logical) value).getValue() : null;
          }
          else if (value instanceof int64)
          {
             if (Integer.class.equals(jasperValueClass))
             {
                return ((int64) value).toJavaIntegerType();
             }
             else if (Long.class.equals(jasperValueClass))
             {
                return ((int64) value).toJavaLongType();
             }
             else if (Short.class.equals(jasperValueClass))
             {
                Integer val = ((int64) value).toJavaIntegerType();
                return val != null ? val.shortValue() : null;
             }
          }
          else if (value instanceof decimal)
          {
             if (Double.class.equals(jasperValueClass))
             {
                BigDecimal val = ((decimal) value).toBigDecimal();
                return val != null ? val.doubleValue() : null;
             }
             else if (Float.class.equals(jasperValueClass))
             {
                BigDecimal val = ((decimal) value).toBigDecimal();
                return val != null ? val.floatValue() : null;
             }
             else if (Integer.class.equals(jasperValueClass))
             {
                BigDecimal val = ((decimal) value).toBigDecimal();
                return val != null ? val.intValueExact() : null;
             }
             else if (BigDecimal.class.equals(jasperValueClass))
             {
                return ((decimal) value).toBigDecimal();
             }
          }
          else if (value instanceof date && Date.class.equals(jasperValueClass))
          {
             return !value.isUnknown() ? ((date) value).dateValue() : null;
          }

          throw new IllegalArgumentException(
                String.format("Value of type %s cannot be converted to Jasper report parameter " +
                      "of type %s", value.getClass(), jasperValueClass));
       }

       /**
        * Set the parameter used by the report.
        *
        * @param  param
        *         Name of the parameter.
        * @param  value
        *         Value of the parameter.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical setReportParameter(String param, BaseDataType value)
       {
          return setReportParameter(new character(param), value);
       }

       /**
        * Set the parameter used by the report.
        *
        * @param  param
        *         Name of the parameter.
        * @param  value
        *         Value of the parameter.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical setReportParameter(String param, String value)
       {
          return setReportParameter(new character(param), new character(value));
       }

       /**
        * Set the parameter used by the report.
        *
        * @param  param
        *         Name of the parameter.
        * @param  value
        *         Value of the parameter.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical setReportParameter(character param, BaseDataType value)
       {
          if (param.isUnknown())
          {
             showError("Report parameter name cannot be an unknown value.");
             return new logical(false);
          }

          if (parameters == null)
          {
             parameters = new HashMap<>();
          }
          parameters.put(param.getValue(), value);

          return new logical(true);
       }

       /**
        * Run report end export result to the PDF file.
        *
        * @param  fileName
        *         Name of the output PDF file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportPdf(String fileName)
       {
          return exportReportPdf(new character(fileName));
       }

       /**
        * Run report end export result to the PDF file.
        *
        * @param  fileName
        *         Name of the output PDF file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportPdf(character fileName)
       {
          return exportReport(fileName, MediaType.PDF);
       }

       /**
        * Run report end export result to the XLS file.
        *
        * @param  fileName
        *         Name of the output XLS file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportXls(String fileName)
       {
          return exportReportXls(new character(fileName));
       }

       /**
        * Run report end export result to the XLS file.
        *
        * @param  fileName
        *         Name of the output XLS file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportXls(character fileName)
       {
          return exportReport(fileName, MediaType.XLS);
       }

       /**
        * Run report end export result to the XLSX file.
        *
        * @param  fileName
        *         Name of the output XLSX file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportXlsx(String fileName)
       {
          return exportReportXlsx(new character(fileName));
       }

       /**
        * Run report end export result to the XLSX file.
        *
        * @param  fileName
        *         Name of the output XLSX file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportXlsx(character fileName)
       {
          return exportReport(fileName, MediaType.XLSX);
       }

       /**
        * Run report end export result to the RTF file.
        *
        * @param  fileName
        *         Name of the output RTF file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportRtf(String fileName)
       {
          return exportReportRtf(new character(fileName));
       }

       /**
        * Run report end export result to the RTF file.
        *
        * @param  fileName
        *         Name of the output RTF file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportRtf(character fileName)
       {
          return exportReport(fileName, MediaType.RTF);
       }

       /**
        * Run report end export result to the DOCX file.
        *
        * @param  fileName
        *         Name of the output DOCX file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportDocx(String fileName)
       {
          return exportReportDocx(new character(fileName));
       }

       /**
        * Run report end export result to the DOCX file.
        *
        * @param  fileName
        *         Name of the output DOCX file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportDocx(character fileName)
       {
          return exportReport(fileName, MediaType.DOCX);
       }

       /**
        * Run report end export result to the CSV file.
        *
        * @param  fileName
        *         Name of the output CSV file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportCsv(String fileName)
       {
          return exportReportCsv(new character(fileName));
       }

       /**
        * Run report end export result to the CSV file.
        *
        * @param  fileName
        *         Name of the output CSV file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportCsv(character fileName)
       {
          return exportReport(fileName, MediaType.CSV);
       }

       /**
        * Import CSV file as data source for report generation.
        *
        * @param  fileName
        *         Name of the output CSV file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical importCsvDataSource(character fileName)
       {
          return importCsvDataSource(fileName.toStringMessage());
       }

       /**
        * Import CSV file as data source for report generation.
        *
        * @param  fileName
        *         Name of the output CSV file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical importCsvDataSource(String fileName)
       {
          return new logical(true);
       }

       /**
        * Gets the total coluns range of the report sheet.
        *
        * @param  sheetName
        *         The name of the sheet to inspect.
        *
        * @return The total columns count.
        */
       @Override
       public integer getSheetRangeColumns(character sheetName)
       {
          return getSheetRangeColumns(sheetName.toStringMessage());
       }

       /**
        * Gets the total coluns range of the report sheet.
        *
        * @param  sheetName
        *         The name of the sheet to inspect.
        *
        * @return The total columns count.
        */
       @Override
       public integer getSheetRangeColumns(String sheetName)
       {
          int iRet = 0;

          return new integer(iRet);
       }

       /**
        * Gets the total rows range of the report sheet.
        *
        * @param  sheetName
        *         The name of the sheet to inspect.
        *
        * @return The total rows count.
        */
       @Override
       public integer getSheetRangeRows(character sheetName)
       {
          return getSheetRangeRows(sheetName.toStringMessage());
       }

       /**
        * Gets the total rows range of the report sheet.
        *
        * @param  sheetName
        *         The name of the sheet to inspect.
        *
        * @return The total rows count.
        */
       @Override
       public integer getSheetRangeRows(String sheetName)
       {
          int iRet = 0;

          return new integer(iRet);
       }

       /**
        * Starts to prepare data for new chart.
        */
       @Override
       public void addChart()
       {
       }

       /**
        * Gets the total rows range of the report sheet.
        *
        * @param  pictName
        *         The name of the picture file.
        * @param  x
        *         X position of the picture.
        * @param  y
        *         Y position of the picture.
        * @param  w
        *         Width of the picture.
        * @param  h
        *         Height of the picture.
        */
       @Override
       public void chartAddPicture(character pictName, int x, int y, int w, int h)
       {
          chartAddPicture(pictName.toStringMessage(), x, y, w, h);
       }

       /**
        * Gets the total rows range of the report sheet.
        *
        * @param  pictName
        *         The name of the picture file.
        * @param  x
        *         X position of the picture.
        * @param  y
        *         Y position of the picture.
        * @param  w
        *         Width of the picture.
        * @param  h
        *         Height of the picture.
        */
       @Override
       public void chartAddPicture(String pictName, int x, int y, int w, int h)
       {
          // TODO: Implement this
       }

       /**
        * Run report end export result to the HTML file.
        *
        * @param  fileName
        *         Name of the output HTML file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportHtml(String fileName)
       {
          return exportReportHtml(new character(fileName));
       }

       /**
        * Run report end export result to the HTML file.
        *
        * @param  fileName
        *         Name of the output HTML file.
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReportHtml(character fileName)
       {
          return exportReport(fileName, MediaType.HTML);
       }

       /**
        * Set report design file.
        *
        * @param designFileName
        *        Name of the report design file.
        */
       @Override
       public void setReportDesign(character designFileName)
       {
          if (designFileName.isUnknown() || designFileName.getValue().isEmpty())
          {
             showError("Report design file name cannot be an unknown value or empty string.");
             return;
          }

          this.designFileName = designFileName.getValue();
       }

       /**
        * Set report design file.
        *
        * @param designFileName
        *        Name of the report design file.
        */
       @Override
       public void setReportDesign(String designFileName)
       {
          setReportDesign(new character(designFileName));
       }

       /**
        * Get the name of the report design file.
        *
        * @return the name of the report design file.
        */
       @Override
       public character getReportDesign()
       {
          return new character(designFileName);
       }

       /**
        * Set the data source used by this report.
        *
        * @param dataSource
        *        Handle to the data source.
        */
       @Override
       public void setReportDataSource(handle dataSource)
       {
          Object resource = dataSource.get();

          if (resource instanceof JasperDataSource)
          {
             this.dataSource = (JasperDataSource) resource;
          }
          else if (resource instanceof QueryWrapper)
          {
             this.dataSource = new QueryJasperDataSource((QueryWrapper) resource);
          }
          else if (resource instanceof BrowseWidget)
          {
             BrowseWidget browse = (BrowseWidget) resource;
             // designFileName will be set later when we know the output format
             this.dataSource = new BrowseJasperDataSource(browse);
          }
          else
          {
             showError(String.format(
                   "Invalid handle of type %s was was passed an report's DATA-SOURCE attribute",
                   ((HandleResource) resource).type()));
          }
       }

       /**
        * Set the query used as the data source by this report.
        *
        * @param dataSource
        *        Handle to the query used as the data source by this report.
        */
       @Override
       public void setReportDataSource(QueryWrapper dataSource)
       {
          setReportDataSource(new handle(dataSource));
       }

       /**
        * Get the data source used by this report.
        *
        * @return handle to the data source used by this report.
        */
       @Override
       public handle getReportDataSource()
       {
          return dataSource.asProgressObject();
       }

       /**
        * Reports if this object is valid for use.
        *
        * @return   always <code>true</code>.
        */
       @Override
       public boolean valid()
       {
          return true;
       }

       /**
        * Reports if this object is unknown.
        *
        * @return   always <code>false</code>.
        */
       @Override
       public boolean unknown()
       {
          return false;
       }

       /**
        * Get this resource's ID, if is already set.
        * <p>
        * This is for internal usage only.  If you need the resource ID, use
        * {@link handle#resourceId}, which will compute it and save it at the resource, too.
        *
        * @return   The resource's ID or <code>null</code> if not set.
        */
       @Override
       public Long id()
       {
          return id;
       }

       /**
        * Set this resource's ID.
        *
        * @param    id
        *           The resource's ID.
        */
       @Override
       public void id(long id)
       {
          this.id = id;
       }

       /**
        * Run report end export result using the specified format to the specified file.
        *
        * @param  outputFileName
        *         Name of the output file.
        * @param  outputFormat
        *         Output format (e.g. PDF).
        *
        * @return <code>true</code> on success.
        */
       @Override
       public logical exportReport(character outputFileName, MediaType outputFormat)
       {
          if (designFileName == null)
          {
             showError("Cannot execute report: REPORT-DESIGN is not set.");
             return new logical(false);
          }

          if (dataSource == null)
          {
             showError("Cannot execute report: REPORT-DATA-SOURCE is not set.");
             return new logical(false);
          }

          if (outputFileName.isUnknown())
          {
             showError(String.format("Output file name is not specified for report %s.",
                                     designFileName));
             return new logical(false);
          }

          // load report file
          JasperReport report = dataSource.getReport(designFileName);
          if (report == null)
          {
             return new logical(false);
          }

          // convert Report parameters from 4GL to Jasper data types
          Map<String, Object> jasperParameters = new HashMap<>();
          if (parameters != null)
          {
             for (JRParameter parameter : report.getParameters())
             {
                String parameterName = parameter.getName();
                BaseDataType progressValue = parameters.get(parameterName);
                if (progressValue != null)
                {
                   try
                   {
                      jasperParameters.put(parameterName, getJavaValue(progressValue,
                                                                       parameter.getValueClass()));
                   }
                   catch (IllegalArgumentException e)
                   {
                      showError("Invalid data type of report parameter " + parameterName, e);
                      return new logical(false);
                   }
                }
             }
          }

          if (!dataSource.reportStarted(report))
          {
             return new logical(false);
          }

          // fill report with data
          boolean error = false;
          try
          {
             JasperPrint jasperPrint;
             try
             {
                jasperPrint = JasperFillManager.fillReport(report, jasperParameters, dataSource);
             }
             catch (JRException e)
             {
                error = true;
                showError(String.format("Cannot fill report %s", designFileName), e);
                return new logical(false);
             }
             catch (JasperReportException jre)
             {
                error = true;
                showError(String.format("Internal error for the report %s", designFileName), jre);
                return new logical(false);
             }

             // if columns overflowed the page width, we need to split each page
             String colNoProperty = report.getProperty("colNo");
             if (colNoProperty != null)
             {
                int colNo = Integer.parseInt(colNoProperty);
                int splitsPerPage = Integer.parseInt(report.getProperty("splitsPerPage"));
                List<JRPrintPage> oldPages = jasperPrint.getPages();
                List<JRPrintPage> newPages = splitPages(oldPages, colNo, splitsPerPage);

                oldPages.clear();
                oldPages.addAll(newPages);
             }

             // export report
             String output = outputFileName.getValue();
             Stream reportStream;
             try
             {
                reportStream = StreamFactory.openFileStream(output, true, false);
             }
             catch (ErrorConditionException e)
             {
                error = true;
                showError(String.format("Cannot export report stream for file %s", output), e);
                return new logical(false);
             }

             OutputStream streamWrapper =
                   new BufferedOutputStream(new OutputStreamWrapper(reportStream), OUTPUT_BUFFER_SIZE);
             try
             {
                Exporter exporter;
                switch (outputFormat)
                {
                   case CSV:
                      exporter = new JRCsvExporter();
                      exporter.setExporterOutput(new SimpleWriterExporterOutput(streamWrapper));
                      break;

                   case DOCX:
                      exporter = new JRDocxExporter();
                      exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(streamWrapper));
                      break;

                   case HTML:
                      exporter = new HtmlExporter();
                      exporter.setExporterOutput(new SimpleHtmlExporterOutput(streamWrapper));
                      break;

                   case PDF:
                      exporter = new JRPdfExporter();
                      exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(streamWrapper));
                      break;

                   case RTF:
                      exporter = new JRRtfExporter();
                      exporter.setExporterOutput(new SimpleWriterExporterOutput(streamWrapper));
                      break;

                   case XLS:
                      exporter = new JRXlsExporter();
                      exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(streamWrapper));
                      break;

                   case XLSX:
                      exporter = new JRXlsxExporter();
                      exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(streamWrapper));
                      break;

                   default:
                      throw new IllegalArgumentException("Invalid export format " + outputFormat);
                }
                exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
                exporter.exportReport();
             }
             catch (JRException e)
             {
                error = true;
                showError(String.format("Cannot export report %s into %s",
                      designFileName, output), e);
                return new logical(false);
             }
             finally
             {
                try
                {
                   streamWrapper.flush();
                   streamWrapper.close();
                }
                catch (IOException e)
                {
                   if (!error)
                   {
                      error = true;
                      showError(String.format("Cannot flush report %s into %s",
                            designFileName, output), e);
                      return new logical(false);
                   }
                }
             }
          }
          finally
          {
             dataSource.reportFinished(error);
          }

          return new logical(true);
       }

       /**
        * Splits every given page into multiple ones. A fresh page will be created by moving the column
        * and data cells until it detects that the X coordinate goes back to the start of the page.
        * The X coordinate need to be already arranged before the splitting is performed because the method
        * will just copy them as they are to new pages.<p>
        * Used primarily when there are too many columns in a PDF report, being an alternative to shrinking
        * down the columns in order for all to fit, which can make the cells unintelligible.
        *
        * @param  pages
        *         The pages which will be split.
        * @param  colNo
        *         The number of columns for the data source. Used to iterate through the data cells.
        * @param  splitsPerPage
        *         Represents how many times we need to create a fresh page due to a split.
        *
        * @return A list with all the fresh pages created.
        */
       private List<JRPrintPage> splitPages(List<JRPrintPage> pages, int colNo, int splitsPerPage)
       {
          List<JRPrintPage> newPages = new ArrayList<>();
          if (pages == null || pages.isEmpty())
          {
             return newPages;
          }

          if (colNo == 1)
          {
             return pages;
          }

          int startRow = 1;
          int columnStart;
          for (int i = 0, len = pages.size(); i < len; i++)
          {
             JRPrintPage page = pages.get(i);
             List<JRPrintPage> freshPages = splitPage(page, i, colNo, len, splitsPerPage, startRow);
             if (freshPages != null)
             {
                newPages.addAll(freshPages);
             }

             columnStart = i == 0 ? 1 : 0;
             startRow += (page.getElements().size() - columnStart - colNo - 2) / colNo;
          }

          return newPages;
       }

       /**
        * Actual implementation of splitting a single page into multiple ones.
        *
        * @param  page
        *         The page which will be split.
        * @param  pageNo
        *         The number of the page from the original report. If <code>0</code>,
        *         it will assume the first element is a report title, and it will include it.
        * @param  colNo
        *         The number of columns for the data source. Used to iterate through the data cells.
        * @param  oldPagesNumber
        *         The size (pages number) of the report from which the page was taken. Used to customize the
        *         pagination footer.
        * @param  splitsPerPage
        *         Represents how many times we need to create a fresh page due to a split. Used to customize
        *         the pagination footer.
        * @param  startRow
        *         The 1-indexed number of the first row present in the page. Used to customize the pagination
        *         footer.
        *
        * @return A list with the fresh pages created, or <code>null</code> if the page is null or empty,
        *         or the column number is less or equal than 1 which means the page can't be split.
        */
       private List<JRPrintPage> splitPage(JRPrintPage page, int pageNo, int colNo,
                                           int oldPagesNumber, int splitsPerPage, int startRow)
       {
          if (page == null)
          {
             return null;
          }

          List<JRPrintElement> oldElements = page.getElements();
          if (pageNo < 0 || oldElements == null || oldElements.isEmpty() || colNo <= 1)
          {
             return null;
          }

          List<JRPrintPage> newPages = new ArrayList<>();
          JRPrintPage newPage = new JRBasePrintPage();

          List<JRPrintElement> newElements = newPage.getElements();
          int oldElementsSize = oldElements.size();
          int columnStart = 0;

          if (pageNo == 0)
          {
             // add the report title
             columnStart = 1;
             newElements.add(oldElements.get(0));
          }

          int totalPageNumber = (splitsPerPage + 1) * oldPagesNumber;
          int currentPageNumber = (splitsPerPage + 1) * pageNo + 1;

          for (int i = columnStart; i < colNo + columnStart; i++)
          {
             JRPrintElement currentElement = oldElements.get(i);

             int currentX = currentElement.getX();
             int nextX = oldElements.get(i + 1).getX();

             while (currentX < nextX)
             {
                // add the current column + all the data cells for this column
                newElements.add(currentElement);
                int index = colNo + i;
                // when we reach at the last 2 page elements which are considered to be the page numbers, exit
                while (oldElementsSize - index > 2)
                {
                   newElements.add(oldElements.get(index));
                   index += colNo;
                }

                if (++i == colNo + columnStart)
                {
                   // we reached at the last column
                   break;
                }
                else
                {
                   currentX = nextX;
                   currentElement = oldElements.get(i);
                   nextX = oldElements.get(i + 1).getX();
                }
             }

             if (i != colNo + columnStart)
             {
                // we reached at the split point, add the last column + data cells to the page
                newElements.add(currentElement);
                int index = colNo + i;
                while (oldElementsSize - index > 2)
                {
                   newElements.add(oldElements.get(index));
                   index += colNo;
                }
             }

             // clone and customize the footer element
             int endRow = startRow + (oldElementsSize - columnStart - colNo - 2) / colNo - 1;
             String pageInfo = totalPageNumber + ", Rows " + startRow + " to " + endRow;

             JRTemplatePrintText oldFooterElement = (JRTemplatePrintText) oldElements.get(oldElementsSize - 2);
             JRTemplatePrintText newFooterElement = cloneTextElementCore(oldFooterElement);

             String customPage = "Page " + currentPageNumber++ + " of " + pageInfo;

             newFooterElement.setText(customPage);

             newElements.add(newFooterElement);
             newPages.add(newPage);

             newPage = new JRBasePrintPage();
             newElements = newPage.getElements();
          }

          return newPages;
       }

       /**
        * Clones only the core fields from a text element. Properties such as hyperlinks are omitted.
        *
        * @param  element
        *         The element to be cloned.
        *
        * @return A partial copy with only the core fields set.
        */
       private JRTemplatePrintText cloneTextElementCore(JRTemplatePrintText element)
       {
          JRTemplatePrintText newElement = new JRTemplatePrintText();

          newElement.setText(element.getFullText());
          newElement.setValue(element.getValue());
          newElement.setLineSpacingFactor(element.getLineSpacingFactor());
          newElement.setLeadingOffset(element.getLeadingOffset());
          newElement.setRunDirection(element.getRunDirectionValue());
          newElement.setTextHeight(element.getTextHeight());
          newElement.setTemplate(element.getTemplate());
          newElement.setX(element.getX());
          newElement.setY(element.getY());
          newElement.setHeight(element.getHeight());
          newElement.setWidth(element.getWidth());
          newElement.setSourceElementId(element.getSourceElementId());
          newElement.setAnchorName(element.getAnchorName());

          return newElement;
       }
    }