# AbstractReportBuilder.java

    /*
    ** Module   : AbstractReportBuilder.java
    ** Abstract : AbstractReportBuilder is a base PDF report builder.
    **
    ** Copyright (c) 2017-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------Description----------------------
    ** 001 SBI 20170601 Created initial version. Fixed fixColumns if the accumulated error of
    **                  the float division influences the sum of all column's widths. Fixed to remove
    **                  the first empty row that can be encountered if a cell's content has been split
    **                  by sub rows.
    ** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 003 GBB 20241010 Moving getPreviewPageNumber here from AccountsReportBuilder to be reused.
    */
    /*
    ** 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.admin.server.reports;

    import java.io.*;
    import java.util.*;
    import java.util.Map.Entry;
    import java.util.function.*;
    import java.util.regex.*;
    import java.util.stream.Stream;

    import com.goldencode.p2j.util.logging.*;
    import org.apache.pdfbox.pdmodel.*;
    import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
    import org.apache.pdfbox.pdmodel.common.*;
    import org.apache.pdfbox.pdmodel.font.*;
    import org.apache.pdfbox.pdmodel.interactive.action.*;
    import org.apache.pdfbox.util.*;

    import com.goldencode.p2j.admin.shared.*;

    import java.util.function.Predicate;

    /**
     * AbstractReportBuilder is an abstract implementation of PdfReportBuilder.
     *
     * @param    <THeaderObject>
     *           Represents a header of the target report
     * @param    <TRowObject>
     *           Represents a row of the target report
     * @param    <TFooterObject>
     *           Represents a footer of the target report
     */
    public abstract class AbstractReportBuilder<THeaderObject extends AbstractReportBuilder.Header,
                                                TRowObject,
                                                TFooterObject extends AbstractReportBuilder.Footer>
    implements PdfReportBuilder
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(AbstractReportBuilder.class);

       /** Maximal number of report pages that this builder can produce in a one document. */
       private int maxPagesNumber;

       /** Defines page margin in this order: left, top, right and bottom. */
       private float[] margin;

       /** Defines a cell margin. */
       private float cellMargin;

       /** Defines a cell pading. */
       private float cellPadding;

       /** Defines top and bottom margin for the header. */
       private float headerMargin;

       /** Defines top and bottom margin for the footer. */
       private float footerMargin;

       /** Report id */
       private final String reportId;

       /**
        * Holds binary documents that comprise the all report and their maximal numbers of pages don't
        * exceed the defined limit given by MAX_PAGE_NUMBER.
        */
       private final LinkedList<byte[]> splittedDoc;

       /** The current instance of the created pdf document */
       PDDocument doc;

       /** The page size of the built report */
       PDRectangle reportSize;

       /** The page orientation of the built report */
       PaperOrientation orient;

       /** The size of the header font */
       float headerFontSize;

       /** The size of the table column font */
       float columnFontSize;

       /** The size of the table cell font */
       float cellFontSize;

       /** The size of the footer font */
       float footerFontSize;

       /** The normal font used for the built report */
       PDFont font;

       /** The bold font used for the built report */
       PDFont boldFont;

       /** The render of the report header */
       private Renderer<THeaderObject> headerRenderer;

       /** The render of the report footer */
       private Renderer<TFooterObject> footerRenderer;

       /** The filter records predicate that tests if a row should be present in new reports */
       private Predicate<TRowObject> filterRecords;

       /** The render of the section header */
       private SectionRenderer<TRowObject> sectionRenderer;

       /** The render of the section footer */
       private SectionRenderer<TRowObject> footerSectionRenderer;

       /**
        * True value indicates that new section must be printed on the next new page, otherwise
        * the new section must be printed on the current page if it is possible.
        */
       private boolean breakPageForNewSection;

       /**
        * The page to preview, in the preview mode PDF is built only for the selected page, to do
        * list of pages to preview.
        */
       private Integer previewPage;

       /**
        * True value indicates that PDF report will request to print itself if while it is opened or
        * rendered by PDF viewer with enabled java script support.
        */
       private boolean selfPrintedDocument;

       /**
        * Creates a report builder per a report request.
        *
        * @param    reportId
        *           Report id.
        */
       public AbstractReportBuilder(String reportId)
       {
          this.reportId = reportId;
          this.splittedDoc = new LinkedList<byte[]>();
       }

       /**
        * Returns the current records filter used internally to select the target sub set of data.
        *
        * @return   The records filter.
        */
       public Predicate<TRowObject> getFilterRecords()
       {
          return filterRecords;
       }

       /**
        * Sets the current records filter that will be used to build reports for the target sub set of
        * data.
        *
        * @param    filterRecords
        *           The records filter.
        */
       public void setFilterRecords(Predicate<TRowObject> filterRecords)
       {
          this.filterRecords = filterRecords;
       }

       /**
        * Returns the current header renderer.
        *
        * @return   The header renderer
        */
       public Renderer<THeaderObject> getHeaderRenderer()
       {
          return headerRenderer;
       }

       /**
        * Sets the current header renderer to draw a page header.
        *
        * @param    headerRenderer
        *           The header renderer
        */
       public void setHeaderRenderer(Renderer<THeaderObject> headerRenderer)
       {
          this.headerRenderer = headerRenderer;
       }

       /**
        * Returns the current header renderer.
        *
        * @return   The footer renderer
        */
       public Renderer<TFooterObject> getFooterRenderer()
       {
          return footerRenderer;
       }

       /**
        * Sets the current footer renderer to draw a page footer.
        *
        * @param    footerRenderer
        *           The footer renderer
        */
       public void setFooterRenderer(Renderer<TFooterObject> footerRenderer)
       {
          this.footerRenderer = footerRenderer;
       }

       /**
        * Returns the current section renderer.
        *
        * @return   The section renderer
        */
       public SectionRenderer<TRowObject> getSectionRenderer()
       {
          return sectionRenderer;
       }

       /**
        * Sets the current section renderer to draw a page section header.
        *
        * @param    sectionRenderer
        *           The section renderer
        */
       public void setSectionRenderer(SectionRenderer<TRowObject> sectionRenderer)
       {
          this.sectionRenderer = sectionRenderer;
       }

       /**
        * Returns the current renderer to draw a page section footer.
        *
        * @return   The section footer renderer
        */
       public SectionRenderer<TRowObject> getFooterSectionRenderer()
       {
          return footerSectionRenderer;
       }

       /**
        * Sets the current renderer to draw a page section footer.
        *
        * @param    footerSectionRenderer
        *           The section footer renderer
        */
       public void setFooterSectionRenderer(SectionRenderer<TRowObject> footerSectionRenderer)
       {
          this.footerSectionRenderer = footerSectionRenderer;
       }

       /**
        * The returned true value indicates that the current page should be interrupted if it is
        * followed by a new section.
        *
        * @return   The break page boolean value
        */
       public boolean isBreakPageForNewSection()
       {
          return breakPageForNewSection;
       }

       /**
        * Sets a boolean value indicating the current page should be interrupted or not if it is
        * followed by a new section.
        *
        * @param    breakPageForNewSection
        *           The break page boolean value
        */
       public void setBreakPageForNewSection(boolean breakPageForNewSection)
       {
          this.breakPageForNewSection = breakPageForNewSection;
       }

       /**
        * Returns the report id.
        *
        * @return   The report id
        */
       public String getReportId()
       {
          return reportId;
       }

       /**
        * Returns the page number to preview.
        *
        * @return The page number to preview
        */
       public Integer getPreviewPage()
       {
          return previewPage;
       }

       /**
        * Sets the page number to preview. If the page number is set and is a positive whole number,
        * then this builder is switched on the preview mode. It means that only this preview page will
        * be produced.
        *
        * @param    previewPage
        *           The page number to preview
        */
       public void setPreviewPage(Integer previewPage)
       {
          this.previewPage = previewPage;
       }

       /**
        * Returned true value indicates that PDF report will request to print itself if it will be
        * rendered.
        *
        * @return   The self printed boolean value
        */
       public boolean isSelfPrintedDocument()
       {
          return selfPrintedDocument;
       }

       /**
        * Sets the boolean value indicating if the produced PDF report will print itself or not
        * while it is opened or rendered by PDF viewer with enabled java script support.
        *
        * @param    selfPrintedDocument
        *           The self printed boolean value
        */
       public void setSelfPrintedDocument(boolean selfPrintedDocument)
       {
          this.selfPrintedDocument = selfPrintedDocument;
       }

       /**
        * Builds a document according to the provided report settings.
        *
        * @param    parameters
        *           The provided report parameters
        * @return   The PDF report document
        *
        * @throws   IOException
        *           This exception can be thrown if there are failed output operations
        */
       @Override
       public final PdfDocument buildReport(PdfReportSettings parameters) throws IOException
       {
          ReportDirector director = new ReportDirector(reportId, parameters, getPreviewPage() != null,
                   isSelfPrintedDocument());

          // setup table columns
          director.columns = getColumns();

          // calculate maximal column width based on the column values
          getDataStream().forEach(director.columnWidthAccumulator());

          // add first page
          director.createReport(getReportHeader(), getReportFooter());

          // fill data
          getDataStream().forEach(director);

          //draw final section if it is required
          director.drawFinalSection();

          if (isSelfPrintedDocument())
          {
             PDActionJavaScript printAction = new PDActionJavaScript("this.print();");
             doc.getDocumentCatalog().setOpenAction(printAction);
          }

          //add the last document in splittedDoc list
          ByteArrayOutputStream bout = new ByteArrayOutputStream();

          doc.save(bout);
          splittedDoc.add(bout.toByteArray());
          bout = null;

          // return pdf document
          return new PdfDocument() {

             @Override
             public DocumentType getDocumentType()
             {
                return DocumentType.PDF;
             }

             @Override
             public byte[] getBinaryContent() throws IOException
             {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                doc.save(out);
                return out.toByteArray();
             }

             @Override
             public PDDocument getDocument()
             {
                return doc;
             }

             @Override
             public void close() throws IOException
             {
                doc.close();
             }

             @Override
             public String getDocumentFileName()
             {
                return reportId + ".pdf";
             }

             @Override
             public int getPagesNumber()
             {
                return director.currentPageIndex + 1;
             }

             @Override
             public List<byte[]> getSplittedContent()
             {
                return splittedDoc;
             }

          };
       }

       /**
        * Returns the low bound for the page content.
        *
        * @return   The low bound for the page content
        */
       public float getContentLowBound()
       {
          return margin[BOTTOM] + footerRenderer.getHeight();
       }

       /**
        * Returns available space for content between its page header and footer.
        *
        * @return   The available space for content
        */
       public float getAvailablePageSpace()
       {
          float heightLimit;

          if (orient == PaperOrientation.LANDSCAPE)
          {
             heightLimit = reportSize.getWidth();
          }
          else
          {
             heightLimit = reportSize.getHeight();
          }

          heightLimit -= margin[TOP];
          heightLimit -= margin[BOTTOM];
          heightLimit -= footerRenderer.getHeight();
          heightLimit -= headerRenderer.getHeight();

          return heightLimit;
       }

       /**
        * Gets the min cell height.
        *
        * @return   The min cell height
        */
       public float getMinCellHeight()
       {
          return 2 * cellPadding + 2 * cellMargin + cellFontSize;
       }

       /**
        * Gets the cell outer space.
        *
        * @return   The cell outer space
        */
       public float getCellOuterSpace()
       {
          return 2 * cellPadding + 2 * cellMargin;
       }

       /**
        * Gets the margin.
        *
        * @return   The margin
        */
       public float[] getMargin()
       {
          return margin;
       }

       /**
        * Gets the cell margin.
        *
        * @return   The cell margin
        */
       public float getCellMargin()
       {
          return cellMargin;
       }

       /**
        * Gets the cell padding.
        *
        * @return   The cell padding
        */
       public float getCellPadding()
       {
          return cellPadding;
       }

       /**
        * Gets the header margin.
        *
        * @return   The header margin
        */
       public float getHeaderMargin()
       {
          return headerMargin;
       }

       /**
        * Gets the footer margin.
        *
        * @return   The footer margin
        */
       public float getFooterMargin()
       {
          return footerMargin;
       }

       /**
        * Gets the doc.
        *
        * @return   The doc
        */
       public PDDocument getDoc()
       {
          return doc;
       }

       /**
        * Gets the report size.
        *
        * @return   The report size
        */
       public PDRectangle getReportSize()
       {
          return reportSize;
       }

       /**
        * Gets the orient.
        *
        * @return   The orient
        */
       public PaperOrientation getOrient()
       {
          return orient;
       }

       /**
        * Gets the header font size.
        *
        * @return   The header font size
        */
       public float getHeaderFontSize()
       {
          return headerFontSize;
       }

       /**
        * Gets the column font size.
        *
        * @return   The column font size
        */
       public float getColumnFontSize()
       {
          return columnFontSize;
       }

       /**
        * Gets the cell font size.
        *
        * @return   The cell font size
        */
       public float getCellFontSize()
       {
          return cellFontSize;
       }

       /**
        * Gets the footer font size.
        *
        * @return   The footer font size
        */
       public float getFooterFontSize()
       {
          return footerFontSize;
       }

       /**
        * Gets the font.
        *
        * @return   The font
        */
       public PDFont getFont()
       {
          return font;
       }

       /**
        * Gets the bold font.
        *
        * @return   The bold font
        */
       public PDFont getBoldFont()
       {
          return boldFont;
       }

       /**
        * Gets the number of pages to preview.
        *
        * @param    reportParameters
        *           The report parameters
        *
        * @return   The number of pages to preview
        */
       int getPreviewPageNumber(Map<String, String> reportParameters)
       {
          String page = reportParameters.getOrDefault(ReportParameters.PREVIEW_PAGE.getParameter(), "0");
          try
          {
             return Integer.parseInt(page);
          }
          catch(NumberFormatException e)
          {
             return -1;
          }
       }

       /**
        * Returns the report data stream. It must be provided by the concrete implementation.
        *
        * @return   The report data stream
        */
       protected abstract Stream<TRowObject> getDataStream();

       /**
        * Returns the list of report columns. It must be provided by the concrete implementation.
        *
        * @return   The list of report columns
        */
       protected abstract List<AbstractReportBuilder.ColumnInfo<TRowObject>> getColumns();

       /**
        * Returns the header data. The concrete implementation must provide the header data.
        *
        * @return   The header data
        */
       protected abstract THeaderObject getReportHeader();

       /**
        * Returns the footer data. The concrete implementation must provide the footer data.
        *
        * @return   The footer data
        */
       protected abstract TFooterObject getReportFooter();

       /**
        * Defines a cell renderer.
        *
        * @param    <TRowObject>
        *           The row data
        */
       public interface CellValueRenderer<TRowObject>
       {
          /**
           * Draws the table cell for the given data row and column.
           *
           * @param    t
           *           The data row
           * @param    column
           *           The column info
           * @param    canvas
           *           The canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          void render(TRowObject t, ColumnInfo<TRowObject> column, IPageCanvas canvas)
                   throws IOException;

          /**
           * Provides the cell's height.
           *
           * @return   The cell's height
           */
          float getHeight();
       }

       /**
        * Defines methods to draw column headers.
        */
       public interface ColumnHeaderRenderer extends Renderer<ColumnInfo<?>>
       {
       }

       /**
        * Defines the header data.
        */
       public interface Header
       {

          /**
           * Gets the header.
           *
           * @return   The header
           */
          String getHeader();

          /**
           * Sets the header.
           *
           * @param    header
           *           The header
           */
          void setHeader(String header);

          /**
           * Gets the page.
           *
           * @return   The page
           */
          String getPage();

          /**
           * Sets the page.
           *
           * @param    page
           *           The page
           */
          void setPage(String page);
       }

       /**
        * Defines the footer data.
        */
       public interface Footer
       {

          /**
           * Gets the footer.
           *
           * @return   The footer
           */
          String getFooter();

          /**
           * Sets the footer.
           *
           * @param    footer
           *           The footer
           */
          void setFooter(String footer);

          /**
           * Gets the page.
           *
           * @return   The page
           */
          String getPage();

          /**
           * Sets the page.
           *
           * @param    page
           *           The page
           */
          void setPage(String page);
       }

       /**
        * The Class SimpleHeader.
        */
       public static class SimpleHeader implements Header
       {

          /** The header. */
          private String header;

          /** The page. */
          private String page;

          /** The additional data. */
          private Map<String, String> additionalData;

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Header#getHeader()
           */
          @Override
          public String getHeader()
          {
             return header;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Header#setHeader(java.lang.String)
           */
          @Override
          public void setHeader(String header)
          {
             this.header = header;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Header#getPage()
           */
          @Override
          public String getPage()
          {
             return page;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Header#setPage(java.lang.String)
           */
          @Override
          public void setPage(String page)
          {
             this.page = page;
          }

          /**
           * Gets the additional data.
           *
           * @return   The additional data
           */
          public Map<String, String> getAdditionalData()
          {
             return additionalData;
          }

          /**
           * Sets the additional data.
           *
           * @param    additionalData
           *           The additional data
           */
          public void setAdditionalData(Map<String, String> additionalData)
          {
             this.additionalData = additionalData;
          }
       }

       /**
        * The Class SimpleFooter.
        */
       public static class SimpleFooter implements Footer
       {

          /** The footer. */
          private String footer;

          /** The page. */
          private String page;

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Footer#getPage()
           */
          @Override
          public String getPage()
          {
             return page;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Footer#setPage(java.lang.String)
           */
          @Override
          public void setPage(String page)
          {
             this.page = page;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Footer#getFooter()
           */
          @Override
          public String getFooter()
          {
             return footer;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Footer#setFooter(java.lang.String)
           */
          @Override
          public void setFooter(String footer)
          {
             this.footer = footer;
          }
       }

       /**
        * Defines methods to draw the report data on the page
        *
        * @param    <T>
        *           The report data type
        */
       public interface Renderer<T>
       {
          /**
           * Renders the given report data.
           *
           * @param    t
           *           The report data
           * @param    canvas
           *           The report page canvas
           *
           * @return   The current report page canvas
           *
           * @throws   IOException
           *           Throws this exception if IO operation is failed
           */
          IPageCanvas render(T t, IPageCanvas canvas) throws IOException;

          /**
           * Returns the height of the area on which this renderer makes its output.
           *
           * @return  The height of the area on which this renderer makes its output
           */
          float getHeight();
       }

       /**
        * The Interface SectionRenderer.
        *
        * @param    <T>
        *           The generic type
        */
       public interface SectionRenderer<T> extends Renderer<T>, Consumer<T>
       {

          /**
           * Checks if the given object represents new section.
           *
           * @param    t
           *           The given section
           *
           * @return   true, if it is new section
           */
          boolean isNewSection(T t);

          /**
           * Render current section.
           *
           * @param    canvas
           *           The canvas
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          IPageCanvas renderCurrentSection(IPageCanvas canvas) throws IOException;
       }

       /**
        * The Default Header Renderer.
        */
       class DefaultHeaderRenderer implements Renderer<THeaderObject>
       {

          /** The report director. */
          private ReportDirector reportDirector;

          /**
           * Instantiates a new default header renderer.
           *
           * @param    reportDirector
           *           The report director
           */
          public DefaultHeaderRenderer(ReportDirector reportDirector)
          {
             this.reportDirector = reportDirector;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Renderer#render(java.lang.Object, com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.IPageCanvas)
           */
          @Override
          public IPageCanvas render(THeaderObject headerText, IPageCanvas canvas) throws IOException
          {
             PageState state = canvas.getState();
             canvas.drawTextCentered(
                      headerText.getHeader(),
                      getBoldFont(),
                      getHeaderFontSize(), 2f,
                      state.widthLimit - margin[LEFT] - margin[RIGHT],
                      margin[LEFT],
                      state.heightLimit - margin[TOP] - headerMargin);
             return canvas;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Renderer#getHeight()
           */
          @Override
          public float getHeight()
          {
             return getHeaderFontSize() + 2 * getHeaderMargin();
          }
       }

       /**
        * The Default Cell Renderer.
        */
       class DefaultCellRenderer implements CellValueRenderer<TRowObject>
       {

          /** The report director. */
          private ReportDirector reportDirector;

          /**
           * Instantiates a new default cell renderer.
           *
           * @param    reportDirector
           *           The report director
           */
          public DefaultCellRenderer(ReportDirector reportDirector)
          {
             this.reportDirector = reportDirector;
          }

          /** The current row height. */
          private float currentRowHeight;

          /** The rows number. */
          private int rowsNumber;

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.CellValueRenderer#render(java.lang.Object, com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.ColumnInfo, com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.IPageCanvas)
           */
          @Override
          public void render(TRowObject t, ColumnInfo<TRowObject> column, IPageCanvas canvas)
          throws IOException
          {
             String value = column.getValueGetter().apply(t);
             PageState s = canvas.getState();
             IPageCanvas c = canvas;
             if (!reportDirector.isColumnTextFittedThisPage(
                      column.width, getCellFontSize(),
                      s.x, s.y, s.widthLimit, s.heightLimit))
             {
                c = reportDirector.getNextPage(t, c, column);
                s = c.getState();
                // build section
                if (sectionRenderer != null && sectionRenderer.isNewSection(t)
                         && c.getColumnRange().getFirst() == column && s.row > 0)
                {
                   c.getState().sectionFilled = true;
                   c = sectionRenderer.render(t, c);
                }

             }

             float x = s.x;
             int col = s.column;

             List<String> wrapped;
             if (value != null && !value.isEmpty())
             {
                wrapped = wrapValue(column, value);
             }
             else
             {
                wrapped = Arrays.asList("");
             }
             Iterator<String> nextValue = wrapped.iterator();

             int actualRows = wrapped.size();

             float height = getHeight();
             PDFont f = getFont();
             float fontSize = getCellFontSize();
             float delta = fontSize;

             while (height >= (delta + getCellOuterSpace()))
             {
                float availableSpace;
                if (s.y - height < getContentLowBound())
                {
                   availableSpace = s.y - getContentLowBound();
                }
                else
                {
                   availableSpace = height;
                }

                // calculate the number of available rows
                float rows = getAvailableRows(availableSpace - getCellMargin() - getCellPadding());

                rows = Math.min(rows,  actualRows);

                height -= availableSpace;

                // save nextRowOffset
                s.nextRowOffset = availableSpace;

                // offset for the first row in this cell
                float dHeight = (availableSpace - (rows * fontSize)) / 2f;

                // offset for the current row in this cell
                float h = 0;

                for (int i = 0; i < rows; i++)
                {
                   if (nextValue.hasNext())
                   {
                      String part = nextValue.next();
                      float partWidth = f.getStringWidth(part) * fontSize / 1000f;
                      float dWidth = column.width - partWidth;
                      switch (column.valueAlignment)
                      {
                         case RIGHT:
                            dWidth = (column.width - partWidth) / 2f;
                            break;
                         case LEFT:
                            dWidth = ( - column.width + partWidth) / 2f;
                            break;
                         case CENTERED:
                         default:
                            dWidth = 0;
                      }
                      c.drawTextCentered(part,
                               f,
                               fontSize, 1f, column.width,
                               s.x + dWidth,
                               s.y - dHeight - h);

                      h += cellFontSize;

                   } // has next value
                }

                c.strokeHorizontal(s.x, s.y, column.width, 0.5f);
                c.strokeHorizontal(s.x, s.y - availableSpace, column.width, 0.5f);
                c.strokeVertical(s.x, s.y, availableSpace, 0.5f);
                s.x += column.width;
                c.strokeVertical(s.x, s.y, availableSpace, 0.5f);
                s.column++;

                actualRows -= rows;

                if (height > 0)
                {
                   c = reportDirector.getNextPage(t, c, column);
                   // build section
                   if (sectionRenderer != null && sectionRenderer.isNewSection(t)
                            && c.getColumnRange().getFirst() == column && c.getState().row > 0)
                   {
                      c.getState().sectionFilled = true;
                      c = sectionRenderer.render(t, c);
                   }
                   s = c.getState();
                   s.x = x;
                   s.column = col;
                }
             } // while height > 0
          }

          /**
           * Sets the height.
           *
           * @param    currentRowHeight
           *           The height
           */
          public void setHeight(float currentRowHeight)
          {
             this.currentRowHeight = currentRowHeight;
          }

          /**
           * Returns an outer row height including margin and padding.
           *
           * @return   The row height
           */
          public float getHeight()
          {
             return currentRowHeight;
          }

          /**
           * Gets the rows number.
           *
           * @return   The rows number
           */
          public int getRowsNumber()
          {
             return rowsNumber;
          }

          /**
           * Sets the rows number.
           *
           * @param    rowsNumber
           *           The rows number
           */
          public void setRowsNumber(int rowsNumber)
          {
             this.rowsNumber = rowsNumber;
          }

          /**
           * Wraps the given text into multiple lines of text to fit the column width.
           *
           * @param    column
           *           The column
           * @param    value
           *           The given text
           *
           * @return   The list of lines
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          List<String> wrapValue(ColumnInfo<TRowObject> column, String value) throws IOException
          {
             LinkedList<String> wrapped = new LinkedList<String>();
             float valueWidth = getFont().getStringWidth(value) * getCellFontSize() / 1000f;
             float availableWidth = column.width - 2 * getCellPadding() - 2 * getCellMargin();
             if (valueWidth > availableWidth)
             {
                float avgCharWidth = valueWidth / value.length();
                int maxChars = Math.round(availableWidth / avgCharWidth);
                Pattern splitExp = Pattern.compile("[,\\s]");
                Matcher m = splitExp.matcher(value);
                int start = 0, end = 0;
                while(end < value.length())
                {
                   String part = null;
                   boolean found = false;
                   while(m.find() && (m.end() < (maxChars + start)))
                   {
                      part = value.substring(start, m.end());
                      end = m.end();
                      found = true;
                   }
                   if (!found)
                   {
                      end = Math.min(start + maxChars, value.length());
                      part = value.substring(start, end);
                   }
                   start = end;
                   if (part != null)
                   {
                      wrapped.add(part);
                   }
                }
             }
             else
             {
                wrapped.add(value);
             }

             return wrapped;
          }

          /**
           * Calculate an outer cell height including its margin and padding
           *
           * @param    t
           *           The row object that holds its values
           * @param    column
           *           The current column
           *
           * @return   The cell's height
           *
           * @throws   IOException
           *           The IO exception iff the IO operation is failed.
           */
          public float calculateCellHeight(TRowObject t, ColumnInfo<TRowObject> column)
          throws IOException
          {
             float rowHeight = getCellOuterSpace();
             String value = column.getValueGetter().apply(t);
             int rows = 1;
             if (value != null && !value.isEmpty())
             {
                List<String> wrapped = wrapValue(column, value);
                rows = wrapped.size();
             }

             rowHeight += (rows * getCellFontSize());

             return rowHeight;
          }

          /**
           * Calculate the number of rows in this cell.
           *
           * @param    t
           *           The row object that holds its values
           * @param    column
           *           The current column
           *
           * @return   The number of rows
           *
           * @throws   IOException
           *           The IO exception iff the IO operation is failed.
           */
          public int calculateRowsNumber(TRowObject t, ColumnInfo<TRowObject> column) throws IOException
          {
             String value = column.getValueGetter().apply(t);
             int rows = 1;
             if (value != null && !value.isEmpty())
             {
                List<String> wrapped = wrapValue(column, value);
                rows = wrapped.size();
             }

             return rows;
          }

          /**
           * Gets the available rows.
           *
           * @param    availableSpace
           *           The available space
           *
           * @return   The available rows
           */
          private int getAvailableRows(float availableSpace)
          {
             int rows = -1;
             float h = 0;
             while (h <= availableSpace)
             {
                rows++;
                h += getCellFontSize();
             }

             return rows;
          }
       }

       /**
        * The Default Column Header Renderer.
        */
       class DefaultColumnHeaderRenderer implements ColumnHeaderRenderer
       {

          /** The report director. */
          private ReportDirector reportDirector;

          /** The height. */
          private float height;

          /**
           * Instantiates a new default column header renderer.
           *
           * @param    reportDirector
           *           The report director
           */
          public DefaultColumnHeaderRenderer(ReportDirector reportDirector)
          {
             this.reportDirector = reportDirector;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Renderer#render(java.lang.Object, com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.IPageCanvas)
           */
          @Override
          public IPageCanvas render(ColumnInfo<?> columnInfo, IPageCanvas canvas) throws IOException
          {
             IPageCanvas c = canvas;
             PageState  s = c.getState();

             if (!reportDirector.isColumnTextFittedThisPage(
                      columnInfo.width, getColumnFontSize(),
                      s.x, s.y,
                      s.widthLimit, s.heightLimit))
             {

                c = reportDirector.buildHeaderAndFooterTemplate(reportDirector.addNewPage());
                s = c.getState();
                // build section
                if (sectionRenderer != null && !s.sectionFilled)
                {
                   s.sectionFilled = true;
                   c = sectionRenderer.renderCurrentSection(c);
                }

                c.getColumnRange().add(columnInfo);
             }
             else
             {
                c.getColumnRange().add(columnInfo);
             }

             String[] labels = columnInfo.getLabels();
             float h = 0;
             int rows = labels.length;
             float dHeight = (getHeight() - (rows * getColumnFontSize())) / 2f;
             for(String label : labels)
             {
                float labelWidth = getBoldFont().getStringWidth(label) * getColumnFontSize() / 1000f;
                float dWidth = columnInfo.width - labelWidth;
                switch (columnInfo.valueAlignment)
                {
                   case RIGHT:
                      dWidth = (columnInfo.width - labelWidth) / 2f;
                      break;
                   case LEFT:
                      dWidth = ( - columnInfo.width + labelWidth) / 2f;
                      break;
                   case CENTERED:
                   default:
                      dWidth = 0;
                }

                c.drawTextCentered(label,
                         getBoldFont(), getColumnFontSize(), 2f,
                         columnInfo.width,
                         s.x + dWidth,
                         s.y - dHeight - h);
                h += getColumnFontSize();
             }

             c.strokeHorizontal(s.x, s.y, columnInfo.width, 0.5f);
             c.strokeHorizontal(s.x, s.y - getHeight(), columnInfo.width, 0.5f);
             c.strokeVertical(s.x, s.y, getHeight(), 0.5f);
             s.x += columnInfo.width;
             c.strokeVertical(s.x, s.y, getHeight(), 0.5f);
             s.column++;
             return c;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Renderer#getHeight()
           */
          @Override
          public float getHeight()
          {
             return height;
          }

          /**
           * Sets the height.
           *
           * @param    height
           *           The height
           */
          public void setHeight(float height)
          {
             this.height = height;
          }

          /**
           * Calculate the header height.
           *
           * @param    columnInfo
           *           The given column info
           *
           * @return   The header height
           */
          public float calculateHeight(ColumnInfo<?> columnInfo)
          {
             float h = getCellOuterSpace();

             int rows = 1;

             if (columnInfo.labels != null && columnInfo.labels.length > 0)
             {
                rows = columnInfo.labels.length;
             }

             h += (getColumnFontSize() * rows);

             return h;
          }
       }

       /**
        * The Default Footer Renderer.
        */
       class DefaultFooterRenderer implements Renderer<TFooterObject>
       {

          /** The report director. */
          private ReportDirector reportDirector;

          /**
           * Instantiates a new default footer renderer.
           *
           * @param    reportDirector
           *           The report director
           */
          public DefaultFooterRenderer(ReportDirector reportDirector)
          {
             this.reportDirector = reportDirector;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Renderer#render(java.lang.Object, com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.IPageCanvas)
           */
          @Override
          public IPageCanvas render(TFooterObject footerText, IPageCanvas canvas) throws IOException
          {
             PageState state = canvas.getState();
             canvas.drawTextCentered(
                      "Page: " + footerText.getPage(),
                      getFont(),
                      getFooterFontSize(), 1f,
                      state.widthLimit - margin[LEFT] - margin[RIGHT],
                      margin[LEFT],
                      margin[BOTTOM] + getFooterFontSize());
             return canvas;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.Renderer#getHeight()
           */
          @Override
          public float getHeight()
          {
             return getFooterFontSize() + 2 * getFooterMargin();
          }
       }

       /**
        * Defines the page canvas.
        */
       public interface IPageCanvas {

          /**
           * Gets the page number.
           *
           * @return   The page number
           */
          public int getPageNumber();

          /**
           * Gets the current page state.
           *
           * @return   The current page state
           */
          public PageState getState();

          /**
           * Gets the current column range that have been drawn on this page canvas already
           *
           * @return   The current column range
           */
          public LinkedList<ColumnInfo<?>> getColumnRange();

          /**
           * Draws the shape of a rectangle provided by its (x, y) upper left corner and its size.
           *
           * @param    x
           *           The rectangle's left side coordinate
           * @param    y
           *           The rectangle's top side coordinate
           * @param    width
           *           The rectangle's width
           * @param    height
           *           The rectangle's height
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public IPageCanvas strokeRectangle(float x, float y, float width, float height)
                   throws IOException;

          /**
           * Draws the vertical starting at (x, y) upper left corner provided with its height
           * and its line width.
           *
           * @param    x
           *           The vertical's left side coordinate
           * @param    y
           *           The vertical's top side coordinate
           * @param    height
           *           The height of the vertical
           * @param    lineWidth
           *           The line width
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public IPageCanvas strokeVertical(float x, float y, float height, float lineWidth)
                   throws IOException;

          /**
           * Draws the horizontal starting at (x, y) upper left corner provided with its width
           * and its line width.
           *
           * @param    x
           *           The horizontal's left side coordinate
           * @param    y
           *           The horizontal's top side coordinate
           * @param    width
           *           The width of the horizontal
           * @param    lineWidth
           *           The line width
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public IPageCanvas strokeHorizontal(float x, float y, float width, float lineWidth)
                   throws IOException;

          /**
           * Draw the provided text at (x, y) upper left corner
           *
           * @param    text
           *           The text
           * @param    font
           *           The font
           * @param    fontSize
           *           The font size
           * @param    lineWidth
           *           The line width
           * @param    x
           *           The left side text position
           * @param    y
           *           The top side text position
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public IPageCanvas drawTextAtLeftTopCorner(String text, PDFont font, float fontSize,
                   float lineWidth, float x, float y) throws IOException;

          /**
           * Draw the provided text centered at y upper level
           *
           * @param    text
           *           The text
           * @param    font
           *           The font
           * @param    fontSize
           *           The font size
           * @param    lineWidth
           *           The line width
           * @param    width
           *           The width of the rectangle for text drawing
           * @param    x
           *           The left side of the rectangle for text drawing
           * @param    y
           *           The top side of the rectangle for text drawing
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public IPageCanvas drawTextCentered(String text, PDFont font, float fontSize, float lineWidth,
                   float width, float x, float y) throws IOException;

          /**
           * Draw the provided text aligned right at y upper level
           *
           * @param    text
           *           The text
           * @param    font
           *           The font
           * @param    fontSize
           *           The font size
           * @param    lineWidth
           *           The line width
           * @param    x
           *           The left side text position
           * @param    y
           *           The top side text position
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public IPageCanvas drawTextAtRightTopCorner(String text, PDFont font, float fontSize,
                   float lineWidth, float x, float y) throws IOException;
       }

       /**
        * Implements IPageCanvas based on the PDFBox API
        */
       class PageCanvas implements IPageCanvas
       {

          /** The doc. */
          private PDDocument doc;

          /** The page. */
          private PDPage page;

          /** The page number. */
          private int pageNumber;

          /** The state. */
          private PageState state;

          /** The column range. */
          private LinkedList<ColumnInfo<?>> columnRange;

          /**
           * Instantiates a new page canvas.
           *
           * @param    doc
           *           The current doc
           * @param    page
           *           The page
           * @param    pageNumber
           *           The page number
           * @param    state
           *           The state
           */
          PageCanvas(PDDocument doc, PDPage page, int pageNumber, PageState state)
          {
             this.doc = doc;
             this.page = page;
             this.pageNumber = pageNumber;
             this.state = state;
             this.columnRange = new LinkedList<>();
          }

          /**
           * Gets the doc.
           *
           * @return   The doc
           */
          public PDDocument getDoc()
          {
             return doc;
          }

          /**
           * Gets the page.
           *
           * @return   The page
           */
          public PDPage getPage()
          {
             return page;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.IPageCanvas#getPageNumber()
           */
          public int getPageNumber()
          {
             return pageNumber;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.IPageCanvas#getState()
           */
          public PageState getState()
          {
             return state;
          }

          /**
           * {@inheritDoc}
           * @see com.goldencode.p2j.admin.server.reports.AbstractReportBuilder.IPageCanvas#getColumnRange()
           */
          public LinkedList<ColumnInfo<?>> getColumnRange()
          {
             return columnRange;
          }

          /**
           * Creates the content stream.
           *
           * @return   The PD page content stream
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          private PDPageContentStream createContentStream() throws IOException
          {
             PDPageContentStream contentStream = new PDPageContentStream(doc, page,
                      AppendMode.APPEND, false);

             if (state.pageTransformation != null && !state.transformApplied)
             {
                contentStream.transform(state.pageTransformation);
                state.transformApplied = true;
             }

             return contentStream;
          }

          /**
           * Draws the shape of a rectangle provided by its (x, y) upper left corner and its size.
           *
           * @param    x
           *           The rectangle's left side coordinate
           * @param    y
           *           The rectangle's top side coordinate
           * @param    width
           *           The rectangle's width
           * @param    height
           *           The rectangle's height
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public PageCanvas strokeRectangle(float x, float y, float width, float height)
          throws IOException
          {
             if (page == null)
             {
                return this;
             }

             try(PDPageContentStream contentStream = createContentStream())
             {
                contentStream.moveTo(x, y);
                contentStream.lineTo(x, y - height);
                contentStream.stroke();

                contentStream.moveTo(x + width, y);
                contentStream.lineTo(x + width, y - height);
                contentStream.stroke();

                contentStream.moveTo(x , y);
                contentStream.lineTo(x + width, y);
                contentStream.stroke();

                contentStream.moveTo(x , y - height);
                contentStream.lineTo(x + width, y - height);
                contentStream.stroke();
             }
             return this;
          }

          /**
           * Draws the vertical starting at (x, y) upper left corner provided with its height
           * and its line width.
           *
           * @param    x
           *           The vertical's left side coordinate
           * @param    y
           *           The vertical's top side coordinate
           * @param    height
           *           The height of the vertical
           * @param    lineWidth
           *           The line width
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public PageCanvas strokeVertical(float x, float y, float height, float lineWidth)
          throws IOException
          {
             if (page == null)
             {
                return this;
             }
             try(PDPageContentStream contentStream = createContentStream())
             {
                contentStream.saveGraphicsState();
                contentStream.setLineWidth(lineWidth);
                contentStream.moveTo(x, y);
                contentStream.lineTo(x, y - height);
                contentStream.stroke();
                contentStream.restoreGraphicsState();
             }

             return this;
          }

          /**
           * Draws the horizontal starting at (x, y) upper left corner provided with its width
           * and its line width.
           *
           * @param    x
           *           The horizontal's left side coordinate
           * @param    y
           *           The horizontal's top side coordinate
           * @param    width
           *           The width of the horizontal
           * @param    lineWidth
           *           The line width
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public PageCanvas strokeHorizontal(float x, float y, float width, float lineWidth)
          throws IOException
          {
             if (page == null)
             {
                return this;
             }
             try(PDPageContentStream contentStream = createContentStream())
             {
                contentStream.saveGraphicsState();
                contentStream.setLineWidth(lineWidth);
                contentStream.moveTo(x , y);
                contentStream.lineTo(x + width, y);
                contentStream.stroke();
                contentStream.restoreGraphicsState();
             }

             return this;
          }

          /**
           * Draw the provided text at (x, y) upper left corner
           *
           * @param    text
           *           The text
           * @param    font
           *           The font
           * @param    fontSize
           *           The font size
           * @param    lineWidth
           *           The line width
           * @param    x
           *           The left side text position
           * @param    y
           *           The top side text position
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public PageCanvas drawTextAtLeftTopCorner(String text,
                                                    PDFont font,
                                                    float fontSize,
                                                    float lineWidth,
                                                    float x,
                                                    float y)
          throws IOException
          {
             if (page == null)
             {
                return this;
             }
             try(PDPageContentStream contentStream = createContentStream())
             {
                contentStream.saveGraphicsState();
                contentStream.setLineWidth(lineWidth);
                contentStream.beginText();
                contentStream.setFont(font, fontSize);
                contentStream.newLineAtOffset(x, y - fontSize);
                contentStream.showText(text);
                contentStream.endText();
                contentStream.restoreGraphicsState();
             }

             return this;
          }

          /**
           * Draw the provided text centered at y upper level
           *
           * @param    text
           *           The text
           * @param    font
           *           The font
           * @param    fontSize
           *           The font size
           * @param    lineWidth
           *           The line width
           * @param    width
           *           The width of the rectangle for text drawing
           * @param    x
           *           The left side of the rectangle for text drawing
           * @param    y
           *           The top side of the rectangle for text drawing
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public PageCanvas drawTextCentered(String text,
                                             PDFont font,
                                             float fontSize,
                                             float lineWidth,
                                             float width,
                                             float x,
                                             float y)
          throws IOException
          {
             if (page == null)
             {
                return this;
             }
             try(PDPageContentStream contentStream = createContentStream())
             {
                contentStream.saveGraphicsState();
                contentStream.setLineWidth(lineWidth);
                float stringWidth = font.getStringWidth(text) * fontSize / 1000f;
                contentStream.beginText();
                contentStream.setFont(font, fontSize);
                contentStream.setLeading(fontSize);
                contentStream.newLineAtOffset(x + (width - stringWidth) / 2, y - fontSize);
                contentStream.showText(text);
                contentStream.endText();
                contentStream.restoreGraphicsState();
             }

             return this;
          }

          /**
           * Draw the provided text aligned right at y upper level
           *
           * @param    text
           *           The text
           * @param    font
           *           The font
           * @param    fontSize
           *           The font size
           * @param    lineWidth
           *           The line width
           * @param    x
           *           The left side text position
           * @param    y
           *           The top side text position
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          public PageCanvas drawTextAtRightTopCorner(String text,
                                                     PDFont font,
                                                     float fontSize,
                                                     float lineWidth,
                                                     float x,
                                                     float y)
          throws IOException
          {
             if (page == null)
             {
                return this;
             }
             try(PDPageContentStream contentStream = createContentStream())
             {
                contentStream.saveGraphicsState();
                contentStream.setLineWidth(lineWidth);
                float stringWidth = font.getStringWidth(text) * fontSize / 1000f;
                contentStream.beginText();
                contentStream.setFont(font, fontSize);
                contentStream.setLeading(fontSize);
                contentStream.newLineAtOffset(x - stringWidth, y - fontSize);
                contentStream.showText(text);
                contentStream.endText();
                contentStream.restoreGraphicsState();
             }

             return this;
          }
       }

       /**
        * The column info provides with an access getter to its cell's value and its cell's layout and
        * size.
        *
        * @param    <TRowObject>
        *           The row data type
        */
       public static class ColumnInfo<TRowObject>
       {

          /**
           * The Enum Alignment.
           */
          enum Alignment
          {

             /** The left. */
             LEFT,

             /** The right. */
             RIGHT,

             /** The centered. */
             CENTERED
          }

          /** The labels. */
          private String[] labels;

          /** The header alignment. */
          private Alignment headerAlignment = Alignment.CENTERED;

          /** The value getter. */
          private Function<TRowObject, String> valueGetter;

          /** The max width. */
          private float maxWidth = 60;

          /** The width. */
          private float width;

          /** The header width. */
          private float headerWidth;

          /** The value alignment. */
          private Alignment valueAlignment = Alignment.CENTERED;

          /**
           * Instantiates a new column info.
           *
           * @param    headerLabel
           *           The header label
           * @param    valueGetter
           *           The value getter
           */
          public ColumnInfo(String headerLabel, Function<TRowObject, String> valueGetter)
          {
             this.labels = new String[] {headerLabel};
             this.valueGetter = valueGetter;
          }

          /**
           * Instantiates a new column info.
           *
           * @param    labels
           *           The labels
           * @param    valueGetter
           *           The value getter
           */
          public ColumnInfo(String[] labels, Function<TRowObject, String> valueGetter)
          {
             this.labels = labels;
             this.valueGetter = valueGetter;
          }

          /**
           * Sets the column maximal width.
           *
           * @param    maxWidth
           *           The max width
           *
           * @return   The column info
           */
          public ColumnInfo maxWidth(float maxWidth)
          {
             setMaxWidth(maxWidth);

             return this;
          }

          /**
           * Sets the value horizontal alignment.
           *
           * @param    valueAlignment
           *           The value alignment
           *
           * @return   The column info
           */
          public ColumnInfo valueAlignment(Alignment valueAlignment)
          {
             setValueAlignment(valueAlignment);
             return this;
          }

          /**
           * Gets the max width.
           *
           * @return   The max width
           */
          public float getMaxWidth()
          {
             return maxWidth;
          }

          /**
           * Sets the max width.
           *
           * @param    maxWidth
           *           The max width
           */
          public void setMaxWidth(float maxWidth)
          {
             this.maxWidth = maxWidth;
          }

          /**
           * Gets the value alignment.
           *
           * @return   The value alignment
           */
          public Alignment getValueAlignment()
          {
             return valueAlignment;
          }

          /**
           * Sets the value horizontal alignment.
           *
           * @param    valueAlignment
           *           The value alignment
           */
          public void setValueAlignment(Alignment valueAlignment)
          {
             this.valueAlignment = valueAlignment;
          }

          /**
           * Gets the labels.
           *
           * @return   The labels
           */
          public String[] getLabels()
          {
             return this.labels;
          }

          /**
           * Sets the labels.
           *
           * @param    labels
           *           The labels
           */
          public void setLabels(String[] labels)
          {
             this.labels = labels;
          }

          /**
           * Gets the value getter.
           *
           * @return   The value getter
           */
          public Function<TRowObject, String> getValueGetter()
          {
             return valueGetter;
          }

          /**
           * Sets the value getter.
           *
           * @param    valueGetter
           *           The value getter
           */
          public void setValueGetter(Function<TRowObject, String> valueGetter)
          {
             this.valueGetter = valueGetter;
          }

          /**
           * {@inheritDoc}
           * @see java.lang.Object#toString()
           */
          public String toString()
          {
             StringBuilder builder = new StringBuilder();
             builder.append("label=").append(Arrays.toString(labels));
             builder.append(", maxWidth=").append(maxWidth);
             builder.append(", width=").append(width);
             builder.append(", headerWidth=").append(headerWidth);

             return builder.toString();
          }
       }

       /**
        * The Extended Column Info.
        *
        * @param    <TRowObject>
        *           The row object type
        */
       public static class ExtendedColumnInfo<TRowObject extends ExtendedRowInfo>
       extends ColumnInfo<TRowObject>
       {

          /** The extendees. */
          private final ColumnInfo<TRowObject>[] extendees;

          /** The offset. */
          private final int offset;

          /**
           * Instantiates a new extended column info.
           *
           * @param    headerLabel
           *           The header label
           * @param    offset
           *           The offset
           * @param    extendees
           *           The extendees
           */
          public ExtendedColumnInfo(String headerLabel,
                                    int offset,
                                    ColumnInfo<TRowObject>... extendees)
          {
             super(headerLabel, null);
             this.offset = offset;
             this.extendees = extendees;
             setValueGetter(this::getValue);
          }

          /**
           * Gets the value.
           *
           * @param    row
           *           The row
           *
           * @return   The value
           */
          private String getValue(TRowObject row)
          {
             int i = row.getExtendedRowIndex();
             if (i >= 0 && i < extendees.length)
             {
                return extendees[i].getValueGetter().apply(row);
             }

             return "";
          }
       }

       /**
        * The Static Column Info.
        *
        * @param    <TRowObject>
        *           The generic type
        */
       public static class StaticColumnInfo<TRowObject extends ExtendedRowInfo>
       extends ColumnInfo<TRowObject>
       {

          /** The values. */
          private String[] values;

          /** The offset. */
          private int offset;

          /**
           * Instantiates a new static column info.
           *
           * @param    headerLabel
           *           The header label
           * @param    offset
           *           The offset
           * @param    values
           *           The values
           */
          public StaticColumnInfo(String headerLabel, int offset, String... values)
          {
             super(headerLabel, null);
             this.values = values;
             this.offset = offset;
             setValueGetter(this::getValue);
          }

          /**
           * Gets the value.
           *
           * @param    row
           *           The row
           *
           * @return   The value
           */
          private String getValue(TRowObject row)
          {
             int i = row.getExtendedRowIndex();
             if (i >= 0 && i < values.length)
             {
                return values[i];
             }

             return "";
          }
       }

       /**
        * Defines the current page state
        */
       public static class PageState
       {

          /** The row. */
          public int row = 0;

          /** The column. */
          public int column = 0;

          /** The x. */
          public float x;

          /** The y. */
          public float y;

          /** The width limit. */
          public float widthLimit;

          /** The height limit. */
          public float heightLimit;

          /** The page transformation. */
          public Matrix pageTransformation;

          /** The transform applied. */
          public boolean transformApplied;

          /** The page. */
          public int page;

          /** The filled. */
          public boolean filled;

          /** The next row offset. */
          public float nextRowOffset;

          /** The section filled. */
          public boolean sectionFilled;

          /**
           * {@inheritDoc}
           * @see java.lang.Object#toString()
           */
          public String toString()
          {
             StringBuilder builder = new StringBuilder();
             builder.append("page=").append(page);
             builder.append(", filled=").append(filled);
             builder.append(", sectionFilled=").append(sectionFilled);
             builder.append(", widthLimit=").append(widthLimit);
             builder.append(", heightLimit=").append(heightLimit);
             builder.append(", x=").append(x);
             builder.append(", y=").append(y);
             builder.append(", row=").append(row);
             builder.append(", column=").append(column);
             builder.append(", transformApplied=").append(transformApplied);

             return builder.toString();
          }
       }

       /**
        * The report director.
        */
       private class ReportDirector implements Consumer<TRowObject>
       {

          /** The columns. */
          List<ColumnInfo<TRowObject>> columns;

          /** The linked pages. */
          LinkedHashMap<Integer, IPageCanvas> linkedPages = new LinkedHashMap<Integer, IPageCanvas>();

          /** The column header renderer. */
          DefaultColumnHeaderRenderer columnHeaderRenderer;

          /** The cell renderer. */
          DefaultCellRenderer cellRenderer;

          /** The header. */
          THeaderObject header;

          /** The footer. */
          TFooterObject footer;

          /** The required to refresh doc. */
          boolean requiredToRefreshDoc;

          /** The preview or self printed mode. */
          boolean previewOrSelfPrintedMode;

          /** The self printed. */
          boolean selfPrinted;

          /** zero based index */
          int currentPageIndex = -1;

          /**
           * Instantiates a new report director.
           *
           * @param    documentTitle
           *           The document title
           * @param    parameters
           *           The parameters
           * @param    previewMode
           *           The preview mode
           * @param    selfPrinted
           *           The self printed
           */
          public ReportDirector(String documentTitle,
                                PdfReportSettings parameters,
                                boolean previewMode,
                                boolean selfPrinted)
          {
             doc = new PDDocument();
             PDDocumentInformation pdd = doc.getDocumentInformation();
             pdd.setTitle(documentTitle);
             headerFontSize = parameters.getHeaderFontSize();
             columnFontSize = parameters.getColumnFontSize();
             cellFontSize   = parameters.getCellFontSize();
             footerFontSize = parameters.getFooterFontSize();

             font = parameters.getFont();

             boldFont = parameters.getBoldFont();

             reportSize = parameters.getReportSize();

             orient = parameters.getPaperOrientation();

             margin = parameters.getPageMargin();
             cellMargin = parameters.getCellMargin();
             cellPadding = parameters.getCellPadding();
             footerMargin = parameters.getFooterMargin();
             headerMargin = parameters.getHeaderMargin();

             maxPagesNumber = parameters.getMaxPagesNumber();

             columnHeaderRenderer = new DefaultColumnHeaderRenderer(this);

             cellRenderer = new DefaultCellRenderer(this);

             if (headerRenderer == null)
             {
                headerRenderer = new DefaultHeaderRenderer(this);
             }

             if (footerRenderer == null)
             {
                footerRenderer = new DefaultFooterRenderer(this);
             }

             this.previewOrSelfPrintedMode = previewMode;
             this.selfPrinted = selfPrinted;
          }

          /**
           * Fix columns.
           *
           * @param    canvas
           *           The canvas
           */
          public void fixColumns(IPageCanvas canvas)
          {
             PageState s = canvas.getState();
             float w = 0;
             int pageColumnCount = 0;
             float availableWidth = s.widthLimit - margin[LEFT] - margin[RIGHT];
             TreeMap<Integer, Float> columnBreaks = new TreeMap<Integer, Float>();
             int columnCounts = columns.size();
             for(int i = 0; i < columnCounts; i++)
             {
                ColumnInfo<TRowObject> col1 = columns.get(i);
                w += col1.width;
                pageColumnCount++;
                if (w <= availableWidth)
                {
                   if (i < columnCounts - 1)
                   {
                      ColumnInfo<TRowObject> col2 = columns.get(i + 1);
                      if (w + col2.width > availableWidth)
                      {
                         float delta = (int) Math.floor((availableWidth - w) / pageColumnCount);
                         columnBreaks.put(Integer.valueOf(i), Float.valueOf(delta));
                         w = 0; // start new page
                         pageColumnCount = 0;
                      }
                   }
                   else
                   {
                      float delta = (int) Math.floor((availableWidth - w) / pageColumnCount);
                      columnBreaks.put(Integer.valueOf(i), Float.valueOf(delta));
                   }
                }
             }
             int start = 0, end;
             for (Integer index : columnBreaks.keySet())
             {
                Float delta = columnBreaks.get(index);
                end = index.intValue();
                for (int i = start; i <= end; i++)
                {
                   ColumnInfo<TRowObject> col = columns.get(i);
                   col.width += delta.floatValue();
                }
                start = end + 1;
             }
          }

          /**
           * Creates the report.
           *
           * @param    header
           *           The header
           * @param    footer
           *           The footer
           */
          public void createReport(THeaderObject header, TFooterObject footer)
          {
             // save the provided footer and header
             this.header = header;
             this.footer = footer;
             try
             {
                IPageCanvas canvas = addNewPage();

                //fix columns to fit all available page width
                fixColumns(canvas);

                // calculate table header height
                float headerHeight = 0;

                for(ColumnInfo<TRowObject> column : columns)
                {
                   headerHeight = Math.max(headerHeight, columnHeaderRenderer.calculateHeight(column));
                }
                columnHeaderRenderer.setHeight(headerHeight);

                buildHeaderAndFooterTemplate(canvas);
             }
             catch (IOException e)
             {
                LOG.severe("Create report exception", e);
             }
          }

          /**
           * Builds the header and footer template.
           *
           * @param    canvas
           *           The canvas
           *
           * @return   The current page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          private IPageCanvas buildHeaderAndFooterTemplate(IPageCanvas canvas)
          throws IOException
          {
             /** header and footer don't change report state */
             drawReportHeader(this.header, canvas);
             drawReportFooter(this.footer, canvas);

             return canvas;
          }

          /**
           * Column width accumulator.
           *
           * @return   The consumer
           */
          public Consumer<TRowObject> columnWidthAccumulator()
          {
             return new Consumer<TRowObject>()
             {
                @Override
                public void accept(TRowObject t)
                {
                   if (filterRecords != null && !filterRecords.test(t))
                   {
                      return;
                   }

                   for(ColumnInfo<TRowObject> column : columns)
                   {
                      String value  = column.getValueGetter().apply(t);
                      float width = column.width;
                      if (column.headerWidth <= 0)
                      {
                         String[] labels = column.getLabels();
                         for (String label : labels)
                         {
                            try
                            {
                               // bold font is used for column headers
                               width = getBoldFont().getStringWidth(label) * columnFontSize / 1000f  + 2*cellMargin
                                        + 2*cellPadding;
                            }
                            catch (IOException e)
                            {
                               LOG.warning("Get bold font exception", e);
                            }
                            column.headerWidth = Math.max(column.headerWidth, width);
                         }
                         // adjust maximal column width
                         column.maxWidth = Math.max(column.headerWidth, column.maxWidth);
                         column.width = Math.max(column.headerWidth, column.width);
                      }

                      if (value != null)
                      {
                         try
                         {
                            width = Math.max(font.getStringWidth(value) * cellFontSize / 1000f
                                     + 2*cellMargin + 2*cellPadding, width);
                         }
                         catch (IOException e)
                         {
                            LOG.warning("Get font string width exception", e);
                         }
                      }

                      column.width = Math.min(Math.max(width, column.width), column.maxWidth);
                   }
                }
             };
          }

          /**
           * {@inheritDoc}
           * @see java.util.function.Consumer#accept(java.lang.Object)
           */
          @Override
          public void accept(TRowObject t)
          {
             if (filterRecords != null && !filterRecords.test(t))
             {
                return;
             }

             SectionRenderer<TRowObject> previousSectionRenderer = getFooterSectionRenderer();

             if (previousSectionRenderer != null)
             {
                previousSectionRenderer.accept(t);

                if (previousSectionRenderer.isNewSection(t))
                {
                   try{
                      IPageCanvas canvas = getFirstAvailablePageForSection(previousSectionRenderer);

                      if (canvas.getColumnRange().isEmpty())
                      {
                         // build the previous section
                         if (sectionRenderer != null && !canvas.getState().sectionFilled)
                         {
                            canvas.getState().sectionFilled = true;
                            canvas = sectionRenderer.renderCurrentSection(canvas);
                         }
                         // build columns
                         drawTableHeader(canvas);
                      }
                      // renderer should change the current page state accordingly
                      previousSectionRenderer.renderCurrentSection(canvas);
                      /** change state for the possibly next pages with extended columns*/
                      PageState state = canvas.getState();
                      float y = state.y;
                      while ((canvas = linkedPages.get(Integer.valueOf(canvas.getPageNumber()))) != null)
                      {
                         state = canvas.getState();
                         state.column = 0;
                         state.row++;
                         state.y = y;
                         state.x = margin[LEFT];
                      }
                   }
                   catch (IOException e)
                   {
                      LOG.warning("", e);
                   }
                }
             }

             SectionRenderer<TRowObject> sectionRenderer = getSectionRenderer();

             if (sectionRenderer != null)
             {
                sectionRenderer.accept(t);
                if (sectionRenderer.isNewSection(t))
                {
                   try
                   {
                      IPageCanvas canvas = getFirstAvailablePageForSection(sectionRenderer);

                      //force to break page new if new page should be used for new section
                      if (!canvas.getColumnRange().isEmpty() && isBreakPageForNewSection())
                      {
                         canvas.getState().filled = true;
                         // get page index and remove it since it has been filled
                         int index = canvas.getPageNumber() - 1;
                         linkedPages.remove(Integer.valueOf(index));
                         // skip pages with extended columns
                         while ((canvas = linkedPages.get(Integer.valueOf(canvas.getPageNumber()))) != null)
                         {
                            canvas.getState().filled = true;
                            // get page index and remove it since it has been filled
                            index = canvas.getPageNumber() - 1;
                            linkedPages.remove(Integer.valueOf(index));
                         }

                         canvas = buildHeaderAndFooterTemplate(addNewPage());
                      }

                      sectionRenderer.render(t, canvas);
                      canvas.getState().sectionFilled = true;
                   }
                   catch (IOException e)
                   {
                      LOG.warning("", e);
                   }
                }
             }

             try
             {
                IPageCanvas canvas = getFirstAvailablePageForRow(t);
                if (canvas.getColumnRange().isEmpty())
                {
                   // build section
                   if (sectionRenderer != null && !canvas.getState().sectionFilled)
                   {
                      canvas.getState().sectionFilled = true;
                      canvas = sectionRenderer.renderCurrentSection(canvas);
                   }
                   // build columns
                   drawTableHeader(canvas);
                }
                drawTableRow(t, canvas);

             }
             catch (IOException e)
             {
                LOG.warning("", e);
             }
          }

          /**
           * Draw final section.
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          void drawFinalSection() throws IOException
          {
             SectionRenderer<TRowObject> previousSectionRenderer = getFooterSectionRenderer();

             if (previousSectionRenderer != null)
             {
                IPageCanvas canvas = getFirstAvailablePageForSection(previousSectionRenderer);
                if (canvas.getColumnRange().isEmpty())
                {
                   // build the previous section
                   if (sectionRenderer != null && !canvas.getState().sectionFilled)
                   {
                      canvas.getState().sectionFilled = true;
                      canvas = sectionRenderer.renderCurrentSection(canvas);
                   }
                   // build columns
                   drawTableHeader(canvas);
                }
                // renderer should change the current page state accordingly
                previousSectionRenderer.renderCurrentSection(canvas);
             }
          }

          /**
           * Draw table row.
           *
           * @param    t
           *           The row
           * @param    canvas
           *           The canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          void drawTableRow(TRowObject t, IPageCanvas canvas) throws IOException
          {
             IPageCanvas c = canvas;

             for(ColumnInfo<TRowObject> column : columns)
             {
                cellRenderer.render(t, column, c);
             };

             PageState state = canvas.getState();
             c = canvas;
             /** change report state for the next row*/
             state.row++;
             state.column = 0;
             state.y -= state.nextRowOffset;
             state.nextRowOffset = 0;
             state.x = margin[LEFT];

             while ((c = linkedPages.get(c.getPageNumber())) != null)
             {
                state = c.getState();
                state.row++;
                state.column = 0;
                state.y -= state.nextRowOffset;
                state.nextRowOffset = 0;
                state.x = margin[LEFT];
             }
          }

          /**
           * Draw report header.
           *
           * @param    header
           *           The header
           * @param    canvas
           *           The canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          void drawReportHeader(THeaderObject header, IPageCanvas canvas) throws IOException
          {
             header.setPage(String.valueOf(canvas.getPageNumber()));
             headerRenderer.render(header, canvas);
          }

          /**
           * Draw table header.
           *
           * @param    canvas
           *           The canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          void drawTableHeader(IPageCanvas canvas) throws IOException
          {
             IPageCanvas c = canvas;
             for(ColumnInfo<TRowObject> column : columns)
             {
                c = columnHeaderRenderer.render(column, c);
             }

             /** change state */
             PageState state = canvas.getState();
             state.column = 0;
             state.row++;
    //         canvas.strokeHorizontal(margin[LEFT], state.y, state.x - margin[LEFT], 0.5f);
             state.y -= columnHeaderRenderer.getHeight();
             /** draw horizontal line under this header content */
    //         canvas.strokeHorizontal(margin[LEFT], state.y, state.x - margin[LEFT], 0.5f);
             state.x = margin[LEFT];

             while ((canvas = linkedPages.get(canvas.getPageNumber())) != null)
             {
                state = canvas.getState();
                state.column = 0;
                state.row++;
    //            canvas.strokeHorizontal(margin[LEFT], state.y, state.x - margin[LEFT], 0.5f);
                state.y -= columnHeaderRenderer.getHeight();
                /** draw horizontal line under this header content */
    //            canvas.strokeHorizontal(margin[LEFT], state.y, state.x - margin[LEFT], 0.5f);
                state.x = margin[LEFT];
             }
          }

          /**
           * Draw report footer.
           *
           * @param    footer
           *           The footer
           * @param    canvas
           *           The canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          void drawReportFooter(TFooterObject footer, IPageCanvas canvas) throws IOException
          {
             footer.setPage(String.valueOf(canvas.getPageNumber()));
             footerRenderer.render(footer, canvas);
          }

          /**
           * Checks if the column text is fitted this page.
           *
           * @param    stringWidth
           *           The width of the target column text
           * @param    fontSize
           *           The font size
           * @param    x
           *           The x-axis coordinate of the most left text position
           * @param    y
           *           The y-axis coordinate of the most left text position
           * @param    widthLimit
           *           The width limit
           * @param    heightLimit
           *           The height limit
           *
           * @return   true, if the column is fitted this page, otherwise false.
           */
          boolean isColumnTextFittedThisPage(
                   float stringWidth, float fontSize,
                   float x, float y,
                   float widthLimit, float heightLimit)
          {
             if (((x + stringWidth) <= (widthLimit - margin[RIGHT])) &&
                 ((y - fontSize) >= (margin[BOTTOM] + footerFontSize)) &&
                 (x >= margin[LEFT]) &&
                 (y <= (heightLimit - margin[TOP] - headerFontSize)))
             {
                return true;
             }

             return false;
          }

          /**
           * Gets the first available page for the given section.
           *
           * @param    r
           *           The given section represented by the section renderer
           *
           * @return   The first available page for section
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          IPageCanvas getFirstAvailablePageForSection(SectionRenderer<TRowObject> r)
          throws IOException
          {
             float outerHeight = r.getHeight();

             Iterator<Entry<Integer,IPageCanvas>> iter = linkedPages.entrySet().iterator();

             while(iter.hasNext())
             {
                Entry<Integer,IPageCanvas> entry = iter.next();
                IPageCanvas c = entry.getValue();
                PageState s = c.getState();
                if (!s.filled)
                {
                   if (s.y >= (getContentLowBound() + getMinCellHeight() + outerHeight))
                   {
                      return c;
                   }
                   else
                   {
                      s.filled = true;
                      iter.remove();
                   }
                }
             }

             return buildHeaderAndFooterTemplate(addNewPage());
          }

          /**
           * Gets the first available page for the given row.
           *
           * @param    t
           *           The given row
           *
           * @return   The first available page for row
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          IPageCanvas getFirstAvailablePageForRow(TRowObject t) throws IOException
          {
             //calculate row height
             float spaceHeight = getCellOuterSpace();
             float rowHeight = spaceHeight;

             int rows = 1;

             for(ColumnInfo<TRowObject> column : columns)
             {
                rows = Math.max(cellRenderer.calculateRowsNumber(t, column), rows);
             }

             rowHeight += rows * cellFontSize;

             // take into account row breaks
             //TODO important to add row breaks dynamically if it is required
             if (rows > 1)
             {
                float availablePage = getAvailablePageSpace();

                int breaksNumber =(int) Math.ceil(rowHeight / availablePage);
                float minRowHeight = getCellFontSize() + getCellOuterSpace();
                rowHeight += (breaksNumber * minRowHeight);
             }

             cellRenderer.setHeight(rowHeight);
             cellRenderer.setRowsNumber(rows);

             Iterator<Entry<Integer,IPageCanvas>> iter = linkedPages.entrySet().iterator();

             while(iter.hasNext())
             {
                Entry<Integer,IPageCanvas> entry = iter.next();
                IPageCanvas c = entry.getValue();
                PageState s = c.getState();
                if (!s.filled)
                {
                   if (s.y >= (getContentLowBound() + getMinCellHeight()))
                   {
                      return c;
                   }
                   else
                   {
                      s.filled = true;
                      iter.remove();
                   }
                }
             }

             return buildHeaderAndFooterTemplate(addNewPage());
          }

          /**
           * Gets the next page.
           *
           * @param    t
           *           The given row
           * @param    c
           *           The current page canvas
           * @param    columnInfo
           *           The current column info
           *
           * @return   The next available page
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          IPageCanvas getNextPage(TRowObject t, IPageCanvas c, ColumnInfo<?> columnInfo)
          throws IOException
          {
             int i = c.getPageNumber();
             IPageCanvas next;
             while ((next = linkedPages.get(Integer.valueOf(i))) != null)
             {
                if (next.getColumnRange().contains(columnInfo))
                {
                   return next;
                }

                i++;
             }

             // prepare new pages for all columns

             next = buildHeaderAndFooterTemplate(addNewPage());
             // build section

             if (sectionRenderer != null)
             {
                boolean newSection = sectionRenderer.isNewSection(t);
                if (!next.getState().sectionFilled  || newSection)
                {
                   next.getState().sectionFilled = true;
                   if (newSection)
                   {
                      next = sectionRenderer.render(t, next);
                   }
                   else
                   {
                      next = sectionRenderer.renderCurrentSection(next);
                   }
                }
             }
             // build columns
             drawTableHeader(next);

             // find page for the given column
             if (next.getColumnRange().contains(columnInfo))
             {
                return next;
             }

             i = next.getPageNumber();

             while ((next = linkedPages.get(Integer.valueOf(i))) != null)
             {
                if (next.getColumnRange().contains(columnInfo))
                {
                   return next;
                }

                i++;
             }

             throw new IllegalStateException("current column: " + columnInfo.toString());
          }

          /**
           * Adds the new page.
           *
           * @return   The new page canvas
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          IPageCanvas addNewPage() throws IOException
          {
             splitDocAndReleaseResources();

             PageState state = new PageState();

             /** initial state */
             if (orient == PaperOrientation.LANDSCAPE)
             {
                state.widthLimit = reportSize.getHeight();
                state.heightLimit = reportSize.getWidth();
             }
             else
             {
                state.widthLimit = reportSize.getWidth();
                state.heightLimit = reportSize.getHeight();
             }
             state.x = margin[LEFT];
             state.y = state.heightLimit - margin[TOP] - headerRenderer.getHeight();
             state.row = -1;
             state.column = 0;

             if (orient == PaperOrientation.LANDSCAPE)
             {
                /**
                 *  |portrait_x|   | 0 -1 landscape_height |   | landscape_x |
                 *  |portrait_y| = | 1  0        0         | * | landscape_y |
                 *  |    1     |   | 0  0        1         |   |     1       |
                 */
                state.pageTransformation = new Matrix(0, 1, -1, 0, state.heightLimit, 0);
             }

             currentPageIndex++;

             state.page = currentPageIndex + 1;

             Integer previewPage = getPreviewPage();

             PDPage page = null;

             if (this.previewOrSelfPrintedMode && Integer.valueOf(state.page).equals(previewPage)
                      || !this.previewOrSelfPrintedMode)
             {
                page = new PDPage(reportSize);

                if (orient == PaperOrientation.LANDSCAPE)
                {
                   page.setRotation(90);
                }

                doc.addPage(page);
             }

             PageCanvas pageCanvas = new PageCanvas(doc, page, currentPageIndex + 1, state);

             linkedPages.put(currentPageIndex, pageCanvas);

             return pageCanvas;
          }

          /**
           * Split doc and release resources.
           *
           * @throws   IOException
           *           The IO exception if there is a failed output operation
           */
          private void splitDocAndReleaseResources() throws IOException
          {
             if (previewOrSelfPrintedMode)
             {
                return;
             }

             if (currentPageIndex % maxPagesNumber == 0 && currentPageIndex > 0)
             {
                requiredToRefreshDoc = true;
             }

             if (requiredToRefreshDoc && linkedPages.isEmpty())
             {
                if (selfPrinted)
                {
                   // set true to skip the next pages >= MAX_PAGE_NUMBER
                   // stop to build this report
                   previewOrSelfPrintedMode = true;
                }
                else
                {
                   // continue to build this report
                   ByteArrayOutputStream bout = new ByteArrayOutputStream();
                   doc.save(bout);
                   doc.close();
                   doc = null;
                   splittedDoc.add(bout.toByteArray());
                   bout = null;
                   doc = new PDDocument();
                   requiredToRefreshDoc = false;
                }
             }
          }
       }
    }