001/*
002 * Copyright 2009-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2009-2014 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.util;
022
023
024
025import java.io.Serializable;
026import java.text.DecimalFormat;
027import java.text.DecimalFormatSymbols;
028import java.text.SimpleDateFormat;
029import java.util.Date;
030
031import static com.unboundid.util.UtilityMessages.*;
032
033
034
035/**
036 * This class provides a utility for formatting output in multiple columns.
037 * Each column will have a defined width and alignment.  It can alternately
038 * generate output as tab-delimited text or comma-separated values (CSV).
039 */
040@NotMutable()
041@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
042public final class ColumnFormatter
043       implements Serializable
044{
045  /**
046   * The symbols to use for special characters that might be encountered when
047   * using a decimal formatter.
048   */
049  private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS =
050       new DecimalFormatSymbols();
051  static
052  {
053    DECIMAL_FORMAT_SYMBOLS.setInfinity("inf");
054    DECIMAL_FORMAT_SYMBOLS.setNaN("NaN");
055  }
056
057
058
059  /**
060   * The default output format to use.
061   */
062  private static final OutputFormat DEFAULT_OUTPUT_FORMAT =
063       OutputFormat.COLUMNS;
064
065
066
067  /**
068   * The default spacer to use between columns.
069   */
070  private static final String DEFAULT_SPACER = " ";
071
072
073
074  /**
075   * The default date format string that will be used for timestamps.
076   */
077  private static final String DEFAULT_TIMESTAMP_FORMAT = "HH:mm:ss";
078
079
080
081  /**
082   * The serial version UID for this serializable class.
083   */
084  private static final long serialVersionUID = -2524398424293401200L;
085
086
087
088  // Indicates whether to insert a timestamp before the first column.
089  private final boolean includeTimestamp;
090
091  // The column to use for the timestamp.
092  private final FormattableColumn timestampColumn;
093
094  // The columns to be formatted.
095  private final FormattableColumn[] columns;
096
097  // The output format to use.
098  private final OutputFormat outputFormat;
099
100  // The string to insert between columns.
101  private final String spacer;
102
103  // The format string to use for the timestamp.
104  private final String timestampFormat;
105
106  // The thread-local formatter to use for floating-point values.
107  private final transient ThreadLocal<DecimalFormat> decimalFormatter;
108
109  // The thread-local formatter to use when formatting timestamps.
110  private final transient ThreadLocal<SimpleDateFormat> timestampFormatter;
111
112
113
114  /**
115   * Creates a column formatter that will format the provided columns with the
116   * default settings.
117   *
118   * @param  columns  The columns to be formatted.  At least one column must be
119   *                  provided.
120   */
121  public ColumnFormatter(final FormattableColumn... columns)
122  {
123    this(false, null, null, null, columns);
124  }
125
126
127
128  /**
129   * Creates a column formatter that will format the provided columns.
130   *
131   * @param  includeTimestamp  Indicates whether to insert a timestamp before
132   *                           the first column when generating data lines
133   * @param  timestampFormat   The format string to use for the timestamp.  It
134   *                           may be {@code null} if no timestamp should be
135   *                           included or the default format should be used.
136   *                           If a format is provided, then it should be one
137   *                           that will always generate timestamps with a
138   *                           constant width.
139   * @param  outputFormat      The output format to use.
140   * @param  spacer            The spacer to use between columns.  It may be
141   *                           {@code null} if the default spacer should be
142   *                           used.  This will only apply for an output format
143   *                           of {@code COLUMNS}.
144   * @param  columns           The columns to be formatted.  At least one
145   *                           column must be provided.
146   */
147  public ColumnFormatter(final boolean includeTimestamp,
148                         final String timestampFormat,
149                         final OutputFormat outputFormat, final String spacer,
150                         final FormattableColumn... columns)
151  {
152    Validator.ensureNotNull(columns);
153    Validator.ensureTrue(columns.length > 0);
154
155    this.includeTimestamp = includeTimestamp;
156    this.columns          = columns;
157
158    decimalFormatter   = new ThreadLocal<DecimalFormat>();
159    timestampFormatter = new ThreadLocal<SimpleDateFormat>();
160
161    if (timestampFormat == null)
162    {
163      this.timestampFormat = DEFAULT_TIMESTAMP_FORMAT;
164    }
165    else
166    {
167      this.timestampFormat = timestampFormat;
168    }
169
170    if (outputFormat == null)
171    {
172      this.outputFormat = DEFAULT_OUTPUT_FORMAT;
173    }
174    else
175    {
176      this.outputFormat = outputFormat;
177    }
178
179    if (spacer == null)
180    {
181      this.spacer = DEFAULT_SPACER;
182    }
183    else
184    {
185      this.spacer = spacer;
186    }
187
188    if (includeTimestamp)
189    {
190      final SimpleDateFormat dateFormat =
191           new SimpleDateFormat(this.timestampFormat);
192      final String timestamp = dateFormat.format(new Date());
193      final String label = INFO_COLUMN_LABEL_TIMESTAMP.get();
194      final int width = Math.max(label.length(), timestamp.length());
195
196      timestampFormatter.set(dateFormat);
197      timestampColumn =
198           new FormattableColumn(width, HorizontalAlignment.LEFT, label);
199    }
200    else
201    {
202      timestampColumn = null;
203    }
204  }
205
206
207
208  /**
209   * Indicates whether timestamps will be included in the output.
210   *
211   * @return  {@code true} if timestamps should be included, or {@code false}
212   *          if not.
213   */
214  public boolean includeTimestamps()
215  {
216    return includeTimestamp;
217  }
218
219
220
221  /**
222   * Retrieves the format string that will be used for generating timestamps.
223   *
224   * @return  The format string that will be used for generating timestamps.
225   */
226  public String getTimestampFormatString()
227  {
228    return timestampFormat;
229  }
230
231
232
233  /**
234   * Retrieves the output format that will be used.
235   *
236   * @return  The output format for this formatter.
237   */
238  public OutputFormat getOutputFormat()
239  {
240    return outputFormat;
241  }
242
243
244
245  /**
246   * Retrieves the spacer that will be used between columns.
247   *
248   * @return  The spacer that will be used between columns.
249   */
250  public String getSpacer()
251  {
252    return spacer;
253  }
254
255
256
257  /**
258   * Retrieves the set of columns for this formatter.
259   *
260   * @return  The set of columns for this formatter.
261   */
262  public FormattableColumn[] getColumns()
263  {
264    final FormattableColumn[] copy = new FormattableColumn[columns.length];
265    System.arraycopy(columns, 0, copy, 0, columns.length);
266    return copy;
267  }
268
269
270
271  /**
272   * Obtains the lines that should comprise the column headers.
273   *
274   * @param  includeDashes  Indicates whether to include a row of dashes below
275   *                        the headers if appropriate for the output format.
276   *
277   * @return  The lines that should comprise the column headers.
278   */
279  public String[] getHeaderLines(final boolean includeDashes)
280  {
281    if (outputFormat == OutputFormat.COLUMNS)
282    {
283      int maxColumns = 1;
284      final String[][] headerLines = new String[columns.length][];
285      for (int i=0; i < columns.length; i++)
286      {
287        headerLines[i] = columns[i].getLabelLines();
288        maxColumns = Math.max(maxColumns, headerLines[i].length);
289      }
290
291      final StringBuilder[] buffers = new StringBuilder[maxColumns];
292      for (int i=0; i < maxColumns; i++)
293      {
294        final StringBuilder buffer = new StringBuilder();
295        buffers[i] = buffer;
296        if (includeTimestamp)
297        {
298          if (i == (maxColumns - 1))
299          {
300            timestampColumn.format(buffer, timestampColumn.getSingleLabelLine(),
301                 outputFormat);
302          }
303          else
304          {
305            timestampColumn.format(buffer, "", outputFormat);
306          }
307        }
308
309        for (int j=0; j < columns.length; j++)
310        {
311          if (includeTimestamp || (j > 0))
312          {
313            buffer.append(spacer);
314          }
315
316          final int rowNumber = i + headerLines[j].length - maxColumns;
317          if (rowNumber < 0)
318          {
319            columns[j].format(buffer, "", outputFormat);
320          }
321          else
322          {
323            columns[j].format(buffer, headerLines[j][rowNumber], outputFormat);
324          }
325        }
326      }
327
328      final String[] returnArray;
329      if (includeDashes)
330      {
331        returnArray = new String[maxColumns+1];
332      }
333      else
334      {
335        returnArray = new String[maxColumns];
336      }
337
338      for (int i=0; i < maxColumns; i++)
339      {
340        returnArray[i] = buffers[i].toString();
341      }
342
343      if (includeDashes)
344      {
345        final StringBuilder buffer = new StringBuilder();
346        if (timestampColumn != null)
347        {
348          for (int i=0; i < timestampColumn.getWidth(); i++)
349          {
350            buffer.append('-');
351          }
352        }
353
354        for (int i=0; i < columns.length; i++)
355        {
356          if (includeTimestamp || (i > 0))
357          {
358            buffer.append(spacer);
359          }
360
361          for (int j=0; j < columns[i].getWidth(); j++)
362          {
363            buffer.append('-');
364          }
365        }
366
367        returnArray[returnArray.length - 1] = buffer.toString();
368      }
369
370      return returnArray;
371    }
372    else
373    {
374      final StringBuilder buffer = new StringBuilder();
375      if (timestampColumn != null)
376      {
377        timestampColumn.format(buffer, timestampColumn.getSingleLabelLine(),
378             outputFormat);
379      }
380
381      for (int i=0; i < columns.length; i++)
382      {
383        if (includeTimestamp || (i > 0))
384        {
385          if (outputFormat == OutputFormat.TAB_DELIMITED_TEXT)
386          {
387            buffer.append('\t');
388          }
389          else if (outputFormat == OutputFormat.CSV)
390          {
391            buffer.append(',');
392          }
393        }
394
395        final FormattableColumn c = columns[i];
396        c.format(buffer, c.getSingleLabelLine(), outputFormat);
397      }
398
399      return new String[] { buffer.toString() };
400    }
401  }
402
403
404
405  /**
406   * Formats a row of data.  The provided data must correspond to the columns
407   * used when creating this formatter.
408   *
409   * @param  columnData  The elements to include in each row of the data.
410   *
411   * @return  A string containing the formatted row.
412   */
413  public String formatRow(final Object... columnData)
414  {
415    final StringBuilder buffer = new StringBuilder();
416
417    if (includeTimestamp)
418    {
419      SimpleDateFormat dateFormat = timestampFormatter.get();
420      if (dateFormat == null)
421      {
422        dateFormat = new SimpleDateFormat(timestampFormat);
423        timestampFormatter.set(dateFormat);
424      }
425
426      timestampColumn.format(buffer, dateFormat.format(new Date()),
427           outputFormat);
428    }
429
430    for (int i=0; i < columns.length; i++)
431    {
432      if (includeTimestamp || (i > 0))
433      {
434        switch (outputFormat)
435        {
436          case TAB_DELIMITED_TEXT:
437            buffer.append('\t');
438            break;
439          case CSV:
440            buffer.append(',');
441            break;
442          case COLUMNS:
443            buffer.append(spacer);
444            break;
445        }
446      }
447
448      if (i >= columnData.length)
449      {
450        columns[i].format(buffer, "", outputFormat);
451      }
452      else
453      {
454        columns[i].format(buffer, toString(columnData[i]), outputFormat);
455      }
456    }
457
458    return buffer.toString();
459  }
460
461
462
463  /**
464   * Retrieves a string representation of the provided object.  If the object
465   * is {@code null}, then the empty string will be returned.  If the object is
466   * a {@code Float} or {@code Double}, then it will be formatted using a
467   * DecimalFormat with a format string of "0.000".  Otherwise, the
468   * {@code String.valueOf} method will be used to obtain the string
469   * representation.
470   *
471   * @param  o  The object for which to retrieve the string representation.
472   *
473   * @return  A string representation of the provided object.
474   */
475  private String toString(final Object o)
476  {
477    if (o == null)
478    {
479      return "";
480    }
481
482    if ((o instanceof Float) || (o instanceof Double))
483    {
484      DecimalFormat f = decimalFormatter.get();
485      if (f == null)
486      {
487        f = new DecimalFormat("0.000", DECIMAL_FORMAT_SYMBOLS);
488        decimalFormatter.set(f);
489      }
490
491      final double d;
492      if (o instanceof Float)
493      {
494        d = ((Float) o).doubleValue();
495      }
496      else
497      {
498        d = ((Double) o);
499      }
500
501      return f.format(d);
502    }
503
504    return String.valueOf(o);
505  }
506}