001    /*
002     * Copyright 2009-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2009-2015 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     */
021    package com.unboundid.util;
022    
023    
024    
025    import java.io.Serializable;
026    import java.text.DecimalFormat;
027    import java.text.DecimalFormatSymbols;
028    import java.text.SimpleDateFormat;
029    import java.util.Date;
030    
031    import 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)
042    public 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    }