001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2007-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.util.table;
028    
029    
030    
031    import static org.opends.server.util.ServerConstants.*;
032    
033    import java.io.BufferedWriter;
034    import java.io.OutputStream;
035    import java.io.OutputStreamWriter;
036    import java.io.PrintWriter;
037    import java.io.Writer;
038    import java.util.ArrayList;
039    import java.util.HashMap;
040    import java.util.List;
041    import java.util.Map;
042    
043    
044    
045    /**
046     * An interface for creating a text based table. Tables have
047     * configurable column widths, padding, and column separators.
048     */
049    public final class TextTablePrinter extends TablePrinter {
050    
051      /**
052       * Table serializer implementation.
053       */
054      private final class Serializer extends TableSerializer {
055    
056        // The current column being output.
057        private int column = 0;
058    
059        // The real column widths taking into account size constraints but
060        // not including padding or separators.
061        private final List<Integer> columnWidths = new ArrayList<Integer>();
062    
063        // The cells in the current row.
064        private final List<String> currentRow = new ArrayList<String>();
065    
066        // Width of the table in columns.
067        private int totalColumns = 0;
068    
069        // The padding to use for indenting the table.
070        private final String indentPadding;
071    
072    
073    
074        // Private constructor.
075        private Serializer() {
076          // Compute the indentation padding.
077          StringBuilder builder = new StringBuilder();
078          for (int i = 0; i < indentWidth; i++) {
079            builder.append(' ');
080          }
081          this.indentPadding = builder.toString();
082        }
083    
084    
085    
086        /**
087         * {@inheritDoc}
088         */
089        @Override
090        public void addCell(String s) {
091          currentRow.add(s);
092          column++;
093        }
094    
095    
096    
097        /**
098         * {@inheritDoc}
099         */
100        @Override
101        public void addColumn(int width) {
102          columnWidths.add(width);
103          totalColumns++;
104        }
105    
106    
107    
108        /**
109         * {@inheritDoc}
110         */
111        @Override
112        public void addHeading(String s) {
113          if (displayHeadings) {
114            addCell(s);
115          }
116        }
117    
118    
119    
120        /**
121         * {@inheritDoc}
122         */
123        @Override
124        public void endHeader() {
125          if (displayHeadings) {
126            endRow();
127    
128            // Print the header separator.
129            StringBuilder builder = new StringBuilder(indentPadding);
130            for (int i = 0; i < totalColumns; i++) {
131              int width = columnWidths.get(i);
132              if (totalColumns > 1) {
133                if (i == 0 || i == (totalColumns - 1)) {
134                  // Only one lot of padding for first and last columns.
135                  width += padding;
136                } else {
137                  width += padding * 2;
138                }
139              }
140    
141              for (int j = 0; j < width; j++) {
142                if (headingSeparatorStartColumn > 0) {
143                  if (i < headingSeparatorStartColumn) {
144                    builder.append(' ');
145                  } else if (i == headingSeparatorStartColumn && j < padding) {
146                    builder.append(' ');
147                  } else {
148                    builder.append(headingSeparator);
149                  }
150                } else {
151                  builder.append(headingSeparator);
152                }
153              }
154    
155              if ((i >= headingSeparatorStartColumn) && i < (totalColumns - 1)) {
156                builder.append(columnSeparator);
157              }
158            }
159            writer.println(builder.toString());
160          }
161        }
162    
163    
164    
165        /**
166         * {@inheritDoc}
167         */
168        @Override
169        public void endRow() {
170          boolean isRemainingText;
171          do {
172            StringBuilder builder = new StringBuilder(indentPadding);
173            isRemainingText = false;
174            for (int i = 0; i < currentRow.size(); i++) {
175              int width = columnWidths.get(i);
176              String contents = currentRow.get(i);
177    
178              // Determine what parts of contents can be displayed on this
179              // line.
180              String head;
181              String tail = null;
182    
183              if (contents == null) {
184                // This cell has been displayed fully.
185                head = "";
186              } else if (contents.length() > width) {
187                // We're going to have to split the cell on next word
188                // boundary.
189                int endIndex = contents.lastIndexOf(' ', width);
190                if (endIndex == -1) {
191                  endIndex = width;
192                  head = contents.substring(0, endIndex);
193                  tail = contents.substring(endIndex);
194    
195                } else {
196                  head = contents.substring(0, endIndex);
197                  tail = contents.substring(endIndex + 1);
198                }
199              } else {
200                // The contents fits ok.
201                head = contents;
202              }
203    
204              // Add this cell's contents to the current line.
205              if (i > 0) {
206                // Add right padding for previous cell.
207                for (int j = 0; j < padding; j++) {
208                  builder.append(' ');
209                }
210    
211                // Add separator.
212                builder.append(columnSeparator);
213    
214                // Add left padding for this cell.
215                for (int j = 0; j < padding; j++) {
216                  builder.append(' ');
217                }
218              }
219    
220              // Add cell contents.
221              builder.append(head);
222    
223              // Now pad with extra space to make up the width.
224              // Only if it's not the last cell (see issue #3210)
225              if (i != currentRow.size() - 1)
226              {
227                for (int j = head.length(); j < width; j++)
228                {
229                  builder.append(' ');
230                }
231              }
232    
233              // Update the row contents.
234              currentRow.set(i, tail);
235              if (tail != null) {
236                isRemainingText = true;
237              }
238            }
239    
240            // Output the line.
241            writer.println(builder.toString());
242    
243          } while (isRemainingText);
244        }
245    
246    
247    
248        /**
249         * {@inheritDoc}
250         */
251        @Override
252        public void endTable() {
253          writer.flush();
254        }
255    
256    
257    
258        /**
259         * {@inheritDoc}
260         */
261        @Override
262        public void startHeader() {
263          determineColumnWidths();
264    
265          column = 0;
266          currentRow.clear();
267        }
268    
269    
270    
271        /**
272         * {@inheritDoc}
273         */
274        @Override
275        public void startRow() {
276          column = 0;
277          currentRow.clear();
278        }
279    
280    
281    
282        // We need to calculate the effective width of each column.
283        private void determineColumnWidths() {
284          // First calculate the minimum width so that we know how much
285          // expandable columns can expand.
286          int minWidth = indentWidth;
287          int expandableColumnSize = 0;
288    
289          for (int i = 0; i < totalColumns; i++) {
290            int actualSize = columnWidths.get(i);
291    
292            if (fixedColumns.containsKey(i)) {
293              int requestedSize = fixedColumns.get(i);
294    
295              if (requestedSize == 0) {
296                expandableColumnSize += actualSize;
297              } else {
298                columnWidths.set(i, requestedSize);
299                minWidth += requestedSize;
300              }
301            } else {
302              minWidth += actualSize;
303            }
304    
305            // Must also include padding and separators.
306            if (i > 0) {
307              minWidth += padding * 2 + columnSeparator.length();
308            }
309          }
310    
311          if (minWidth > totalWidth) {
312            // The table is too big: leave expandable columns at their
313            // requested width, as there's not much else that can be done.
314          } else {
315            int available = totalWidth - minWidth;
316    
317            if (expandableColumnSize > available) {
318              // Only modify column sizes if necessary.
319              for (int i = 0; i < totalColumns; i++) {
320                int actualSize = columnWidths.get(i);
321    
322                if (fixedColumns.containsKey(i)) {
323                  int requestedSize = fixedColumns.get(i);
324                  if (requestedSize == 0) {
325                    // Calculate size based on requested actual size as a
326                    // proportion of the total.
327                    requestedSize =
328                      ((actualSize * available) / expandableColumnSize);
329                    columnWidths.set(i, requestedSize);
330                  }
331                }
332              }
333            }
334          }
335        }
336      }
337    
338      /**
339       * The default string which should be used to separate one column
340       * from the next (not including padding).
341       */
342      private static final String DEFAULT_COLUMN_SEPARATOR = "";
343    
344      /**
345       * The default character which should be used to separate the table
346       * heading row from the rows beneath.
347       */
348      private static final char DEFAULT_HEADING_SEPARATOR = '-';
349    
350      /**
351       * The default padding which will be used to separate a cell's
352       * contents from its adjacent column separators.
353       */
354      private static final int DEFAULT_PADDING = 1;
355    
356      // The string which should be used to separate one column
357      // from the next (not including padding).
358      private String columnSeparator = DEFAULT_COLUMN_SEPARATOR;
359    
360      // Indicates whether or not the headings should be output.
361      private boolean displayHeadings = true;
362    
363      // Table indicating whether or not a column is fixed width.
364      private final Map<Integer, Integer> fixedColumns =
365        new HashMap<Integer, Integer>();
366    
367      // The number of characters the table should be indented.
368      private int indentWidth = 0;
369    
370      // The character which should be used to separate the table
371      // heading row from the rows beneath.
372      private char headingSeparator = DEFAULT_HEADING_SEPARATOR;
373    
374      // The column where the heading separator should begin.
375      private int headingSeparatorStartColumn = 0;
376    
377      // The padding which will be used to separate a cell's
378      // contents from its adjacent column separators.
379      private int padding = DEFAULT_PADDING;
380    
381      // Total permitted width for the table which expandable columns
382      // can use up.
383      private int totalWidth = MAX_LINE_WIDTH;
384    
385      // The output destination.
386      private PrintWriter writer = null;
387    
388    
389    
390      /**
391       * Creates a new text table printer for the specified output stream.
392       * The text table printer will have the following initial settings:
393       * <ul>
394       * <li>headings will be displayed
395       * <li>no separators between columns
396       * <li>columns are padded by one character
397       * </ul>
398       *
399       * @param stream
400       *          The stream to output tables to.
401       */
402      public TextTablePrinter(OutputStream stream) {
403        this(new BufferedWriter(new OutputStreamWriter(stream)));
404      }
405    
406    
407    
408      /**
409       * Creates a new text table printer for the specified writer. The
410       * text table printer will have the following initial settings:
411       * <ul>
412       * <li>headings will be displayed
413       * <li>no separators between columns
414       * <li>columns are padded by one character
415       * </ul>
416       *
417       * @param writer
418       *          The writer to output tables to.
419       */
420      public TextTablePrinter(Writer writer) {
421        this.writer = new PrintWriter(writer);
422      }
423    
424    
425    
426      /**
427       * Sets the column separator which should be used to separate one
428       * column from the next (not including padding).
429       *
430       * @param columnSeparator
431       *          The column separator.
432       */
433      public void setColumnSeparator(String columnSeparator) {
434        this.columnSeparator = columnSeparator;
435      }
436    
437    
438    
439      /**
440       * Set the maximum width for a column. If a cell is too big to fit
441       * in its column then it will be wrapped.
442       *
443       * @param column
444       *          The column to make fixed width (0 is the first column).
445       * @param width
446       *          The width of the column (this should not include column
447       *          separators or padding), or <code>0</code> to indicate
448       *          that this column should be expandable.
449       * @throws IllegalArgumentException
450       *           If column is less than 0.
451       */
452      public void setColumnWidth(int column, int width)
453          throws IllegalArgumentException {
454        if (column < 0) {
455          throw new IllegalArgumentException("Negative column " + column);
456        }
457    
458        if (width < 0) {
459          throw new IllegalArgumentException("Negative width " + width);
460        }
461    
462        fixedColumns.put(column, width);
463      }
464    
465    
466    
467      /**
468       * Specify whether the column headings should be displayed or not.
469       *
470       * @param displayHeadings
471       *          <code>true</code> if column headings should be
472       *          displayed.
473       */
474      public void setDisplayHeadings(boolean displayHeadings) {
475        this.displayHeadings = displayHeadings;
476      }
477    
478    
479    
480      /**
481       * Sets the heading separator which should be used to separate the
482       * table heading row from the rows beneath.
483       *
484       * @param headingSeparator
485       *          The heading separator.
486       */
487      public void setHeadingSeparator(char headingSeparator) {
488        this.headingSeparator = headingSeparator;
489      }
490    
491    
492    
493      /**
494       * Sets the heading separator start column. The heading separator
495       * will only be display in the specified column and all subsequent
496       * columns. Usually this should be left at zero (the default) but
497       * sometimes it useful to indent the heading separate in order to
498       * provide additional emphasis (for example in menus).
499       *
500       * @param startColumn
501       *          The heading separator start column.
502       */
503      public void setHeadingSeparatorStartColumn(int startColumn) {
504        if (startColumn < 0) {
505          throw new IllegalArgumentException("Negative start column "
506              + startColumn);
507        }
508        this.headingSeparatorStartColumn = startColumn;
509      }
510    
511    
512    
513      /**
514       * Sets the amount of characters that the table should be indented.
515       * By default the table is not indented.
516       *
517       * @param indentWidth
518       *          The number of characters the table should be indented.
519       * @throws IllegalArgumentException
520       *           If indentWidth is less than 0.
521       */
522      public void setIndentWidth(int indentWidth) throws IllegalArgumentException {
523        if (indentWidth < 0) {
524          throw new IllegalArgumentException("Negative indentation width "
525              + indentWidth);
526        }
527    
528        this.indentWidth = indentWidth;
529      }
530    
531    
532    
533      /**
534       * Sets the padding which will be used to separate a cell's contents
535       * from its adjacent column separators.
536       *
537       * @param padding
538       *          The padding.
539       */
540      public void setPadding(int padding) {
541        this.padding = padding;
542      }
543    
544    
545    
546      /**
547       * Sets the total permitted width for the table which expandable
548       * columns can use up.
549       *
550       * @param totalWidth
551       *          The total width.
552       */
553      public void setTotalWidth(int totalWidth) {
554        this.totalWidth = totalWidth;
555      }
556    
557    
558    
559      /**
560       * {@inheritDoc}
561       */
562      @Override
563      protected TableSerializer getSerializer() {
564        return new Serializer();
565      }
566    }