mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

matthew_swift
22.59.2007 a88d0cab3caee99d585e9dd22823b9c4f80f796c
Add various classes which facilitate generating tables in text-based applications.

This includes a generic API for constructing the table (TableBuilder) and three "table printers":

* TextTablePrinter: a pretty human-readable table that supports configurable padding, column sizes, separators, etc

* CSVTablePrinter: a table printer that outputs in CSV format (useful for spreadsheet imports)

* TabSeparatedTablePrinter: a table printer which uses tabs as field delimiters (e.g. useful for unix cut command)
7 files added
1681 ■■■■■ changed files
opends/src/server/org/opends/server/util/table/CSVTablePrinter.java 253 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/table/TabSeparatedTablePrinter.java 220 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/table/TableBuilder.java 381 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/table/TablePrinter.java 56 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/table/TableSerializer.java 160 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/table/TextTablePrinter.java 531 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/table/package-info.java 80 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/table/CSVTablePrinter.java
New file
@@ -0,0 +1,253 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.util.table;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
/**
 * An interface for creating a CSV formatted table.
 */
public final class CSVTablePrinter extends TablePrinter {
  /**
   * Table serializer implementation.
   */
  private static final class Serializer extends TableSerializer {
    // The current column being output.
    private int column = 0;
    // Indicates whether or not the headings should be output.
    private final boolean displayHeadings;
    // Counts the number of separators that should be output the next
    // time a non-empty cell is displayed. The comma separators are
    // not displayed immediately so that we can avoid displaying
    // unnecessary trailing separators.
    private int requiredSeparators = 0;
    // The output destination.
    private final PrintWriter writer;
    // Private constructor.
    private Serializer(PrintWriter writer, boolean displayHeadings) {
      this.writer = writer;
      this.displayHeadings = displayHeadings;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void addCell(String s) {
      // Avoid printing comma separators for trailing empty cells.
      if (s.length() == 0) {
        requiredSeparators++;
      } else {
        for (int i = 0; i < requiredSeparators; i++) {
          writer.print(',');
        }
        requiredSeparators = 1;
      }
      boolean needsQuoting = false;
      if (s.contains(",")) {
        needsQuoting = true;
      }
      if (s.contains("\n")) {
        needsQuoting = true;
      }
      if (s.contains("\r")) {
        needsQuoting = true;
      }
      if (s.contains("\"")) {
        needsQuoting = true;
        s = s.replace("\"", "\"\"");
      }
      if (s.startsWith(" ")) {
        needsQuoting = true;
      }
      if (s.endsWith(" ")) {
        needsQuoting = true;
      }
      StringBuilder builder = new StringBuilder();
      if (needsQuoting) {
        builder.append("\"");
      }
      builder.append(s);
      if (needsQuoting) {
        builder.append("\"");
      }
      writer.print(builder.toString());
      column++;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void addHeading(String s) {
      if (displayHeadings) {
        addCell(s);
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endHeader() {
      if (displayHeadings) {
        writer.println();
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endRow() {
      writer.println();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endTable() {
      writer.flush();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void startHeader() {
      column = 0;
      requiredSeparators = 0;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void startRow() {
      column = 0;
      requiredSeparators = 0;
    }
  }
  // Indicates whether or not the headings should be output.
  private boolean displayHeadings = false;
  // The output destination.
  private PrintWriter writer = null;
  /**
   * Creates a new CSV table printer for the specified output stream.
   * Headings will not be displayed by default.
   *
   * @param stream
   *          The stream to output tables to.
   */
  public CSVTablePrinter(OutputStream stream) {
    this(new BufferedWriter(new OutputStreamWriter(stream)));
  }
  /**
   * Creates a new CSV table printer for the specified writer.
   * Headings will not be displayed by default.
   *
   * @param writer
   *          The writer to output tables to.
   */
  public CSVTablePrinter(Writer writer) {
    this.writer = new PrintWriter(writer);
  }
  /**
   * Specify whether or not table headings should be displayed.
   *
   * @param displayHeadings
   *          <code>true</code> if table headings should be
   *          displayed.
   */
  public void setDisplayHeadings(boolean displayHeadings) {
    this.displayHeadings = displayHeadings;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  protected TableSerializer getSerializer() {
    return new Serializer(writer, displayHeadings);
  }
}
opends/src/server/org/opends/server/util/table/TabSeparatedTablePrinter.java
New file
@@ -0,0 +1,220 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.util.table;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
/**
 * An interface for creating a tab-separated formatted table.
 * <p>
 * This table printer will replace any tab, line-feeds, or carriage
 * return control characters encountered in a cell with a single
 * space.
 */
public final class TabSeparatedTablePrinter extends TablePrinter {
  /**
   * Table serializer implementation.
   */
  private static final class Serializer extends TableSerializer {
    // The current column being output.
    private int column = 0;
    // Indicates whether or not the headings should be output.
    private final boolean displayHeadings;
    // Counts the number of separators that should be output the next
    // time a non-empty cell is displayed. The tab separators are
    // not displayed immediately so that we can avoid displaying
    // unnecessary trailing separators.
    private int requiredSeparators = 0;
    // The output destination.
    private final PrintWriter writer;
    // Private constructor.
    private Serializer(PrintWriter writer, boolean displayHeadings) {
      this.writer = writer;
      this.displayHeadings = displayHeadings;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void addCell(String s) {
      // Avoid printing tab separators for trailing empty cells.
      if (s.length() == 0) {
        requiredSeparators++;
      } else {
        for (int i = 0; i < requiredSeparators; i++) {
          writer.print('\t');
        }
        requiredSeparators = 1;
      }
      // Replace all new-lines and tabs with a single space.
      writer.print(s.replaceAll("[\\t\\n\\r]", " "));
      column++;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void addHeading(String s) {
      if (displayHeadings) {
        addCell(s);
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endHeader() {
      if (displayHeadings) {
        writer.println();
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endRow() {
      writer.println();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endTable() {
      writer.flush();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void startHeader() {
      column = 0;
      requiredSeparators = 0;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void startRow() {
      column = 0;
      requiredSeparators = 0;
    }
  }
  // Indicates whether or not the headings should be output.
  private boolean displayHeadings = false;
  // The output destination.
  private PrintWriter writer = null;
  /**
   * Creates a new tab separated table printer for the specified
   * output stream. Headings will not be displayed by default.
   *
   * @param stream
   *          The stream to output tables to.
   */
  public TabSeparatedTablePrinter(OutputStream stream) {
    this(new BufferedWriter(new OutputStreamWriter(stream)));
  }
  /**
   * Creates a new tab separated table printer for the specified
   * writer. Headings will not be displayed by default.
   *
   * @param writer
   *          The writer to output tables to.
   */
  public TabSeparatedTablePrinter(Writer writer) {
    this.writer = new PrintWriter(writer);
  }
  /**
   * Specify whether or not table headings should be displayed.
   *
   * @param displayHeadings
   *          <code>true</code> if table headings should be
   *          displayed.
   */
  public void setDisplayHeadings(boolean displayHeadings) {
    this.displayHeadings = displayHeadings;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  protected TableSerializer getSerializer() {
    return new Serializer(writer, displayHeadings);
  }
}
opends/src/server/org/opends/server/util/table/TableBuilder.java
New file
@@ -0,0 +1,381 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.util.table;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
 * A class which can be used to construct tables of information to be
 * displayed in a terminal. Once built the table can be output using a
 * {@link TableSerializer}.
 */
public final class TableBuilder {
  // The current column number in the current row where 0 represents
  // the left-most column in the table.
  private int column = 0;
  // The current with of each column.
  private List<Integer> columnWidths = new ArrayList<Integer>();
  // The list of column headings.
  private List<String> header = new ArrayList<String>();
  // The current number of rows in the table.
  private int height = 0;
  // The list of table rows.
  private List<List<String>> rows = new ArrayList<List<String>>();
  // The linked list of sort keys comparators.
  private List<Comparator<String>> sortComparators =
    new ArrayList<Comparator<String>>();
  // The linked list of sort keys.
  private List<Integer> sortKeys = new ArrayList<Integer>();
  // The current number of columns in the table.
  private int width = 0;
  /**
   * Creates a new table printer.
   */
  public TableBuilder() {
    // No implementation required.
  }
  /**
   * Adds a table sort key. The table will be sorted according to the
   * case-insensitive string ordering of the cells in the specified
   * column.
   *
   * @param column
   *          The column which will be used as a sort key.
   */
  public void addSortKey(int column) {
    addSortKey(column, String.CASE_INSENSITIVE_ORDER);
  }
  /**
   * Adds a table sort key. The table will be sorted according to the
   * provided string comparator.
   *
   * @param column
   *          The column which will be used as a sort key.
   * @param comparator
   *          The string comparator.
   */
  public void addSortKey(int column, Comparator<String> comparator) {
    sortKeys.add(column);
    sortComparators.add(comparator);
  }
  /**
   * Appends a new blank cell to the current row.
   */
  public void appendCell() {
    appendCell("");
  }
  /**
   * Appends a new cell to the current row containing the provided
   * boolean value.
   *
   * @param value
   *          The boolean value.
   */
  public void appendCell(boolean value) {
    appendCell(String.valueOf(value));
  }
  /**
   * Appends a new cell to the current row containing the provided
   * byte value.
   *
   * @param value
   *          The byte value.
   */
  public void appendCell(byte value) {
    appendCell(String.valueOf(value));
  }
  /**
   * Appends a new cell to the current row containing the provided
   * char value.
   *
   * @param value
   *          The char value.
   */
  public void appendCell(char value) {
    appendCell(String.valueOf(value));
  }
  /**
   * Appends a new cell to the current row containing the provided
   * double value.
   *
   * @param value
   *          The double value.
   */
  public void appendCell(double value) {
    appendCell(String.valueOf(value));
  }
  /**
   * Appends a new cell to the current row containing the provided
   * float value.
   *
   * @param value
   *          The float value.
   */
  public void appendCell(float value) {
    appendCell(String.valueOf(value));
  }
  /**
   * Appends a new cell to the current row containing the provided
   * integer value.
   *
   * @param value
   *          The boolean value.
   */
  public void appendCell(int value) {
    appendCell(String.valueOf(value));
  }
  /**
   * Appends a new cell to the current row containing the provided
   * long value.
   *
   * @param value
   *          The long value.
   */
  public void appendCell(long value) {
    appendCell(String.valueOf(value));
  }
  /**
   * Appends a new cell to the current row containing the provided
   * object value.
   *
   * @param value
   *          The object value.
   */
  public void appendCell(Object value) {
    // Make sure that the first row has been created.
    if (height == 0) {
      startRow();
    }
    // Create the cell.
    String s = String.valueOf(value);
    rows.get(height - 1).add(String.valueOf(value));
    column++;
    // Update statistics.
    if (column > width) {
      width = column;
      columnWidths.add(s.length());
    } else if (columnWidths.get(column - 1) < s.length()) {
      columnWidths.set(column - 1, s.length());
    }
  }
  /**
   * Appends a new blank column heading to the header row.
   */
  public void appendHeading() {
    appendHeading("");
  }
  /**
   * Appends a new column heading to the header row.
   *
   * @param value
   *          The column heading value.
   */
  public void appendHeading(String value) {
    header.add(value);
    // Update statistics.
    if (header.size() > width) {
      width = header.size();
      columnWidths.add(value.length());
    } else if (columnWidths.get(header.size() - 1) < value.length()) {
      columnWidths.set(header.size() - 1, value.length());
    }
  }
  /**
   * Gets the width of the current row.
   *
   * @return Returns the width of the current row.
   */
  public int getRowWidth() {
    return column;
  }
  /**
   * Gets the number of rows in table.
   *
   * @return Returns the number of rows in table.
   */
  public int getTableHeight() {
    return height;
  }
  /**
   * Gets the number of columns in table.
   *
   * @return Returns the number of columns in table.
   */
  public int getTableWidth() {
    return width;
  }
  /**
   * Prints the table in its current state using the provided table
   * printer.
   *
   * @param printer
   *          The table printer.
   */
  public void print(TablePrinter printer) {
    // Create a new printer instance.
    TableSerializer serializer = printer.getSerializer();
    // First sort the table.
    List<List<String>> sortedRows = new ArrayList<List<String>>(rows);
    Comparator<List<String>> comparator = new Comparator<List<String>>() {
      public int compare(List<String> row1, List<String> row2) {
        for (int i = 0; i < sortKeys.size(); i++) {
          String cell1 = row1.get(sortKeys.get(i));
          String cell2 = row2.get(sortKeys.get(i));
          int rc = sortComparators.get(i).compare(cell1, cell2);
          if (rc != 0) {
            return rc;
          }
        }
        // Both rows are equal.
        return 0;
      }
    };
    Collections.sort(sortedRows, comparator);
    // Now ouput the table.
    serializer.startTable(height, width);
    for (int i = 0; i < width; i++) {
      serializer.addColumn(columnWidths.get(i));
    }
    // Column headings.
    serializer.startHeader();
    for (String s : header) {
      serializer.addHeading(s);
    }
    serializer.endHeader();
    // Table contents.
    serializer.startContent();
    for (List<String> row : sortedRows) {
      serializer.startRow();
      // Print each cell in the row, padding missing trailing cells.
      for (int i = 0; i < width; i++) {
        if (i < row.size()) {
          serializer.addCell(row.get(i));
        } else {
          serializer.addCell("");
        }
      }
      serializer.endRow();
    }
    serializer.endContent();
    serializer.endTable();
  }
  /**
   * Appends a new row to the table.
   */
  public void startRow() {
    rows.add(new ArrayList<String>());
    height++;
    column = 0;
  }
}
opends/src/server/org/opends/server/util/table/TablePrinter.java
New file
@@ -0,0 +1,56 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.util.table;
/**
 * An interface for incrementally configuring a table serializer. Once
 * configured, the table printer can be used to create a new
 * {@link TableSerializer} instance using the {@link #getSerializer()}
 * method.
 */
public abstract class TablePrinter {
  /**
   * Creates a new abstract table printer.
   */
  protected TablePrinter() {
    // No implementation required.
  }
  /**
   * Creates a new table serializer based on the configuration of this
   * table printer.
   *
   * @return Returns a new table serializer based on the configuration
   *         of this table printer.
   */
  protected abstract TableSerializer getSerializer();
}
opends/src/server/org/opends/server/util/table/TableSerializer.java
New file
@@ -0,0 +1,160 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.util.table;
/**
 * An interface for serializing tables.
 * <p>
 * The default implementation for each method is to do nothing.
 * Implementations must override methods as required.
 */
public abstract class TableSerializer {
  /**
   * Create a new table serializer.
   */
  protected TableSerializer() {
    // No implementation required.
  }
  /**
   * Prints a table cell.
   *
   * @param s
   *          The cell contents.
   */
  public void addCell(String s) {
    // Default implementation.
  }
  /**
   * Defines a column in the table.
   *
   * @param width
   *          The width of the column in characters.
   */
  public void addColumn(int width) {
    // Default implementation.
  }
  /**
   * Prints a column heading.
   *
   * @param s
   *          The column heading.
   */
  public void addHeading(String s) {
    // Default implementation.
  }
  /**
   * Finish printing the table contents.
   */
  public void endContent() {
    // Default implementation.
  }
  /**
   * Finish printing the column headings.
   */
  public void endHeader() {
    // Default implementation.
  }
  /**
   * Finish printing the current row of the table.
   */
  public void endRow() {
    // Default implementation.
  }
  /**
   * Finish printing the table.
   */
  public void endTable() {
    // Default implementation.
  }
  /**
   * Prepare to start printing the table contents.
   */
  public void startContent() {
    // Default implementation.
  }
  /**
   * Prepare to start printing the column headings.
   */
  public void startHeader() {
    // Default implementation.
  }
  /**
   * Prepare to start printing a new row of the table.
   */
  public void startRow() {
    // Default implementation.
  }
  /**
   * Start a new table having the specified number of rows and
   * columns.
   *
   * @param height
   *          The number of rows in the table.
   * @param width
   *          The number of columns in the table.
   */
  public void startTable(int height, int width) {
    // Default implementation.
  }
}
opends/src/server/org/opends/server/util/table/TextTablePrinter.java
New file
@@ -0,0 +1,531 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.util.table;
import static org.opends.server.util.ServerConstants.*;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * An interface for creating a text based table. Tables have
 * configurable column widths, padding, and column separators.
 */
public final class TextTablePrinter extends TablePrinter {
  /**
   * Table serializer implementation.
   */
  private static final class Serializer extends TableSerializer {
    // The current column being output.
    private int column = 0;
    // The string which should be used to separate one column
    // from the next (not including padding).
    private final String columnSeparator;
    // The real column widths taking into account size constraints but
    // not including padding or separators.
    private final List<Integer> columnWidths = new ArrayList<Integer>();
    // The cells in the current row.
    private final List<String> currentRow = new ArrayList<String>();
    // Indicates whether or not the headings should be output.
    private final boolean displayHeadings;
    // Table indicating whether or not a column is fixed width.
    private final Map<Integer, Integer> fixedColumns;
    // The character which should be used to separate the table
    // heading row from the rows beneath.
    private final char headingSeparator;
    // The padding which will be used to separate a cell's
    // contents from its adjacent column separators.
    private final int padding;
    // Width of the table in columns.
    private int totalColumns = 0;
    // Total permitted width for the table which expandable columns
    // can use up.
    private final int totalWidth;
    // The output writer.
    private final PrintWriter writer;
    // Private constructor.
    private Serializer(PrintWriter writer, String columnSeparator,
        Map<Integer, Integer> fixedColumns, boolean displayHeadings,
        char headingSeparator, int padding, int totalWidth) {
      this.writer = writer;
      this.columnSeparator = columnSeparator;
      this.fixedColumns = new HashMap<Integer, Integer>(fixedColumns);
      this.displayHeadings = displayHeadings;
      this.headingSeparator = headingSeparator;
      this.padding = padding;
      this.totalWidth = totalWidth;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void addCell(String s) {
      currentRow.add(s);
      column++;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void addColumn(int width) {
      columnWidths.add(width);
      totalColumns++;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void addHeading(String s) {
      if (displayHeadings) {
        addCell(s);
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endHeader() {
      if (displayHeadings) {
        endRow();
        // Print the header separator.
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < totalColumns; i++) {
          int width = columnWidths.get(i);
          if (totalColumns > 1) {
            if (i == 0 || i == (totalColumns - 1)) {
              // Only one lot of padding for first and last columns.
              width += padding;
            } else {
              width += padding * 2;
            }
          }
          for (int j = 0; j < width; j++) {
            builder.append(headingSeparator);
          }
          if (i < (totalColumns - 1)) {
            builder.append(columnSeparator);
          }
        }
        writer.println(builder.toString());
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endRow() {
      boolean isRemainingText;
      do {
        isRemainingText = false;
        for (int i = 0; i < currentRow.size(); i++) {
          int width = columnWidths.get(i);
          String contents = currentRow.get(i);
          // Determine what parts of contents can be displayed on this
          // line.
          String head;
          String tail = null;
          if (contents == null) {
            // This cell has been displayed fully.
            head = "";
          } else if (contents.length() > width) {
            // We're going to have to split the cell on next word
            // boundary.
            int endIndex = contents.lastIndexOf(' ', width);
            if (endIndex == -1) {
              // Problem - we have a word which is too big to fit in
              // the
              // cell.
              head = contents.substring(0, width);
              tail = contents.substring(width);
            } else {
              head = contents.substring(0, endIndex);
              tail = contents.substring(endIndex + 1);
            }
          } else {
            // The contents fits ok.
            head = contents;
          }
          // Display this line.
          StringBuilder builder = new StringBuilder();
          if (i > 0) {
            // Add right padding for previous cell.
            for (int j = 0; j < padding; j++) {
              builder.append(' ');
            }
            // Add separator.
            builder.append(columnSeparator);
            // Add left padding for this cell.
            for (int j = 0; j < padding; j++) {
              builder.append(' ');
            }
          }
          // Add cell contents.
          builder.append(head);
          // Now pad with extra space to make up the width.
          for (int j = head.length(); j < width; j++) {
            builder.append(' ');
          }
          writer.print(builder.toString());
          // Update the row contents.
          currentRow.set(i, tail);
          if (tail != null) {
            isRemainingText = true;
          }
        }
        writer.println();
      } while (isRemainingText);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void endTable() {
      writer.flush();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void startHeader() {
      determineColumnWidths();
      column = 0;
      currentRow.clear();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void startRow() {
      column = 0;
      currentRow.clear();
    }
    // We need to calculate the effective width of each column.
    private void determineColumnWidths() {
      // First calculate the minimum width so that we know how much
      // expandable columns can expand.
      int minWidth = 0;
      int expandableColumnSize = 0;
      for (int i = 0; i < totalColumns; i++) {
        int actualSize = columnWidths.get(i);
        if (fixedColumns.containsKey(i)) {
          int requestedSize = fixedColumns.get(i);
          if (requestedSize == 0) {
            expandableColumnSize += actualSize;
          } else {
            columnWidths.set(i, requestedSize);
            minWidth += requestedSize;
          }
        } else {
          minWidth += actualSize;
        }
        // Must also include padding and separators.
        if (i > 0) {
          minWidth += padding * 2 + columnSeparator.length();
        }
      }
      if (minWidth > totalWidth) {
        // The table is too big: leave expandable columns at their
        // requested width, as there's not much else that can be done.
      } else {
        int available = totalWidth - minWidth;
        if (expandableColumnSize > available) {
          // Only modify column sizes if necessary.
          for (int i = 0; i < totalColumns; i++) {
            int actualSize = columnWidths.get(i);
            if (fixedColumns.containsKey(i)) {
              int requestedSize = fixedColumns.get(i);
              if (requestedSize == 0) {
                // Calculate size based on requested actual size as a
                // proportion of the total.
                requestedSize =
                  ((actualSize * available) / expandableColumnSize);
                columnWidths.set(i, requestedSize);
              }
            }
          }
        }
      }
    }
  }
  /**
   * The default string which should be used to separate one column
   * from the next (not including padding).
   */
  private static final String DEFAULT_COLUMN_SEPARATOR = "";
  /**
   * The default character which should be used to separate the table
   * heading row from the rows beneath.
   */
  private static final char DEFAULT_HEADING_SEPARATOR = '-';
  /**
   * The default padding which will be used to separate a cell's
   * contents from its adjacent column separators.
   */
  private static final int DEFAULT_PADDING = 1;
  // The string which should be used to separate one column
  // from the next (not including padding).
  private String columnSeparator = DEFAULT_COLUMN_SEPARATOR;
  // Indicates whether or not the headings should be output.
  private boolean displayHeadings = true;
  // Table indicating whether or not a column is fixed width.
  private final Map<Integer, Integer> fixedColumns =
    new HashMap<Integer, Integer>();
  // The character which should be used to separate the table
  // heading row from the rows beneath.
  private char headingSeparator = DEFAULT_HEADING_SEPARATOR;
  // The padding which will be used to separate a cell's
  // contents from its adjacent column separators.
  private int padding = DEFAULT_PADDING;
  // Total permitted width for the table which expandable columns
  // can use up.
  private int totalWidth = MAX_LINE_WIDTH;
  // The output destination.
  private PrintWriter writer = null;
  /**
   * Creates a new text table printer for the specified output stream.
   * The text table printer will have the following initial settings:
   * <ul>
   * <li>headings will be displayed
   * <li>no separators between columns
   * <li>columns are padded by one character
   * </ul>
   *
   * @param stream
   *          The stream to output tables to.
   */
  public TextTablePrinter(OutputStream stream) {
    this(new BufferedWriter(new OutputStreamWriter(stream)));
  }
  /**
   * Creates a new text table printer for the specified writer. The
   * text table printer will have the following initial settings:
   * <ul>
   * <li>headings will be displayed
   * <li>no separators between columns
   * <li>columns are padded by one character
   * </ul>
   *
   * @param writer
   *          The writer to output tables to.
   */
  public TextTablePrinter(Writer writer) {
    this.writer = new PrintWriter(writer);
  }
  /**
   * Sets the column separator which should be used to separate one
   * column from the next (not including padding).
   *
   * @param columnSeparator
   *          The column separator.
   */
  public final void setColumnSeparator(String columnSeparator) {
    this.columnSeparator = columnSeparator;
  }
  /**
   * Set the maximum width for a column. If a cell is too big to fit
   * in its column then it will be wrapped.
   *
   * @param column
   *          The column to make fixed width (0 is the first column).
   * @param width
   *          The width of the column (this should not include column
   *          separators or padding), or <code>0</code> to indicate
   *          that this column should be expandable.
   * @throws IllegalArgumentException
   *           If column is less than 0 .
   */
  public final void setColumnWidth(int column, int width)
      throws IllegalArgumentException {
    if (column < 0) {
      throw new IllegalArgumentException("Negative column " + column);
    }
    if (width < 0) {
      throw new IllegalArgumentException("Negative width " + width);
    }
    fixedColumns.put(column, width);
  }
  /**
   * Specify whether the column headings should be displayed or not.
   *
   * @param displayHeadings
   *          <code>true</code> if column headings should be
   *          displayed.
   */
  public final void setDisplayHeadings(boolean displayHeadings) {
    this.displayHeadings = displayHeadings;
  }
  /**
   * Sets the heading separator which should be used to separate the
   * table heading row from the rows beneath.
   *
   * @param headingSeparator
   *          The heading separator.
   */
  public final void setHeadingSeparator(char headingSeparator) {
    this.headingSeparator = headingSeparator;
  }
  /**
   * Sets the padding which will be used to separate a cell's contents
   * from its adjacent column separators.
   *
   * @param padding
   *          The padding.
   */
  public final void setPadding(int padding) {
    this.padding = padding;
  }
  /**
   * Sets the total permitted width for the table which expandable
   * columns can use up.
   *
   * @param totalWidth
   *          The total width.
   */
  public final void setTotalWidth(int totalWidth) {
    this.totalWidth = totalWidth;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  protected TableSerializer getSerializer() {
    return new Serializer(writer, columnSeparator, fixedColumns,
        displayHeadings, headingSeparator, padding, totalWidth);
  }
}
opends/src/server/org/opends/server/util/table/package-info.java
New file
@@ -0,0 +1,80 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2006-2007 Sun Microsystems, Inc.
 */
/**
 * Provides support for construction and display of tables in text based
 * applications. Applications construct tables using the {@link TableBuilder}
 * class and display them using on of the {@link TablePrinter}
 * implementations. At the moment two types of table output are supported:
 * <ul>
 * <li>{@link CSVTablePrinter} - displays a table in comma-separated
 *     value format
 * <li>{@link TabSeparatedTablePrinter} - displays a table in tab separated
 *     format
 * <li>{@link TextTablePrinter} - displays a table in a human-readable
 *     format. Using this implementation it is possible to configure
 *     constraints on column widths. The implementation will take care of
 *     wrapping table cells where required.
 * </ul>
 * The following code illustrates the construction of a text-based table:
 * <pre>
 * TableBuilder builder = new TableBuilder();
 *
 * builder.appendHeading("Name");
 * builder.appendHeading("Age");
 * builder.addSortKey(0);
 *
 * builder.startRow();
 * builder.appendCell("Bob");
 * builder.appendCell(11);
 *
 * builder.startRow();
 * builder.appendCell("Alice");
 * builder.appendCell(22);
 *
 * builder.startRow();
 * builder.appendCell("Charlie");
 * builder.appendCell(33);
 *
 * TextTablePrinter printer = new TextTablePrinter(System.out);
 * printer.setColumnSeparator(":");
 * builder.print(printer);
 * </pre>
 *
 * Which will display the following table:
 * <pre>
 * Name    : Age
 * --------:----
 * Alice   : 22
 * Bob     : 11
 * Charlie : 33
 * </pre>
 */
package org.opends.server.util.table;