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 */
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 }