boxtable.table.Table.java Source code

Java tutorial

Introduction

Here is the source code for boxtable.table.Table.java

Source

/*
 * Copyright 2017 Dominik Helm
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package boxtable.table;

import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle;

import boxtable.cell.Cell;
import boxtable.cell.TextCell;
import boxtable.common.CellFiller;
import boxtable.event.EventType;
import boxtable.event.TableEventSource;

/**
 * Represents a table to be added to a PDF document
 * 
 * @author Dominik Helm
 */
public class Table extends TableEventSource {
    /** Columns of this table */
    private final List<Column> columns = new ArrayList<>();
    /** Rows of this table */
    private final List<Row> rows = new ArrayList<>();

    /** A CellFiller to paint cell backgrounds */
    private CellFiller filler = null;

    /** Number of header rows */
    private int numHeaderRows = 1;

    /** Caches the widths of the columns */
    private float[] columnWidths = null;

    /**
     * Creates a table
     */
    public Table() {
        setBorder(1, 1, 1, 1);
    }

    /**
     * Adds a cell to a given row and column, setting its formatting
     * 
     * @param cell
     *            The cell to add
     * @param row
     *            The row the cell will be added to
     * @param column
     *            The column to take formatting data from
     */
    private void addCell(final Cell cell, final Row row, final int column) {
        Column col = columns.get(column);
        if (cell.getHAlign() == null) {
            cell.setHAlign(col.getHAlign());
        }
        if (cell.getVAlign() == null) {
            cell.setVAlign(col.getVAlign());
        }
        if (cell.getTopBorder() < 0) {
            cell.setTopBorder(col.getTopBorder());
        }
        if (cell.getLeftBorder() < 0) {
            cell.setLeftBorder(col.getLeftBorder());
        }
        if (cell.getRightBorder() < 0) {
            cell.setRightBorder(col.getRightBorder());
        }
        if (cell.getBottomBorder() < 0) {
            cell.setBottomBorder(col.getBottomBorder());
        }
        if (cell instanceof TextCell) {
            if (((TextCell) cell).getFont() == null) {
                ((TextCell) cell).setFont(col.getFont());
            }
            if (((TextCell) cell).getMinFontSize() < 0) {
                ((TextCell) cell).setMinFontSize(col.getMinFontSize());
            }
            if (((TextCell) cell).getMaxFontSize() < 0) {
                ((TextCell) cell).setMaxFontSize(col.getMaxFontSize());
            }
        }

        row.addCell(cell);
    }

    /**
     * Adds cells to this table, creating TextCells with toString() for non-cell objects
     * 
     * @param cells
     *            The cells to add to the table
     * @return This Table, for a fluent interface
     */
    public Table addCells(final Object... cells) {
        Row row = null;
        int cols = 0;
        if (rows.isEmpty()) {
            row = new Row();
            rows.add(row);
        } else {
            row = rows.get(rows.size() - 1);
            for (final Cell cell : row.getCells()) {
                cols += cell.getColSpan();
            }
        }
        for (final Object cell : cells) {
            if (cell == null) {
                continue;
            }
            if (cols >= columns.size()) {
                row = new Row();
                rows.add(row);
                cols = 0;
            }
            if (cell instanceof Cell) {
                addCell((Cell) cell, row, cols);
                cols += ((Cell) cell).getColSpan();
            } else {
                addCell(new TextCell(cell.toString()), row, cols);
                ++cols;
            }
        }
        return this;
    }

    /**
     * Adds a column to this table
     * 
     * @param column
     *            The column to be added
     * @return This Table, for a fluent interface
     */
    public Table addColumn(final Column column) {
        columns.add(column);
        return this;
    }

    /**
     * Adds a row of cells to this table, creating TextCells with toString() for non-cell objects, completing the row with empty cells if necessary
     * 
     * @param cells
     *            The cells to add to the table
     * @return This Table, for a fluent interface
     */
    public Table addRow(final Object... cells) {
        return addRowAtIndex(rows.size(), cells);
    }

    /**
     * Adds a row of cells to this table at a specified index, creating TextCells with toString() for non-cell objects, completing the row with empty cells if
     * necessary
     * 
     * @param index
     *            The index the row is added at
     * @param cells
     *            The cells to add to the table
     * @return This Table, for a fluent interface
     */
    public Table addRowAtIndex(final int index, final Object... cells) {
        final Row row = new Row();
        for (int i = 0, j = 0; i < columns.size();) {
            if (j < cells.length) {
                final Object cell = cells[j];
                if (cell == null) {
                    continue;
                }
                if (cell instanceof Cell) {
                    addCell((Cell) cell, row, i);
                    i += ((Cell) cell).getColSpan();
                } else {
                    addCell(new TextCell(cell.toString()), row, i);
                    ++i;
                }
            } else {
                addCell(new Cell(), row, i);
                ++i;
            }
            ++j;
        }
        rows.add(index, row);

        return this;
    }

    /**
     * Completes the current row by adding empty cells as necessary
     * 
     * @return This Table, for a fluent interface
     */
    public Table completeRow() {
        if (!rows.isEmpty()) {
            final Row currentRow = rows.get(rows.size() - 1);
            int numCols = 0;
            for (int i = 0; i < currentRow.size(); ++i) {
                final Cell cell = currentRow.getCell(i);
                numCols += cell.getColSpan();
            }
            while (numCols < columns.size()) {
                addCell(new Cell(), currentRow, numCols);
                ++numCols;
            }
        }
        return this;
    }

    /**
     * Returns a new Table that is identical to the old one, but without contents or event handlers.
     * 
     * @return A new table with the same columns and appearance
     */
    public Table duplicate() {
        Table result = new Table();
        result.columns.addAll(columns);
        result.filler = filler;
        result.numHeaderRows = numHeaderRows;
        result.setBorder(getTopBorder(), getLeftBorder(), getRightBorder(), getBottomBorder());
        return result;
    }

    /**
     * Returns the widths for a column
     * 
     * @param index
     *            The index of the column
     * @return The column's width
     * @throws IOException
     *             If accessing information for calculating the width fails
     */
    private float getColumnWidth(final int index) throws IOException {
        final Column column = columns.get(index);
        final float minWidth = column.getMinWidth();
        final float maxWidth = column.getMaxWidth();
        if (minWidth == maxWidth)
            return minWidth;
        float result = minWidth;
        for (final Row row : rows) {
            Cell cell = null;
            int i = 0;
            for (int j = 0; i <= index; ++j) {
                cell = row.getCell(j);
                i += cell.getColSpan();
            }
            if (cell.getColSpan() != 1) {
                continue;
            }
            result = Math.max(result, cell.getWidth());
        }
        return Math.min(result, maxWidth);
    }

    /**
     * Returns the widths for the columns of this table
     * 
     * @param width
     *            The width of the table
     * @return The column widths
     * @throws IOException
     *             If accessing information for calculating the width fails
     */
    private float[] getColumnWidths(final float width) throws IOException {
        if (columnWidths != null)
            return columnWidths;
        columnWidths = new float[columns.size()];
        float sum = 0;
        for (int i = 0; i < columns.size() - 1; ++i) {
            sum += columnWidths[i] = getColumnWidth(i);
        }
        columnWidths[columnWidths.length - 1] = width - sum;
        return columnWidths;
    }

    /**
     * Returns the height of this table
     * 
     * @param width
     *            The width of the table
     * @return The height
     * @throws IOException
     *             If accessing information for calculating the height fails
     */
    public float getHeight(final float width) throws IOException {
        float[] colWidths = getColumnWidths(width);
        float height = 0;
        for (int i = 0; i < rows.size(); ++i) {
            height += rows.get(i).getHeight(colWidths);
        }
        return height;
    }

    /**
     * Returns the number of columns of this table
     * 
     * @return The number of columns
     */
    public int getNumColumns() {
        return columns.size();
    }

    /**
     * Returns the number of rows in this table's header
     * 
     * @return The number of header rows
     */
    public int getNumHeaderRows() {
        return numHeaderRows;
    }

    /**
     * Returns the number of rows of this table
     * 
     * @return The number of rows
     */
    public int getNumRows() {
        return rows.size();
    }

    /**
     * Returns the list of rows of this table
     * 
     * @return The list of rows
     */
    public List<Row> getRows() {
        return rows;
    }

    /**
     * Starts a new page with the same size as the last one
     * 
     * @param document
     *            The document the table is rendered to
     * @param stream
     *            The PDPageContentStream used to render the table up to now (will be closed after calling this method)
     * @return A new PDPageContentStream for rendering to the new page
     * @throws IOException
     *             If writing to the streams fails
     */
    private PDPageContentStream newPage(final PDDocument document, final PDPageContentStream stream)
            throws IOException {
        final PDRectangle pageSize = document.getPage(document.getNumberOfPages() - 1).getMediaBox();
        handleEvent(EventType.END_PAGE, document, stream, 0, pageSize.getHeight(), pageSize.getWidth(),
                pageSize.getHeight());
        stream.close();
        final PDPage page = new PDPage(pageSize);
        document.addPage(page);
        PDPageContentStream newStream = new PDPageContentStream(document, page, AppendMode.APPEND, true);
        handleEvent(EventType.BEGIN_PAGE, document, newStream, 0, pageSize.getHeight(), pageSize.getWidth(),
                pageSize.getHeight());
        return newStream;
    }

    /**
     * Renders this table to a document
     * 
     * @param document
     *            The document this table will be rendered to
     * @param width
     *            The width of the table
     * @param left
     *            The left edge of the table
     * @param top
     *            The top edge of the table
     * @param paddingTop
     *            The amount of free space at the top of a new page (if a page break is necessary)
     * @param paddingBottom
     *            The minimal amount of free space at the bottom of the page before inserting a page break
     * @return The bottom edge of the last rendered table part
     * @throws IOException
     *             If writing to the document fails
     */
    @SuppressWarnings("resource")
    public float render(final PDDocument document, final float width, final float left, float top,
            final float paddingTop, final float paddingBottom) throws IOException {
        float yPos = top;
        final PDPage page = document.getPage(document.getNumberOfPages() - 1);
        final PDRectangle pageSize = page.getMediaBox();
        PDPageContentStream stream = new PDPageContentStream(document, page, AppendMode.APPEND, true);
        float height = getHeight(width);
        if (height > pageSize.getHeight() - paddingTop - paddingBottom) {
            final float[] colWidths = getColumnWidths(width);
            for (int i = 0; i < rows.size(); ++i) {
                if (rows.get(i).getHeight(colWidths) > yPos - paddingBottom) {
                    drawBorder(stream, left, top, width, top - yPos);
                    stream = newPage(document, stream);
                    top = pageSize.getHeight() - paddingTop;
                    yPos = top;
                    yPos = renderRows(document, stream, 0, getNumHeaderRows(), width, left, yPos);
                    i = Math.max(i, getNumHeaderRows());
                }
                yPos = renderRows(document, stream, i, i + 1, width, left, yPos);
            }
            drawBorder(stream, left, top, width, top - yPos);

            handleEvent(EventType.AFTER_TABLE, document, stream, left, top, width, top - yPos);
        } else {
            if (height > top - paddingBottom) {
                stream = newPage(document, stream);
                top = pageSize.getHeight() - paddingTop;
                yPos = top;
            }
            yPos = renderRows(document, stream, 0, -1, width, left, yPos);
            drawBorder(stream, left, top, width, top - yPos);
            handleEvent(EventType.AFTER_TABLE, document, stream, left, top, width, top - yPos);
        }
        stream.close();

        return yPos;
    }

    /**
     * Renders a subset of the rows of this table
     * 
     * @param document
     *            The document the table is rendered to
     * @param stream
     *            The PDPageContentStream used to render the rows
     * @param startIndex
     *            The start of the rows to be rendered (inclusive)
     * @param endIndex
     *            The end of the rows to be rendered (exclusive) or -1 if all rows up to the last one are to be rendered
     * @param width
     *            The width of the table
     * @param left
     *            The left edge of the rendered rows
     * @param top
     *            The top edge of the rendered rows
     * @return The bottom edge of the last rendered row
     * @throws IOException
     *             If writing to the stream fails
     */
    public float renderRows(final PDDocument document, final PDPageContentStream stream, final int startIndex,
            int endIndex, final float width, final float left, final float top) throws IOException {
        if (endIndex == -1) {
            endIndex = rows.size();
        }

        stream.setStrokingColor(Color.BLACK);

        final float[] colWidths = getColumnWidths(width);
        float yPos = top;

        for (int i = startIndex; i < endIndex; ++i) {
            final Row row = rows.get(i);
            final float height = row.getHeight(colWidths);

            handleEvent(EventType.BEFORE_ROW, document, stream, left, yPos, width, height);
            row.handleEvent(EventType.BEFORE_ROW, document, stream, left, yPos, width, height);

            row.render(this, document, stream, i, colWidths, filler, left, yPos, width, height);

            row.handleEvent(EventType.AFTER_ROW, document, stream, left, yPos, width, height);
            handleEvent(EventType.AFTER_ROW, document, stream, left, yPos, width, height);

            yPos -= height;
        }

        return yPos;
    }

    /*
     * (non-Javadoc)
     * 
     * @see boxtable.common.Bordered#setBorder(float, float, float, float)
     */
    @Override
    public Table setBorder(final float top, final float left, final float right, final float bottom) {
        topBorder = top;
        leftBorder = left;
        rightBorder = right;
        bottomBorder = bottom;
        return this;
    }

    /**
     * Sets a CellFiller to paint cell backgrounds
     * 
     * @param filler
     *            The filler that decides the cell background colors
     * @return This Table, for a fluent interface
     */
    public Table setFiller(final CellFiller filler) {
        this.filler = filler;
        return this;
    }

    /**
     * Sets the number of rows in this table's header
     * 
     * @param numHeaderRows
     *            The number of header rows
     * @return This Table, for a fluent interface
     */
    public Table setNumHeaderRows(final int numHeaderRows) {
        this.numHeaderRows = numHeaderRows;
        return this;
    }
}