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}