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