/* * Copyright © 2015 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.cli.util.table; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.inject.Inject; import java.io.PrintStream; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; /** * {@link TableRenderer} implementation to print an ASCII table in the alt style. 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 | * +=======================================================================+ * * E.g. when cells are multiple lines: * * +========================================================================================+ * | c1 | c2 | c3333 | * +========================================================================================+ * | r1zzzzzzzzzzzzzzzzzzzzzzzz | r11 | r1 | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * | zzzzzzzzzzzzzzzzzzzzzzzzzz | | | * |----------------------------------------------------------------------------------------| * | r2 | r2222 zzzzzzz z z z zzzzzz | r | * | | z z zzzzzzzzz zzzzzzz zzzz | | * |----------------------------------------------------------------------------------------| * | r3333 | r3 | r3 | * | | | 1 | * +========================================================================================+ */ public class AltStyleTableRenderer implements TableRenderer { private static final int DEFAULT_MIN_COLUMN_WIDTH = 5; private static final String DEFAULT_NEWLINE = System.getProperty("line.separator"); private final int minColumnWidth; private final Splitter newlineSplitter; @Inject public AltStyleTableRenderer() { this(DEFAULT_MIN_COLUMN_WIDTH, DEFAULT_NEWLINE); } public AltStyleTableRenderer(int minColumnWidth, String newline) { this.minColumnWidth = minColumnWidth; this.newlineSplitter = Splitter.on(newline); } @Override public void render(TableRendererConfig config, PrintStream output, Table table) { // outer table width int width = config.getLineWidth(); List<String> tableHeader = table.getHeader(); Iterable<List<String>> tableRows = Iterables.filter(table.getRows(), new Predicate<List<String>>() { @Override public boolean apply(@Nullable List<String> input) { return input != null; } }); List<Row> rows = Lists.newArrayList(); // Collects all output cells for all records. // If any record has multiple lines output, a row divider is printed between each row. // inner column widths int[] columnWidths = calculateColumnWidths(tableHeader, tableRows, width); if (columnWidths.length == 0) { return; } boolean useRowDivider = false; for (List<String> row : tableRows) { useRowDivider = generateRow(row, columnWidths, rows) || useRowDivider; } // If has header, prints the header. if (!tableHeader.isEmpty()) { List<Row> headerRow = Lists.newArrayList(); generateRow(tableHeader, columnWidths, headerRow); outputDivider(output, columnWidths, '=', '+'); for (Row row : headerRow) { printRow(output, columnWidths, row); } } // 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. printRow(output, columnWidths, row); } outputDivider(output, columnWidths, '=', '+'); } private void printRow(PrintStream output, int[] columnWidths, Row row) { boolean done = false; int line = 0; while (!done) { done = true; for (int i = 0; i < row.size(); i++) { Cell cell = row.get(i); output.printf("|"); if (columnWidths[i] != 0) { cell.output(output, " %-" + columnWidths[i] + "s ", line); } done = done && (line + 1 >= cell.size()); } output.printf("|").println(); line++; } } /** * 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) { if (columnWidth != 0) { 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 row The row containing the set of columns to output. * @param columnWidths The widths of each column. * @param collection Collection for collecting the generated {@link Row} object. * @return Returns true if the row spans multiple lines. */ private boolean generateRow(List<String> row, int[] columnWidths, Collection<? super Row> collection) { ImmutableList.Builder<Cell> builder = ImmutableList.builder(); boolean multiLines = false; for (int column = 0; column < row.size(); column++) { Object field = row.get(column); int width = columnWidths[column]; if (width == 0) { builder.add(new Cell()); continue; } String fieldString = field == null ? "" : field.toString(); Iterable<String> splitField = newlineSplitter.split(fieldString); List<String> cellLines = Lists.newArrayList(); for (String splitFieldLine : splitField) { if (splitFieldLine.length() <= width) { cellLines.add(splitFieldLine); } else { // line is too long, split and only allow width-long lines int startSplitIdx = 0; int endSplitIdx = width; while (endSplitIdx < splitFieldLine.length()) { cellLines.add(splitFieldLine.substring(startSplitIdx, endSplitIdx)); startSplitIdx = endSplitIdx; endSplitIdx = startSplitIdx + width; } // add any remaining part of the splitFieldLine string if (startSplitIdx < splitFieldLine.length()) { cellLines.add(splitFieldLine.substring(startSplitIdx, splitFieldLine.length())); } multiLines = true; } } Cell cell = new Cell(cellLines); multiLines = multiLines || cell.size() > 1; builder.add(cell); } collection.add(new Row(builder.build())); return multiLines; } /** * Calculates the maximum inner column widths. * * @param header The table header. * @param rows All rows that is going to display. * @param maxOuterTableWidth Maximum outer width of the table. * @return An array of integers, with contains maximum width for each column. */ private int[] calculateColumnWidths(List<String> header, Iterable<List<String>> rows, int maxOuterTableWidth) { int[] widths; if (!header.isEmpty()) { widths = new int[header.size()]; } else if (rows.iterator().hasNext()) { widths = new int[rows.iterator().next().size()]; } else { return new int[0]; } // max(header or content length of cells in row) for every column int[] maxColumnWidths = new int[widths.length]; if (!header.isEmpty()) { for (int i = 0; i < maxColumnWidths.length; i++) { String headerValue = header.get(i); if (headerValue != null) { maxColumnWidths[i] = Math.max(maxColumnWidths[i], headerValue.length()); } } } for (List<String> row : rows) { for (int i = 0; i < maxColumnWidths.length; i++) { String cell = row.get(i); if (cell != null) { maxColumnWidths[i] = Math.max(maxColumnWidths[i], cell.length()); } } } int numBorderAndSpaceChars = (widths.length + 1) // for the '|' borders + (2 * widths.length); // for the spaces within each column // outer width of the table if every row was in a single line int flatOuterTableWidth = numBorderAndSpaceChars; for (int maxColumnWidth : maxColumnWidths) { flatOuterTableWidth += maxColumnWidth; } // may not need the entire maxOuterTableWidth, so downsize if necessary int actualOuterTableWidth = Math.min(maxOuterTableWidth, flatOuterTableWidth); int remainingInnerTableWidth = actualOuterTableWidth - numBorderAndSpaceChars; int maxInnerTableWidth = maxOuterTableWidth - numBorderAndSpaceChars; int defaultWidthPerColumn = (int) (maxInnerTableWidth * 1.0 / widths.length); // downsize any column widths if they're < defaultWidthPerColumn for (int i = 0; i < maxColumnWidths.length; i++) { int maxColumnWidth = maxColumnWidths[i]; if (maxColumnWidth < defaultWidthPerColumn) { widths[i] = maxColumnWidth; remainingInnerTableWidth -= maxColumnWidth; } } // distribute remainingInnerTableWidth equally to the remaining columns int widthPerRemainingColumn = (int) (remainingInnerTableWidth * 1.0 / widths.length); for (int i = 0; i < widths.length; i++) { // 0 means width is not yet set if (widths[i] == 0) { widths[i] = widthPerRemainingColumn; remainingInnerTableWidth -= widths[i]; // fix any rounding issues by resizing the last column width if (i == widths.length - 1) { widths[i] += remainingInnerTableWidth; remainingInnerTableWidth = 0; } } } 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; } Cell() { this(Collections.<String>emptyList()); } /** * 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(); } } }