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