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}