/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2010 Sun Microsystems, Inc. * Portions copyright 2012-2016 ForgeRock AS. */ package com.forgerock.opendj.cli; import static com.forgerock.opendj.cli.Utils.repeat; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; import org.forgerock.util.Reject; /** * Utility class for printing columns of data. *

* This printer can be used to print data in formatted table or in csv format. *

* Regarding the formatting table feature, this class allows you to specify for each {@link Column}s: *

*

* Code to write data is independent of the {@link MultiColumnPrinter} configuration: *

 * void printData(final MultiColumnPrinter printer) {
 *     String[][] myData = new String[][] {
 *         new String[]{"U.S.A", "34.2", "40.8", ".us"},
 *         new String[]{"United Kingdom", "261.1", "31.6", ".uk"},
 *         new String[]{"France", "98.8", "30.1", ".fr"}
 *     };
 *
 *     int i;
 *     for (String[] countryData : myData) {
 *         i = 0;
 *         for (final MultiColumnPrinter.Column column : printer.getColumns()) {
 *             printer.printData(countryData[i++]);
 *         }
 *     }
 *  }
 * 
*

* The following code sample presents how to create a {@link MultiColumnPrinter} to write CSV data: *

 * final List columns = new ArrayList<>();
 * columns.add(MultiColumnPrinter.column("CountryNameColumnId", "country_name", 0));
 * columns.add(MultiColumnPrinter.column("populationDensityId", "population_density", 1));
 * columns.add(MultiColumnPrinter.column("GiniId", "gini", 1));
 * columns.add(MultiColumnPrinter.column("internetTLDId", "internet_tld", 0));
 * MultiColumnPrinter myCsvPrinter = MultiColumnPrinter.builder(System.out, columns)
 *                                                     .columnSeparator(",")
 *                                                     .build();
 * printData(myCsvPrinter);
 * 
*

* The code above would print: *

 * country_name,population_density,gini,internet_tld
 * U.S.A,34.2,40.8,.us
 * United Kingdom,261.1,31.6,.uk
 * France,98.8,30.1,.fr
 * 
*

* The following code sample presents how to configure a {@link MultiColumnPrinter} * to print the same data on console with some title headers. *

 *     final List columns = new ArrayList<>();
 *     columns.add(MultiColumnPrinter.separatorColumn());
 *     columns.add(MultiColumnPrinter.column("CountryNameColumnId", "Country Name", 15, 0));
 *     columns.add(MultiColumnPrinter.column("populationDensityId", "Density", 10, 1));
 *     columns.add(MultiColumnPrinter.separatorColumn());
 *     columns.add(MultiColumnPrinter.column("GiniID", "GINI", 5, 1));
 *     columns.add(MultiColumnPrinter.column("internetTLDID", "TLD", 5, 0));
 *     columns.add(MultiColumnPrinter.separatorColumn());
 *     MultiColumnPrinter myPrinter = MultiColumnPrinter.builder(System.out, columns)
 *                                                      .format(true)
 *                                                      .columnSeparator("  ")
 *                                                      .titleAlignment(MultiColumnPrinter.Alignment.CENTER)
 *                                                      .build();
 *     myPrinter.printDashedLine();
 *     myPrinter.printTitleSection("General Information", 2);
 *     myPrinter.printTitleSection("Data", 2);
 *     myPrinter.printTitleLine();
 *     myPrinter.printDashedLine();
 *     printData(myPrinter);
 *     myPrinter.printDashedLine();
 * 
*

* The code above would print: *

 * --------------------------------------------------
 * |       General Information     |       Data     |
 * |     Country Name     Density  |   GINI    TLD  |
 * --------------------------------------------------
 * |            U.S.A        34.2  |   40.8    .us  |
 * |   United Kingdom       261.1  |   31.6    .uk  |
 * |           France        98.8  |   30.1    .fr  |
 * --------------------------------------------------
 * 
*/ public final class MultiColumnPrinter { /** The data alignment. */ public enum Alignment { /** Data will be left-aligned. */ LEFT, /** Data will be centered. */ CENTER, /** Data will be right-aligned. */ RIGHT } private static final String SEPARATOR_ID = "separator"; private static int separatorIdNumber; /** * Returns a new separator {@link Column}. *

* This kind of {@link Column} can be used to separate data sections. * * @return A new separator {@link Column}. */ public static Column separatorColumn() { return new Column(SEPARATOR_ID + separatorIdNumber++, "", 1, 0); } /** * Creates a new {@link Column} with the provided arguments. * * @param id * The column identifier. * @param title * The column title. * @param doublePrecision * The double precision used to print {@link Double} data for this column. * See {@link MultiColumnPrinter#printData(Double)}. * @return * A new Column with the provided arguments. */ public static Column column(final String id, final String title, final int doublePrecision) { return new Column(id, title, 1, doublePrecision); } /** * Creates a new Column with the provided arguments. * * @param id * The column identifier. * @param title * The column title. * @param width * The column width. * This information will only be used if the associated * {@link MultiColumnPrinter} is configured to apply formatting. * See {@link Builder#format(boolean)}. * @param doublePrecision * The double precision to use to print data for this column. * @return * A new Column with the provided arguments. */ public static Column column(final String id, final String title, final int width, final int doublePrecision) { return new Column(id, title, Math.max(width, title.length()), doublePrecision); } /** * This class describes a Column of data used in the {@link MultiColumnPrinter}. *

* A column consists in the following fields: *

*/ public static final class Column { private final String id; private final String title; private final int width; private final int doublePrecision; private Column(final String id, final String title, final int width, final int doublePrecision) { this.id = id; this.title = title; this.width = Math.max(width, title.length()); this.doublePrecision = doublePrecision; } /** * Returns this {@link Column} identifier. * * @return This {@link Column} identifier. */ public String getId() { return id; } } /** * Creates a new {@link Builder} to build a {@link MultiColumnPrinter}. * * @param stream * The {@link PrintStream} to use to print data. * @param columns * The {@link List} of {@link Column} data to print. * @return * A new {@link Builder} to build a {@link MultiColumnPrinter}. */ public static Builder builder(final PrintStream stream, final List columns) { return new Builder(stream, columns); } /** A fluent API for incrementally constructing {@link MultiColumnPrinter}. */ public static final class Builder { private final PrintStream stream; private final List columns; private Alignment titleAlignment = Alignment.RIGHT; private String columnSeparator = " "; private boolean format; private Builder(final PrintStream stream, final List columns) { Reject.ifNull(stream); this.stream = stream; this.columns = columns; } /** * Sets whether the {@link MultiColumnPrinter} needs to apply formatting. *
* Default value is {@code false}. * * @param format * {@code true} if the {@link MultiColumnPrinter} needs to apply formatting. * @return This builder. */ public Builder format(final boolean format) { this.format = format; return this; } /** * Sets the alignment for title elements which will be printed by the {@link MultiColumnPrinter}. *

* This is used only if the printer is configured to * apply formatting, see {@link Builder#format(boolean)}. *
* Default value is {@link Alignment#RIGHT}. * * @param titleAlignment * The title alignment. * @return This builder. */ public Builder titleAlignment(final Alignment titleAlignment) { this.titleAlignment = titleAlignment; return this; } /** * Sets the sequence to use to separate column. *

* Default value is {@code " "}. * * @param separator * The sequence {@link String}. * @return This builder. */ public Builder columnSeparator(final String separator) { this.columnSeparator = separator; return this; } /** * Creates a new {@link MultiColumnPrinter} as configured in this {@link Builder}. * * @return A new {@link MultiColumnPrinter} as configured in this {@link Builder}. */ public MultiColumnPrinter build() { return new MultiColumnPrinter(this); } } private final PrintStream stream; private final List columns; private final boolean format; private final Alignment titleAlignment; private final String columnSeparator; private List printableColumns; private final int lineLength; private Iterator columnIterator; private Column currentColumn; private MultiColumnPrinter(final Builder builder) { this.stream = builder.stream; this.columns = Collections.unmodifiableList(builder.columns); this.format = builder.format; this.columnSeparator = builder.columnSeparator; this.titleAlignment = builder.titleAlignment; this.lineLength = computeLineLength(); resetIterator(); computePrintableColumns(); } /** Prints a dashed line. */ public void printDashedLine() { startNewLineIfNeeded(); for (int i = 0; i < lineLength; i++) { stream.print('-'); } stream.println(); } /** * Formats and prints the provided text data. * Merge the provided text over the provided number of column. *

* Separator columns between merged columns will not be printed. * * @param data * The section title to print. * @param rowSpan * Specifies the number of rows a cell should span. */ public void printTitleSection(final String data, final int rowSpan) { consumeSeparatorColumn(); int lengthToPad = 0; int nbColumnMerged = 0; while (columnIterator.hasNext() && nbColumnMerged < rowSpan) { lengthToPad += currentColumn.width + columnSeparator.length(); if (!isSeparatorColumn(currentColumn)) { nbColumnMerged++; } currentColumn = columnIterator.next(); } stream.print(align(data, titleAlignment, lengthToPad)); consumeSeparatorColumn(); if (!columnIterator.hasNext()) { nextLine(); } } /** Prints a line with all column title and separator. */ public void printTitleLine() { startNewLineIfNeeded(); passFirstSeparatorColumn(); for (final Column column : this.printableColumns) { printCell(column.title, Alignment.RIGHT); } } /** * Prints the provided {@link Double} value on the current column. *

* If this {@link MultiColumnPrinter} is configured to apply formatting, * the provided value will be truncated according to the decimal * precision set in the corresponding {@link Column}. *
* See {@link MultiColumnPrinter#column(String, String, int, int)} for more details. * * @param value * The double value to print. */ public void printData(final Double value) { passFirstSeparatorColumn(); printData(value.isNaN() ? "-" : String.format(Locale.ENGLISH, "%." + currentColumn.doublePrecision + "f", value)); } /** * Prints the provided text data on the current column. * * @param data * The text data to print. */ public void printData(final String data) { passFirstSeparatorColumn(); printCell(data, Alignment.RIGHT); } /** * Returns the data {@link Column} list of this {@link MultiColumnPrinter}. *

* Separator columns are filtered out. * * @return The {@link Column} list of this {@link MultiColumnPrinter}. */ public List getColumns() { return printableColumns; } private void printCell(final String data, final Alignment alignment) { String toPrint = format ? align(data, alignment, currentColumn.width) : data; if (columnIterator.hasNext()) { toPrint += columnSeparator; } stream.print(toPrint); nextLineOnEOLOrNextColumn(); } /** Provided the provided string data according to the provided width and the provided alignment. */ private String align(final String data, final Alignment alignment, final int width) { final String rawData = data.trim(); final int padding = width - rawData.length(); if (padding <= 0) { return rawData; } switch (alignment) { case RIGHT: return pad(padding, rawData, 0); case LEFT: return pad(0, rawData, padding); case CENTER: final int paddingBefore = padding / 2; return pad(paddingBefore, rawData, padding - paddingBefore); default: return ""; } } private String pad(final int leftPad, final String s, final int rightPad) { return new StringBuilder().append(repeat(' ', leftPad)) .append(s) .append(repeat(' ', rightPad)) .toString(); } private void passFirstSeparatorColumn() { if (cursorOnLineStart()) { consumeSeparatorColumn(); } } private void consumeSeparatorColumn() { if (isSeparatorColumn(currentColumn)) { stream.print('|' + columnSeparator); nextLineOnEOLOrNextColumn(); } } private void startNewLineIfNeeded() { if (!cursorOnLineStart()) { nextLine(); } } private void nextLineOnEOLOrNextColumn() { if (columnIterator.hasNext()) { currentColumn = columnIterator.next(); consumeSeparatorColumn(); } else { nextLine(); } } private void nextLine() { stream.println(); resetIterator(); } private void resetIterator() { columnIterator = columns.iterator(); currentColumn = columnIterator.next(); } private boolean cursorOnLineStart() { return currentColumn == columns.get(0); } private boolean isSeparatorColumn(final Column column) { return column.id.startsWith(SEPARATOR_ID); } private void computePrintableColumns() { printableColumns = new ArrayList<>(columns); final Iterator it = printableColumns.iterator(); while (it.hasNext()) { if (isSeparatorColumn(it.next())) { it.remove(); } } } private int computeLineLength() { int lineLength = 0; final int separatorLength = this.columnSeparator.length(); for (final Column column : this.columns) { lineLength += column.width + separatorLength; } return lineLength - separatorLength; } }