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}