/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.io; import java.io.FilterWriter; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.StringTokenizer; import javax.swing.text.StyleConstants; import org.geotools.util.Utilities; import org.geotools.resources.XArray; /** * A character stream that can be used to format tables. Columns are separated * by tabulations (<code>'\t'</code>) and rows are separated by line terminators * (<code>'\r'</code>, <code>'\n'</code> or <code>"\r\n"</code>). Every table's * cells are stored in memory until {@link #flush()} is invoked. When invoked, * {@link #flush()} copy cell's contents to the underlying stream while replacing * tabulations by some amount of spaces. The exact number of spaces is computed * from cell's widths. {@code TableWriter} produces correct output when * displayed with a monospace font. * <br><br> * For example, the following code... * * <blockquote><pre> * TableWriter out = new TableWriter(new OutputStreamWriter(System.out), 3); * out.write("Prénom\tNom\n"); * out.nextLine('-'); * out.write("Idéphonse\tLaporte\nSarah\tCoursi\nYvan\tDubois"); * out.flush(); * </pre></blockquote> * * ...produces the following output: * * <blockquote><pre> * Prénom Nom * --------- ------- * Idéphonse Laporte * Sarah Coursi * Yvan Dubois * </pre></blockquote> * * @since 2.0 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class TableWriter extends FilterWriter { /** * A possible value for cell alignment. This specifies that the text is aligned * to the left indent and extra whitespace should be placed on the right. */ public static final int ALIGN_LEFT = StyleConstants.ALIGN_LEFT; /** * A possible value for cell alignment. This specifies that the text is aligned * to the right indent and extra whitespace should be placed on the left. */ public static final int ALIGN_RIGHT = StyleConstants.ALIGN_RIGHT; /** * A possible value for cell alignment. This specifies that the text is aligned * to the center and extra whitespace should be placed equally on the left and right. */ public static final int ALIGN_CENTER = StyleConstants.ALIGN_CENTER; /** * A column separator for {@linkplain #TableWriter(Writer,String) constructor}. * * @since 2.5 */ public static final String SINGLE_VERTICAL_LINE = " \u2502 "; /** * A column separator for {@linkplain #TableWriter(Writer,String) constructor}. * * @since 2.5 */ public static final String DOUBLE_VERTICAL_LINE = " \u2551 "; /** * A line separator for {@plain #nextLine(char)}. * * @since 2.5 */ public static final char SINGLE_HORIZONTAL_LINE = '\u2500'; /** * A line separator for {@plain #nextLine(char)}. * * @since 2.5 */ public static final char DOUBLE_HORIZONTAL_LINE = '\u2550'; /** * Drawing-box characters. The last two characters * are horizontal and vertical line respectively. */ private static final char[][] BOX = new char[][] { {// [0000]: single horizontal, single vertical '\u250C','\u252C','\u2510', '\u251C','\u253C','\u2524', '\u2514','\u2534','\u2518', '\u2500','\u2502' }, {// [0001]: single horizontal, double vertical '\u2553','\u2565','\u2556', '\u255F','\u256B','\u2562', '\u2559','\u2568','\u255C', '\u2500','\u2551' }, {// [0010]: double horizontal, single vertical '\u2552','\u2564','\u2555', '\u255E','\u256A','\u2561', '\u2558','\u2567','\u255B', '\u2550','\u2502' }, {// [0011]: double horizontal, double vertical '\u2554','\u2566','\u2557', '\u2560','\u256C','\u2563', '\u255A','\u2569','\u255D', '\u2550','\u2551' }, {// [0100]: ASCII characters only '+','+','+', '+','+','+', '+','+','+', '-','|' } }; /** * Default character for space. */ private static final char SPACE = ' '; /** * Temporary string buffer. This buffer contains only one cell's content. */ private final StringBuilder buffer = new StringBuilder(); /** * List of {@link Cell} objects, from left to right and top to bottom. * By convention, a {@code null} value or a {@link Cell} object * with <code>{@link Cell#text}==null</code> are move to the next line. */ private final List<Cell> cells = new ArrayList<Cell>(); /** * Alignment for current and next cells. */ private int alignment = ALIGN_LEFT; /** * Column position of the cell currently being written. The field * is incremented each time {@link #nextColumn()} is invoked. */ private int column; /** * Line position of the cell currently being written. The field * is incremented each time {@link #nextLine()} is invoked. */ private int row; /** * Maximum width for each columns. This array's length must * be equals to the number of columns in this table. */ private int width[] = new int[0]; /** * The column separator. */ private final String separator; /** * The left table border. */ private final String leftBorder; /** * The right table border. */ private final String rightBorder; /** * Tells if cells can span more than one line. If {@code true}, then EOL characters likes * {@code '\n'} move to the next line <em>inside</em> the current cell. If {@code false}, * then EOL characters move to the next table's row. Default value is {@code false}. */ private boolean multiLinesCells; /** * {@code true} if this {@code TableWriter} has been constructed with the no-arg constructor. */ private final boolean stringOnly; /** * Tells if the next '\n' character must be ignored. This field is * used in order to avoid writing two EOL in place of {@code "\r\n"}. */ private boolean skipCR; /** * Creates a new table writer with a default column separator. * <p> * <b>Note:</b> this writer may produces bad output on Windows console, unless the underlying * stream use the correct codepage (e.g. {@code OutputStreamWriter(System.out, "Cp437")}). * To display the appropriate codepage for a Windows NT console, type {@code chcp} on the * command line. * * @param out Writer object to provide the underlying stream, or {@code null} if there is no * underlying stream. If {@code out} is null, then the {@link #toString} method is the * only way to get the table's content. */ public TableWriter(final Writer out) { super(out!=null ? out : new StringWriter()); stringOnly = (out==null); leftBorder = "\u2551 "; rightBorder = " \u2551" ; separator = " \u2502 "; } /** * Creates a new table writer with the specified amount of spaces as column separator. * * @param out Writer object to provide the underlying stream, or {@code null} if there is no * underlying stream. If {@code out} is null, then the {@link #toString} method is the * only way to get the table's content. * @param spaces Amount of white spaces to use as column separator. */ public TableWriter(final Writer out, final int spaces) { this(out, Utilities.spaces(spaces)); } /** * Creates a new table writer with the specified column separator. * * @param out Writer object to provide the underlying stream, or {@code null} if there is no * underlying stream. If {@code out} is null, then the {@link #toString} method is the * only way to get the table's content. * @param separator String to write between columns. Drawing box characters are treated * specially. For example {@code " \\u2502 "} can be used for a single-line box. * * @see #SINGLE_VERTICAL_LINE * @see #DOUBLE_VERTICAL_LINE */ public TableWriter(final Writer out, final String separator) { super(out!=null ? out : new StringWriter()); stringOnly = (out == null); final int length = separator.length(); int lower = 0; int upper = length; while (lower<length && Character.isSpaceChar(separator.charAt(lower ))) lower++; while (upper>0 && Character.isSpaceChar(separator.charAt(upper-1))) upper--; this.leftBorder = separator.substring(lower); this.rightBorder = separator.substring(0, upper); this.separator = separator; } /** * Writes a border or a corner to the specified stream. * * @param out The destination stream. * @param horizontalBorder -1 for left border, +1 for right border, 0 for center. * @param verticalBorder -1 for top border, +1 for bottom border, 0 for center. * @param horizontalChar Character to use for horizontal line. * @throws IOException if the writting operation failed. */ private void writeBorder(final Writer out, final int horizontalBorder, final int verticalBorder, final char horizontalChar) throws IOException { /* * Obtient les ensembles de caractères qui * conviennent pour la ligne horizontale. */ int boxCount = 0; final char[][] box = new char[BOX.length][]; for (int i=0; i<BOX.length; i++) { if (BOX[i][9] == horizontalChar) { box[boxCount++] = BOX[i]; } } /* * Obtient une chaine contenant les lignes verticales à * dessiner à gauche, à droite ou au centre de la table. */ final String border; switch (horizontalBorder) { case -1: border = leftBorder; break; case +1: border = rightBorder; break; case 0: border = separator; break; default: throw new IllegalArgumentException(String.valueOf(horizontalBorder)); } if (verticalBorder<-1 || verticalBorder>+1) { throw new IllegalArgumentException(String.valueOf(verticalBorder)); } /* * Remplace les espaces par la ligne horizontale, * et les lignes verticales par une intersection. */ final int index = (horizontalBorder+1) + (verticalBorder+1)*3; final int borderLength = border.length(); for (int i=0; i<borderLength; i++) { char c=border.charAt(i); if (Character.isSpaceChar(c)) { c = horizontalChar; } else { for (int j=0; j<boxCount; j++) { if (box[j][10] == c) { c = box[j][index]; break; } } } out.write(c); } } /** * Sets the desired behavior for EOL and tabulations characters. * <ul> * <li>If {@code true}, EOL (<code>'\r'</code>, <code>'\n'</code> or * <code>"\r\n"</code>) and tabulations (<code>'\t'</code>) characters * are copied straight into the current cell, which mean that next write * operations will continue inside the same cell.</li> * <li>If {@code false}, then tabulations move to next column and EOL move * to the first cell of next row (i.e. tabulation and EOL are equivalent to * {@link #nextColumn()} and {@link #nextLine()} calls respectively).</li> * </ul> * The default value is {@code false}. * * @param multiLines {@code true} true if EOL are used for line feeds inside * current cells, or {@code false} if EOL move to the next row. */ public void setMultiLinesCells(final boolean multiLines) { synchronized (lock) { multiLinesCells = multiLines; } } /** * Tells if EOL characters are used for line feeds inside current cells. * * @return {@code true} if EOL characters are to be write inside the cell. */ public boolean isMultiLinesCells() { synchronized (lock) { return multiLinesCells; } } /** * Sets the alignment for all cells in the specified column. This method * overwrite the alignment for all previous cells in the specified column. * * @param column The 0-based column number. * @param alignment Cell alignment. Must be {@link #ALIGN_LEFT} * {@link #ALIGN_RIGHT} or {@link #ALIGN_CENTER}. */ public void setColumnAlignment(final int column, final int alignment) { if (alignment != ALIGN_LEFT && alignment != ALIGN_RIGHT && alignment != ALIGN_CENTER) { throw new IllegalArgumentException(String.valueOf(alignment)); } synchronized (lock) { int current = 0; for (final Cell cell : cells) { if (cell==null || cell.text==null) { current = 0; continue; } if (current == column) { cell.alignment = alignment; } current++; } } } /** * Sets the alignment for current and next cells. Change to the * alignment doesn't affect the alignment of previous cells and * previous rows. The default alignment is {@link #ALIGN_LEFT}. * * @param alignment Cell alignment. Must be {@link #ALIGN_LEFT} * {@link #ALIGN_RIGHT} or {@link #ALIGN_CENTER}. */ public void setAlignment(final int alignment) { if (alignment != ALIGN_LEFT && alignment != ALIGN_RIGHT && alignment != ALIGN_CENTER) { throw new IllegalArgumentException(String.valueOf(alignment)); } synchronized (lock) { this.alignment = alignment; } } /** * Returns the alignment for current and next cells. * * @return Cell alignment: {@link #ALIGN_LEFT} (the default), * {@link #ALIGN_RIGHT} or {@link #ALIGN_CENTER}. */ public int getAlignment() { synchronized (lock) { return alignment; } } /** * Returns the number of rows in this table. This count is reset to 0 by {@link #flush}. * * @return The number of rows in this table. * * @since 2.5 */ public int getRowCount() { int count = row; if (column != 0) { count++; } return count; } /** * Returns the number of columns in this table. * * @return The number of colunms in this table. * * @since 2.5 */ public int getColumnCount() { return width.length; } /** * Write a single character. If {@link #isMultiLinesCells()} * is {@code false} (which is the default), then: * <ul> * <li>Tabulations (<code>'\t'</code>) are replaced by {@link #nextColumn()} invocations.</li> * <li>Line separators (<code>'\r'</code>, <code>'\n'</code> or <code>"\r\n"</code>) * are replaced by {@link #nextLine()} invocations.</li> * </ul> * * @param c Character to write. */ @Override public void write(final int c) { synchronized (lock) { if (!multiLinesCells) { switch (c) { case '\t': { nextColumn(); skipCR = false; return; } case '\r': { nextLine(); skipCR = true; return; } case '\n': { if (!skipCR) { nextLine(); } skipCR = false; return; } } } if (c<Character.MIN_VALUE || c>Character.MAX_VALUE) { throw new IllegalArgumentException(String.valueOf(c)); } buffer.append((char)c); skipCR = false; } } /** * Writes a string. Tabulations and line separators are interpreted as by {@link #write(int)}. * * @param string String to write. */ @Override public void write(final String string) { write(string, 0, string.length()); } /** * Writes a portion of a string. Tabulations and line * separators are interpreted as by {@link #write(int)}. * * @param string String to write. * @param offset Offset from which to start writing characters. * @param length Number of characters to write. */ @Override public void write(final String string, int offset, int length) { if (offset<0 || length<0 || (offset+length)>string.length()) { throw new IndexOutOfBoundsException(); } if (length == 0) { return; } synchronized (lock) { if (skipCR && string.charAt(offset)=='\n') { offset++; length--; } if (!multiLinesCells) { int upper = offset; for (; length!=0; length--) { switch (string.charAt(upper++)) { case '\t': { buffer.append(string.substring(offset, upper-1)); nextColumn(); offset = upper; break; } case '\r': { buffer.append(string.substring(offset, upper-1)); nextLine(); if (length!=0 && string.charAt(upper)=='\n') { upper++; length--; } offset = upper; break; } case '\n': { buffer.append(string.substring(offset, upper-1)); nextLine(); offset = upper; break; } } } length = upper-offset; } skipCR = (string.charAt(offset+length-1) == '\r'); buffer.append(string.substring(offset, offset+length)); } } /** * Writes an array of characters. Tabulations and line * separators are interpreted as by {@link #write(int)}. * * @param cbuf Array of characters to be written. */ @Override public void write(final char cbuf[]) { write(cbuf, 0, cbuf.length); } /** * Writes a portion of an array of characters. Tabulations and * line separators are interpreted as by {@link #write(int)}. * * @param cbuf Array of characters. * @param offset Offset from which to start writing characters. * @param length Number of characters to write. */ @Override public void write(final char cbuf[], int offset, int length) { if (offset<0 || length<0 || (offset+length)>cbuf.length) { throw new IndexOutOfBoundsException(); } if (length == 0) { return; } synchronized (lock) { if (skipCR && cbuf[offset]=='\n') { offset++; length--; } if (!multiLinesCells) { int upper = offset; for (; length!=0; length--) { switch (cbuf[upper++]) { case '\t': { buffer.append(cbuf, offset, upper-offset-1); nextColumn(); offset = upper; break; } case '\r': { buffer.append(cbuf, offset, upper-offset-1); nextLine(); if (length!=0 && cbuf[upper]=='\n') { upper++; length--; } offset = upper; break; } case '\n': { buffer.append(cbuf, offset, upper-offset-1); nextLine(); offset = upper; break; } } } length = upper-offset; } skipCR = (cbuf[offset+length-1] == '\r'); buffer.append(cbuf, offset, length); } } /** * Writes an horizontal separator. */ public void writeHorizontalSeparator() { synchronized (lock) { if (column!=0 || buffer.length()!=0) { nextLine(); } nextLine(SINGLE_HORIZONTAL_LINE); } } /** * Moves one column to the right. Next write operations will occur in a new cell on the * same row. */ public void nextColumn() { nextColumn(SPACE); } /** * Moves one column to the right. Next write operations will occur in a new cell on the * same row. This method fill every remaining space in the current cell with the specified * character. For example calling {@code nextColumn('*')} from the first character of a cell * is a convenient way to put a pad value in this cell. * * @param fill Character filling the cell (default to whitespace). */ public void nextColumn(final char fill) { synchronized (lock) { final String cellText = buffer.toString(); cells.add(new Cell(cellText, alignment, fill)); if (column >= width.length) { width = XArray.resize(width, column+1); } int length = 0; final StringTokenizer tk = new StringTokenizer(cellText, "\r\n"); while (tk.hasMoreTokens()) { final int lg = tk.nextToken().length(); if (lg > length) { length = lg; } } if (length>width[column]) { width[column] = length; } column++; buffer.setLength(0); } } /** * Moves to the first column on the next row. * Next write operations will occur on a new row. */ public void nextLine() { nextLine(SPACE); } /** * Moves to the first column on the next row. Next write operations will occur on a new * row. This method fill every remaining cell in the current row with the specified character. * Calling {@code nextLine('-')} from the first column of a row is a convenient way to fill * this row with a line separator. * * @param fill Character filling the rest of the line (default to whitespace). * This caracter may be use as a row separator. * * @see #SINGLE_HORIZONTAL_LINE * @see #DOUBLE_HORIZONTAL_LINE */ public void nextLine(final char fill) { synchronized (lock) { if (buffer.length() != 0) { nextColumn(fill); } assert buffer.length() == 0; cells.add(!Character.isSpaceChar(fill) ? new Cell(null, alignment, fill) : null); column = 0; row++; } } /** * Flushs the table content to the underlying stream. This method should not be called * before the table is completed (otherwise, columns may have the wrong width). * * @throws IOException if an output operation failed. */ @Override public void flush() throws IOException { synchronized (lock) { if (buffer.length() != 0) { nextLine(); assert buffer.length() == 0; } flushTo(out); row = column = 0; cells.clear(); if (!(out instanceof TableWriter)) { /* * Flush only if this table is not included in an outer (bigger) table. * This is because flushing the outer table would break its formatting. */ out.flush(); } } } /** * Flushs the table content and close the underlying stream. * * @throws IOException if an output operation failed. */ @Override public void close() throws IOException { synchronized (lock) { flush(); out.close(); } } /** * Écrit vers le flot spécifié toutes les cellules qui avaient été disposées * dans le tableau. Ces cellules seront automatiquement alignées en colonnes. * Cette méthode peut être appelée plusieurs fois pour écrire le même tableau * par exemple vers plusieurs flots. * * @param out Flot vers où écrire les données. * @throws IOException si une erreur est survenue lors de l'écriture dans {@code out}. */ private void flushTo(final Writer out) throws IOException { final String columnSeparator = this.separator; final String lineSeparator = System.getProperty("line.separator", "\n"); final Cell[] currentLine = new Cell[width.length]; final int cellCount = cells.size(); for (int cellIndex=0; cellIndex<cellCount; cellIndex++) { /* * Copie dans {@code currentLine} toutes les données qui seront à écrire * sur la ligne courante de la table. Ces données excluent le {@code null} * terminal. La liste {@code currentLine} ne contiendra donc initialement * aucun élément nul, mais ses éléments seront progressivement modifiés (et mis * à {@code null}) pendant l'écriture de la ligne dans la boucle qui suit. */ Cell lineFill = null; int currentCount = 0; do { final Cell cell = cells.get(cellIndex); if (cell == null) { break; } if (cell.text == null) { lineFill = new Cell("", cell.alignment, cell.fill); break; } currentLine[currentCount++] = cell; } while (++cellIndex < cellCount); Arrays.fill(currentLine, currentCount, currentLine.length, lineFill); /* * La boucle suivante sera exécutée tant qu'il reste des lignes à écrire * (c'est-à-dire tant qu'au moins un élément de {@code currentLine} * est non-nul). Si une cellule contient un texte avec des caractères EOL, * alors cette cellule devra s'écrire sur plusieurs lignes dans la cellule * courante. */ while (!isEmpty(currentLine)) { for (int j=0; j<currentLine.length; j++) { final boolean isFirstColumn = (j == 0); final boolean isLastColumn = (j+1 == currentLine.length); final Cell cell = currentLine[j]; final int cellWidth = width[j]; if (cell == null) { if (isFirstColumn) { out.write(leftBorder); } repeat(out, SPACE, cellWidth); out.write(isLastColumn ? rightBorder : columnSeparator); continue; } String cellText = cell.toString(); int endCR = cellText.indexOf('\r'); int endLF = cellText.indexOf('\n'); int end = (endCR<0) ? endLF : (endLF<0) ? endCR : Math.min(endCR,endLF); if (end >= 0) { /* * Si un retour chariot a été trouvé, n'écrit que la première * ligne de la cellule. L'élément {@code currentLine[j]} * sera modifié pour ne contenir que les lignes restantes qui * seront écrites lors d'un prochain passage dans la boucle. */ int top = end+1; if (endCR>=0 && endCR+1==endLF) top++; int scan = top; final int textLength = cellText.length(); while (scan<textLength && Character.isWhitespace(cellText.charAt(scan))) { scan++; } currentLine[j] = (scan<textLength) ? cell.substring(top) : null; cellText = cellText.substring(0, end); } else currentLine[j] = null; final int textLength = cellText.length(); /* * Si la cellule à écrire est en fait une bordure, * on fera un traitement spécial pour utiliser les * caractˆres de jointures {@link #BOX}. */ if (currentCount == 0) { assert textLength == 0; final int verticalBorder; if (cellIndex==0) verticalBorder = -1; else if (cellIndex>=cellCount-1) verticalBorder = +1; else verticalBorder = 0; if (isFirstColumn) { writeBorder(out, -1, verticalBorder, cell.fill); } repeat(out, cell.fill, cellWidth); writeBorder(out, isLastColumn ? +1 : 0, verticalBorder, cell.fill); continue; } /* * Si la cellule n'est pas une bordure, il s'agit * d'une cellule "normale". Procˆde maintenant à * l'écriture d'une ligne de la cellule. */ if (isFirstColumn) { out.write(leftBorder); } final Writer tabExpander = (cellText.indexOf('\t')>=0) ? new ExpandedTabWriter(out) : out; switch (cell.alignment) { default: { // Should not happen. throw new AssertionError(cell.alignment); } case ALIGN_LEFT: { tabExpander.write(cellText); repeat(tabExpander, cell.fill, cellWidth-textLength); break; } case ALIGN_RIGHT: { repeat(tabExpander, cell.fill, cellWidth-textLength); tabExpander.write(cellText); break; } case ALIGN_CENTER: { final int rightMargin = (cellWidth-textLength)/2; repeat(tabExpander, cell.fill, rightMargin); tabExpander.write(cellText); repeat(tabExpander, cell.fill, (cellWidth-rightMargin)-textLength); break; } } out.write(isLastColumn ? rightBorder : columnSeparator); } out.write(lineSeparator); } } } /** * Checks if {@code array} contains only {@code null} elements. */ private static boolean isEmpty(final Object[] array) { for (int i=array.length; --i>=0;) { if (array[i] != null) { return false; } } return true; } /** * Repeats a character. * * @param out The destination stream. * @param car Character to write (usually ' '). * @param count Number of repetition. */ private static void repeat(final Writer out, final char car, int count) throws IOException { while (--count >= 0) { out.write(car); } } /** * Returns the table content as a string. */ @Override public String toString() { synchronized (lock) { int capacity = 2; // Room for EOL. for (int i=0; i<width.length; i++) { capacity += width[i]; } capacity *= getRowCount(); final StringWriter writer; if (stringOnly) { writer = (StringWriter) out; final StringBuffer buffer = writer.getBuffer(); buffer.setLength(0); buffer.ensureCapacity(capacity); } else { writer = new StringWriter(capacity); } try { flushTo(writer); } catch (IOException exception) { // Should not happen throw new AssertionError(exception); } return writer.toString(); } } /** * A class wrapping a cell's content and its text's alignment. * This class if for internal use only. * * @version $Id$ * @author Martin Desruisseaux (IRD) */ private static final class Cell { /** * The text to write inside the cell. */ public final String text; /** * The alignment for {@link #text} inside the cell. */ public int alignment; /** * The fill character, used for filling space inside the cell. */ public final char fill; /** * Returns a new cell wrapping the specified string with the * specified alignment and fill character. */ public Cell(final String text, final int alignment, final char fill) { this.text = text; this.alignment = alignment; this.fill = fill; } /** * Returns a new cell which contains substring of this cell. */ public Cell substring(final int lower) { return new Cell(text.substring(lower), alignment, fill); } /** * Returns the cell's content. */ @Override public String toString() { return text; } } }