/* * 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 * * * Copyright 2007-2008 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 final class Serializer extends TableSerializer { // The current column being output. private int column = 0; // 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>(); // Width of the table in columns. private int totalColumns = 0; // The padding to use for indenting the table. private final String indentPadding; // Private constructor. private Serializer() { // Compute the indentation padding. StringBuilder builder = new StringBuilder(); for (int i = 0; i < indentWidth; i++) { builder.append(' '); } this.indentPadding = builder.toString(); } /** * {@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(indentPadding); 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++) { if (headingSeparatorStartColumn > 0) { if (i < headingSeparatorStartColumn) { builder.append(' '); } else if (i == headingSeparatorStartColumn && j < padding) { builder.append(' '); } else { builder.append(headingSeparator); } } else { builder.append(headingSeparator); } } if ((i >= headingSeparatorStartColumn) && i < (totalColumns - 1)) { builder.append(columnSeparator); } } writer.println(builder.toString()); } } /** * {@inheritDoc} */ @Override public void endRow() { boolean isRemainingText; do { StringBuilder builder = new StringBuilder(indentPadding); 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) { endIndex = width; head = contents.substring(0, endIndex); tail = contents.substring(endIndex); } else { head = contents.substring(0, endIndex); tail = contents.substring(endIndex + 1); } } else { // The contents fits ok. head = contents; } // Add this cell's contents to the current line. 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. // Only if it's not the last cell (see issue #3210) if (i != currentRow.size() - 1) { for (int j = head.length(); j < width; j++) { builder.append(' '); } } // Update the row contents. currentRow.set(i, tail); if (tail != null) { isRemainingText = true; } } // Output the line. writer.println(builder.toString()); } 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 = indentWidth; 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 number of characters the table should be indented. private int indentWidth = 0; // The character which should be used to separate the table // heading row from the rows beneath. private char headingSeparator = DEFAULT_HEADING_SEPARATOR; // The column where the heading separator should begin. private int headingSeparatorStartColumn = 0; // 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 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 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 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 void setHeadingSeparator(char headingSeparator) { this.headingSeparator = headingSeparator; } /** * Sets the heading separator start column. The heading separator * will only be display in the specified column and all subsequent * columns. Usually this should be left at zero (the default) but * sometimes it useful to indent the heading separate in order to * provide additional emphasis (for example in menus). * * @param startColumn * The heading separator start column. */ public void setHeadingSeparatorStartColumn(int startColumn) { if (startColumn < 0) { throw new IllegalArgumentException("Negative start column " + startColumn); } this.headingSeparatorStartColumn = startColumn; } /** * Sets the amount of characters that the table should be indented. * By default the table is not indented. * * @param indentWidth * The number of characters the table should be indented. * @throws IllegalArgumentException * If indentWidth is less than 0. */ public void setIndentWidth(int indentWidth) throws IllegalArgumentException { if (indentWidth < 0) { throw new IllegalArgumentException("Negative indentation width " + indentWidth); } this.indentWidth = indentWidth; } /** * Sets the padding which will be used to separate a cell's contents * from its adjacent column separators. * * @param padding * The padding. */ public 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 void setTotalWidth(int totalWidth) { this.totalWidth = totalWidth; } /** * {@inheritDoc} */ @Override protected TableSerializer getSerializer() { return new Serializer(); } }