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