001/*
002 * Copyright 2009-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2009-2024 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-2024 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  @NotNull 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  @NotNull private static final OutputFormat DEFAULT_OUTPUT_FORMAT =
078       OutputFormat.COLUMNS;
079
080
081
082  /**
083   * The default spacer to use between columns.
084   */
085  @NotNull private static final String DEFAULT_SPACER = " ";
086
087
088
089  /**
090   * The default date format string that will be used for timestamps.
091   */
092  @NotNull 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  @Nullable private final FormattableColumn timestampColumn;
108
109  // The columns to be formatted.
110  @NotNull private final FormattableColumn[] columns;
111
112  // The output format to use.
113  @NotNull private final OutputFormat outputFormat;
114
115  // The string to insert between columns.
116  @NotNull private final String spacer;
117
118  // The format string to use for the timestamp.
119  @NotNull private final String timestampFormat;
120
121  // The thread-local formatter to use for floating-point values.
122  @NotNull private final transient ThreadLocal<DecimalFormat> decimalFormatter;
123
124  // The thread-local formatter to use when formatting timestamps.
125  @NotNull private final transient ThreadLocal<SimpleDateFormat>
126       timestampFormatter;
127
128
129
130  /**
131   * Creates a column formatter that will format the provided columns with the
132   * default settings.
133   *
134   * @param  columns  The columns to be formatted.  At least one column must be
135   *                  provided.
136   */
137  public ColumnFormatter(@NotNull final FormattableColumn... columns)
138  {
139    this(false, null, null, null, columns);
140  }
141
142
143
144  /**
145   * Creates a column formatter that will format the provided columns.
146   *
147   * @param  includeTimestamp  Indicates whether to insert a timestamp before
148   *                           the first column when generating data lines
149   * @param  timestampFormat   The format string to use for the timestamp.  It
150   *                           may be {@code null} if no timestamp should be
151   *                           included or the default format should be used.
152   *                           If a format is provided, then it should be one
153   *                           that will always generate timestamps with a
154   *                           constant width.
155   * @param  outputFormat      The output format to use.
156   * @param  spacer            The spacer to use between columns.  It may be
157   *                           {@code null} if the default spacer should be
158   *                           used.  This will only apply for an output format
159   *                           of {@code COLUMNS}.
160   * @param  columns           The columns to be formatted.  At least one
161   *                           column must be provided.
162   */
163  public ColumnFormatter(final boolean includeTimestamp,
164                         @Nullable final String timestampFormat,
165                         @Nullable final OutputFormat outputFormat,
166                         @Nullable final String spacer,
167                         @NotNull final FormattableColumn... columns)
168  {
169    Validator.ensureNotNull(columns);
170    Validator.ensureTrue(columns.length > 0);
171
172    this.includeTimestamp = includeTimestamp;
173    this.columns          = columns;
174
175    decimalFormatter   = new ThreadLocal<>();
176    timestampFormatter = new ThreadLocal<>();
177
178    if (timestampFormat == null)
179    {
180      this.timestampFormat = DEFAULT_TIMESTAMP_FORMAT;
181    }
182    else
183    {
184      this.timestampFormat = timestampFormat;
185    }
186
187    if (outputFormat == null)
188    {
189      this.outputFormat = DEFAULT_OUTPUT_FORMAT;
190    }
191    else
192    {
193      this.outputFormat = outputFormat;
194    }
195
196    if (spacer == null)
197    {
198      this.spacer = DEFAULT_SPACER;
199    }
200    else
201    {
202      this.spacer = spacer;
203    }
204
205    if (includeTimestamp)
206    {
207      final SimpleDateFormat dateFormat =
208           new SimpleDateFormat(this.timestampFormat);
209      final String timestamp = dateFormat.format(new Date());
210      final String label = INFO_COLUMN_LABEL_TIMESTAMP.get();
211      final int width = Math.max(label.length(), timestamp.length());
212
213      timestampFormatter.set(dateFormat);
214      timestampColumn =
215           new FormattableColumn(width, HorizontalAlignment.LEFT, label);
216    }
217    else
218    {
219      timestampColumn = null;
220    }
221  }
222
223
224
225  /**
226   * Indicates whether timestamps will be included in the output.
227   *
228   * @return  {@code true} if timestamps should be included, or {@code false}
229   *          if not.
230   */
231  public boolean includeTimestamps()
232  {
233    return includeTimestamp;
234  }
235
236
237
238  /**
239   * Retrieves the format string that will be used for generating timestamps.
240   *
241   * @return  The format string that will be used for generating timestamps.
242   */
243  @NotNull()
244  public String getTimestampFormatString()
245  {
246    return timestampFormat;
247  }
248
249
250
251  /**
252   * Retrieves the output format that will be used.
253   *
254   * @return  The output format for this formatter.
255   */
256  @NotNull()
257  public OutputFormat getOutputFormat()
258  {
259    return outputFormat;
260  }
261
262
263
264  /**
265   * Retrieves the spacer that will be used between columns.
266   *
267   * @return  The spacer that will be used between columns.
268   */
269  @NotNull()
270  public String getSpacer()
271  {
272    return spacer;
273  }
274
275
276
277  /**
278   * Retrieves the set of columns for this formatter.
279   *
280   * @return  The set of columns for this formatter.
281   */
282  @NotNull()
283  public FormattableColumn[] getColumns()
284  {
285    final FormattableColumn[] copy = new FormattableColumn[columns.length];
286    System.arraycopy(columns, 0, copy, 0, columns.length);
287    return copy;
288  }
289
290
291
292  /**
293   * Obtains the lines that should comprise the column headers.
294   *
295   * @param  includeDashes  Indicates whether to include a row of dashes below
296   *                        the headers if appropriate for the output format.
297   *
298   * @return  The lines that should comprise the column headers.
299   */
300  @NotNull()
301  public String[] getHeaderLines(final boolean includeDashes)
302  {
303    if (outputFormat == OutputFormat.COLUMNS)
304    {
305      int maxColumns = 1;
306      final String[][] headerLines = new String[columns.length][];
307      for (int i=0; i < columns.length; i++)
308      {
309        headerLines[i] = columns[i].getLabelLines();
310        maxColumns = Math.max(maxColumns, headerLines[i].length);
311      }
312
313      final StringBuilder[] buffers = new StringBuilder[maxColumns];
314      for (int i=0; i < maxColumns; i++)
315      {
316        final StringBuilder buffer = new StringBuilder();
317        buffers[i] = buffer;
318        if (includeTimestamp)
319        {
320          if (i == (maxColumns - 1))
321          {
322            timestampColumn.format(buffer, timestampColumn.getSingleLabelLine(),
323                 outputFormat);
324          }
325          else
326          {
327            timestampColumn.format(buffer, "", outputFormat);
328          }
329        }
330
331        for (int j=0; j < columns.length; j++)
332        {
333          if (includeTimestamp || (j > 0))
334          {
335            buffer.append(spacer);
336          }
337
338          final int rowNumber = i + headerLines[j].length - maxColumns;
339          if (rowNumber < 0)
340          {
341            columns[j].format(buffer, "", outputFormat);
342          }
343          else
344          {
345            columns[j].format(buffer, headerLines[j][rowNumber], outputFormat);
346          }
347        }
348      }
349
350      final String[] returnArray;
351      if (includeDashes)
352      {
353        returnArray = new String[maxColumns+1];
354      }
355      else
356      {
357        returnArray = new String[maxColumns];
358      }
359
360      for (int i=0; i < maxColumns; i++)
361      {
362        returnArray[i] = buffers[i].toString();
363      }
364
365      if (includeDashes)
366      {
367        final StringBuilder buffer = new StringBuilder();
368        if (timestampColumn != null)
369        {
370          for (int i=0; i < timestampColumn.getWidth(); i++)
371          {
372            buffer.append('-');
373          }
374        }
375
376        for (int i=0; i < columns.length; i++)
377        {
378          if (includeTimestamp || (i > 0))
379          {
380            buffer.append(spacer);
381          }
382
383          for (int j=0; j < columns[i].getWidth(); j++)
384          {
385            buffer.append('-');
386          }
387        }
388
389        returnArray[returnArray.length - 1] = buffer.toString();
390      }
391
392      return returnArray;
393    }
394    else
395    {
396      final StringBuilder buffer = new StringBuilder();
397      if (timestampColumn != null)
398      {
399        timestampColumn.format(buffer, timestampColumn.getSingleLabelLine(),
400             outputFormat);
401      }
402
403      for (int i=0; i < columns.length; i++)
404      {
405        if (includeTimestamp || (i > 0))
406        {
407          if (outputFormat == OutputFormat.TAB_DELIMITED_TEXT)
408          {
409            buffer.append('\t');
410          }
411          else if (outputFormat == OutputFormat.CSV)
412          {
413            buffer.append(',');
414          }
415        }
416
417        final FormattableColumn c = columns[i];
418        c.format(buffer, c.getSingleLabelLine(), outputFormat);
419      }
420
421      return new String[] { buffer.toString() };
422    }
423  }
424
425
426
427  /**
428   * Formats a row of data.  The provided data must correspond to the columns
429   * used when creating this formatter.
430   *
431   * @param  columnData  The elements to include in each row of the data.
432   *
433   * @return  A string containing the formatted row.
434   */
435  @NotNull()
436  public String formatRow(@NotNull final Object... columnData)
437  {
438    final StringBuilder buffer = new StringBuilder();
439
440    if (includeTimestamp)
441    {
442      SimpleDateFormat dateFormat = timestampFormatter.get();
443      if (dateFormat == null)
444      {
445        dateFormat = new SimpleDateFormat(timestampFormat);
446        timestampFormatter.set(dateFormat);
447      }
448
449      timestampColumn.format(buffer, dateFormat.format(new Date()),
450           outputFormat);
451    }
452
453    for (int i=0; i < columns.length; i++)
454    {
455      if (includeTimestamp || (i > 0))
456      {
457        switch (outputFormat)
458        {
459          case TAB_DELIMITED_TEXT:
460            buffer.append('\t');
461            break;
462          case CSV:
463            buffer.append(',');
464            break;
465          case COLUMNS:
466            buffer.append(spacer);
467            break;
468        }
469      }
470
471      if (i >= columnData.length)
472      {
473        columns[i].format(buffer, "", outputFormat);
474      }
475      else
476      {
477        columns[i].format(buffer, toString(columnData[i]), outputFormat);
478      }
479    }
480
481    return buffer.toString();
482  }
483
484
485
486  /**
487   * Retrieves a string representation of the provided object.  If the object
488   * is {@code null}, then the empty string will be returned.  If the object is
489   * a {@code Float} or {@code Double}, then it will be formatted using a
490   * DecimalFormat with a format string of "0.000".  Otherwise, the
491   * {@code String.valueOf} method will be used to obtain the string
492   * representation.
493   *
494   * @param  o  The object for which to retrieve the string representation.
495   *
496   * @return  A string representation of the provided object.
497   */
498  @NotNull()
499  private String toString(@Nullable final Object o)
500  {
501    if (o == null)
502    {
503      return "";
504    }
505
506    if ((o instanceof Float) || (o instanceof Double))
507    {
508      DecimalFormat f = decimalFormatter.get();
509      if (f == null)
510      {
511        f = new DecimalFormat("0.000", DECIMAL_FORMAT_SYMBOLS);
512        decimalFormatter.set(f);
513      }
514
515      final double d;
516      if (o instanceof Float)
517      {
518        d = ((Float) o).doubleValue();
519      }
520      else
521      {
522        d = ((Double) o);
523      }
524
525      return f.format(d);
526    }
527
528    return String.valueOf(o);
529  }
530}