co.cask.cdap.shell.util.AsciiTable.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.shell.util.AsciiTable.java

Source

/*
 * Copyright  2012-2014 Cask Data, Inc.
 *
 * 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 co.cask.cdap.shell.util;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

import java.io.PrintStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Utility class to print an ASCII table. e.g.
 *
 * +=======================================================================+
 * | pid                                  | end status | start      | stop |
 * +=======================================================================+
 * | 9bd22850-0017-4a10-972a-bc5ca8173584 | STOPPED    | 1405986408 | 0    |
 * | 7f9f8054-a71f-48e3-965d-39e2aab16d5d | STOPPED    | 1405978322 | 0    |
 * | e1a2d4a9-667c-40e0-86fa-32ea68cc25f6 | STOPPED    | 1405645401 | 0    |
 * | 9276574a-cc2f-458c-973b-aed9669fc80e | STOPPED    | 1405644974 | 0    |
 * | 1c5868d6-04c7-443b-b4db-aab1c3368be3 | STOPPED    | 1405457462 | 0    |
 * | 4003fa1d-15bd-4a09-ad2b-f2c52b4dda54 | STOPPED    | 1405456719 | 0    |
 * | 531dff0a-0441-424b-ae5b-023cc7383344 | STOPPED    | 1405454043 | 0    |
 * | d9cae8f9-3fd3-45f4-b4e9-102ef38cf4e1 | STOPPED    | 1405371545 | 0    |
 * +=======================================================================+
 *
 * @param <T> type of object that the rows represent
 */
public class AsciiTable<T> {

    private final List<String> header;
    private final List<T> records;
    private final RowMaker<T> rowMaker;

    /**
     * @param header strings representing the header of the table
     * @param records list of objects that represent the rows
     * @param rowMaker makes Object arrays from a row object
     */
    public AsciiTable(@Nullable String[] header, List<T> records, RowMaker<T> rowMaker) {
        this.header = (header == null) ? ImmutableList.<String>of() : ImmutableList.copyOf(header);
        this.records = records;
        this.rowMaker = rowMaker;
    }

    /**
     * Prints the ASCII table to the {@link PrintStream} output.
     *
     * @param output {@link PrintStream} to print to
     */
    public void print(PrintStream output) {

        // Collects all output cells for all records.
        // If any record has multiple lines output, a row divider is printed between each row.
        boolean useRowDivider = false;
        List<Row> rows = Lists.newArrayList();
        for (T row : records) {
            useRowDivider = generateRow(rowMaker.makeRow(row), rows) || useRowDivider;
        }

        int[] columnWidths = calculateColumnWidths(header, rows);

        // If has header, prints the header.
        if (!header.isEmpty()) {
            outputDivider(output, columnWidths, '=', '+');
            for (int i = 0; i < columnWidths.length; i++) {
                output.printf("| %-" + columnWidths[i] + "s ", header.get(i));
            }
            output.printf("|").println();
        }

        // Prints a divider between header and first row if no divider is needed between rows.
        // Otherwise it's printed as part of the following row loop.
        char edgeChar = '+';
        char lineChar = '=';
        if (!useRowDivider) {
            outputDivider(output, columnWidths, lineChar, edgeChar);
        }

        // Output each row.
        for (Row row : rows) {
            if (useRowDivider) {
                // The first divider uses a different set of line and edge char
                // As it's either the separate for the header of the table border (without header case)
                outputDivider(output, columnWidths, lineChar, edgeChar);
                edgeChar = '|';
                lineChar = '-';
            }

            // Print each cell. It has to loop until all lines from all cells are printed.
            boolean done = false;
            int line = 0;
            while (!done) {
                done = true;
                for (int i = 0; i < row.size(); i++) {
                    Cell cell = row.get(i);
                    cell.output(output, "| %-" + columnWidths[i] + "s ", line);
                    done = done && (line + 1 >= cell.size());
                }
                output.printf("|").println();
                line++;
            }
        }
        outputDivider(output, columnWidths, '=', '+');
    }

    /**
     * Prints a divider.
     *
     * @param output The {@link PrintStream} to output to
     * @param columnWidths Columns widths for each column
     * @param lineChar Character to use for printing the divider line
     * @param edgeChar Character to use for the left and right edge character
     */
    private void outputDivider(PrintStream output, int[] columnWidths, char lineChar, char edgeChar) {
        output.print(edgeChar);
        for (int columnWidth : columnWidths) {
            output.print(Strings.repeat(Character.toString(lineChar), columnWidth + 2));
        }

        // one for each divider
        output.print(Strings.repeat(Character.toString(lineChar), columnWidths.length - 1));
        output.print(edgeChar);
        output.println();
    }

    /**
     * Generates a record row. A record row can span across multiple lines on the screen.
     *
     * @param columns The set of columns to output.
     * @param collection Collection for collecting the generated {@link Row} object.
     * @return Returns true if the row spans multiple lines.
     */
    private boolean generateRow(Object[] columns, Collection<? super Row> collection) {
        ImmutableList.Builder<Cell> builder = ImmutableList.builder();

        boolean multiLines = false;
        Splitter splitter = Splitter.on(System.getProperty("line.separator"));
        for (Object field : columns) {
            String fieldString = field == null ? "" : field.toString();
            Cell cell = new Cell(splitter.split(fieldString));
            multiLines = multiLines || cell.size() > 1;
            builder.add(cell);
        }

        collection.add(new Row(builder.build()));
        return multiLines;
    }

    /**
     * Calculates the maximum columns' widths.
     *
     * @param header The table header.
     * @param rows All rows that is going to display.
     * @return An array of integers, with contains maximum width for each column.
     */
    private int[] calculateColumnWidths(List<String> header, List<Row> rows) {
        int[] widths = new int[header.isEmpty() ? rows.get(0).size() : header.size()];

        for (int i = 0; i < header.size(); i++) {
            widths[i] = header.get(i).length();
        }

        // Find the maximum width for each column by consulting the width of each cell.
        for (Row row : rows) {
            for (int i = 0; i < row.size(); i++) {
                Cell cell = row.get(i);
                if (cell.getWidth() > widths[i]) {
                    widths[i] = cell.getWidth();
                }
            }
        }
        return widths;
    }

    /**
     * Represents data in one output table cell, which the content can spans multiple lines.
     */
    private static final class Cell implements Iterable<String> {

        private final List<String> content;
        private final int width;

        Cell(Iterable<String> content) {
            this.content = ImmutableList.copyOf(content);
            int maxWidth = 0;
            for (String row : content) {
                if (row.length() > maxWidth) {
                    maxWidth = row.length();
                }
            }
            this.width = maxWidth;
        }

        /**
         * Returns the maximum width of this cell content.
         */
        int getWidth() {
            return width;
        }

        /**
         * Writes a line to the given output with the given format.
         *
         * @param output The {@link PrintStream} to write to.
         * @param format The formatting string to use for printing.
         * @param line The line within this cell.
         */
        void output(PrintStream output, String format, int line) {
            output.printf(format, line >= content.size() ? "" : content.get(line));
        }

        /**
         * Returns the number of rows span for the content in this cell.
         */
        int size() {
            return content.size();
        }

        @Override
        public Iterator<String> iterator() {
            return content.iterator();
        }
    }

    /**
     * Represents a Row content in the output Table. Each row contains multiple cells.
     */
    private static final class Row implements Iterable<Cell> {

        private final List<Cell> cells;

        private Row(Iterable<Cell> cells) {
            this.cells = ImmutableList.copyOf(cells);
        }

        @Override
        public Iterator<Cell> iterator() {
            return null;
        }

        /**
         * Returns the {@link Cell} at the given column.
         */
        Cell get(int i) {
            return cells.get(i);
        }

        /**
         * Returns the number of cells this row contains.
         */
        int size() {
            return cells.size();
        }
    }
}