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; 041 042 043 044/** 045 * This class provides a data structure with information about a column to use 046 * with the {@link ColumnFormatter}. 047 */ 048@NotMutable() 049@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 050public final class FormattableColumn 051 implements Serializable 052{ 053 /** 054 * A system property that can be used to specify what character should be used 055 * when escaping quotation marks in the output. If set, the value of the 056 * property should be a single character, and it is recommended to be either 057 * the double quote character or the backslash character. 058 */ 059 @NotNull public static final String CSV_QUOTE_ESCAPE_CHARACTER_PROPERTY = 060 FormattableColumn.class.getName() + ".csvQuoteEscapeCharacter"; 061 062 063 064 /** 065 * The character that should be used to escape quotation marks in 066 * CSV-formatted output. RFC 4180 says it should be a double quote (so that 067 * '"' will be escaped as '""'), but we have used a backslash for this purpose 068 * in the past. We'll use the quote to be standards-compliant, but will allow 069 * it to be overridden with a system property. 070 */ 071 private static volatile char CSV_QUOTE_ESCAPE_CHARACTER; 072 static 073 { 074 char escapeCharacter = '"'; 075 final String propertyValue = 076 StaticUtils.getSystemProperty(CSV_QUOTE_ESCAPE_CHARACTER_PROPERTY); 077 if ((propertyValue != null) && (propertyValue.length() == 1)) 078 { 079 escapeCharacter = propertyValue.charAt(0); 080 } 081 082 CSV_QUOTE_ESCAPE_CHARACTER = escapeCharacter; 083 } 084 085 086 087 /** 088 * The serial version UID for this serializable class. 089 */ 090 private static final long serialVersionUID = -67186391702592665L; 091 092 093 094 // The alignment for this column. 095 @NotNull private final HorizontalAlignment alignment; 096 097 // The width for this column. 098 private final int width; 099 100 // The lines that comprise the heading label for this column. 101 @NotNull private final String[] labelLines; 102 103 104 105 /** 106 * Creates a new formattable column with the provided information. 107 * 108 * @param width The width to use for this column. It must be greater 109 * than or equal to 1. 110 * @param alignment The alignment to use for this column. It must not be 111 * {@code null}. 112 * @param labelLines The lines to use as the label for this column. It must 113 * not be {@code null}. 114 */ 115 public FormattableColumn(final int width, 116 @NotNull final HorizontalAlignment alignment, 117 @NotNull final String... labelLines) 118 { 119 Validator.ensureTrue(width >= 1); 120 Validator.ensureNotNull(alignment, labelLines); 121 122 this.width = width; 123 this.alignment = alignment; 124 this.labelLines = labelLines; 125 } 126 127 128 129 /** 130 * Retrieves the width for this column. 131 * 132 * @return The width for this column. 133 */ 134 public int getWidth() 135 { 136 return width; 137 } 138 139 140 141 /** 142 * Retrieves the alignment for this column. 143 * 144 * @return The alignment for this column. 145 */ 146 @NotNull() 147 public HorizontalAlignment getAlignment() 148 { 149 return alignment; 150 } 151 152 153 154 /** 155 * Retrieves the lines to use as the label for this column. 156 * 157 * @return The lines to use as the label for this column. 158 */ 159 @NotNull() 160 public String[] getLabelLines() 161 { 162 return labelLines; 163 } 164 165 166 167 /** 168 * Retrieves a single-line representation of the label. If there are multiple 169 * header lines, then they will be concatenated and separated by a space. 170 * 171 * @return A single-line representation of the label. 172 */ 173 @NotNull() 174 public String getSingleLabelLine() 175 { 176 switch (labelLines.length) 177 { 178 case 0: 179 return ""; 180 case 1: 181 return labelLines[0]; 182 default: 183 final StringBuilder buffer = new StringBuilder(); 184 buffer.append(labelLines[0]); 185 for (int i=1; i < labelLines.length; i++) 186 { 187 buffer.append(' '); 188 buffer.append(labelLines[i]); 189 } 190 return buffer.toString(); 191 } 192 } 193 194 195 196 /** 197 * Appends a formatted representation of the provided text to the given 198 * buffer. 199 * 200 * @param buffer The buffer to which the text should be appended. It must 201 * not be {@code null}. 202 * @param text The text to append to the buffer. It must not be 203 * {@code null}. 204 * @param format The format to use for the text. It must not be 205 * {@code null}. 206 */ 207 public void format(@NotNull final StringBuilder buffer, 208 @NotNull final String text, 209 @NotNull final OutputFormat format) 210 { 211 switch (format) 212 { 213 case TAB_DELIMITED_TEXT: 214 for (int i=0; i < text.length(); i++) 215 { 216 final char c = text.charAt(i); 217 switch (c) 218 { 219 case '\t': 220 buffer.append("\\t"); 221 break; 222 case '\r': 223 buffer.append("\\r"); 224 break; 225 case '\n': 226 buffer.append("\\n"); 227 break; 228 case '\\': 229 buffer.append("\\\\"); 230 break; 231 default: 232 buffer.append(c); 233 break; 234 } 235 } 236 break; 237 238 case CSV: 239 boolean quotesNeeded = false; 240 final int length = text.length(); 241 final int startPos = buffer.length(); 242 for (int i=0; i < length; i++) 243 { 244 final char c = text.charAt(i); 245 if (c == ',') 246 { 247 buffer.append(','); 248 quotesNeeded = true; 249 } 250 else if (c == '"') 251 { 252 buffer.append(CSV_QUOTE_ESCAPE_CHARACTER); 253 buffer.append(c); 254 quotesNeeded = true; 255 } 256 else if (c == CSV_QUOTE_ESCAPE_CHARACTER) 257 { 258 buffer.append(c); 259 buffer.append(c); 260 quotesNeeded = true; 261 } 262 else if (c == '\\') 263 { 264 buffer.append(c); 265 quotesNeeded = true; 266 } 267 else if ((c >= ' ') && (c <= '~')) 268 { 269 buffer.append(c); 270 } 271 else 272 { 273 buffer.append(c); 274 quotesNeeded = true; 275 } 276 } 277 278 if (quotesNeeded) 279 { 280 buffer.insert(startPos, '"'); 281 buffer.append('"'); 282 } 283 break; 284 285 case COLUMNS: 286 alignment.format(buffer, text, width); 287 break; 288 } 289 } 290 291 292 293 /** 294 * Specifies the character that should be used to escape the double quote 295 * character in CSV-formatted values. RFC 4180 states that it should be a 296 * double quote character (that is, a single double quote should be formatted 297 * as '""'), and that is now the default behavior, but the LDAP SDK formerly 298 * used a backslash as an escape character (like '\"'), and this method can be 299 * used to restore that behavior if desired. Alternatively, this can be 300 * accomplished without any change to the application source code by launching 301 * the JVM with the 302 * {@code com.unboundid.util.FormattableColumn.csvQuoteEscapeCharacter} system 303 * property set to a value that contains only the backslash character. 304 * 305 * @param c The character to use to escape the double quote character in 306 * CSV-formatted values. This is only recommended to be the 307 * double quote character or the backslash character. 308 */ 309 public static void setCSVQuoteEscapeCharacter(final char c) 310 { 311 CSV_QUOTE_ESCAPE_CHARACTER = c; 312 } 313 314 315 316 /** 317 * Retrieves a string representation of this formattable column. 318 * 319 * @return A string representation of this formattable column. 320 */ 321 @Override() 322 @NotNull() 323 public String toString() 324 { 325 final StringBuilder buffer = new StringBuilder(); 326 toString(buffer); 327 return buffer.toString(); 328 } 329 330 331 332 /** 333 * Appends a string representation of this formattable column to the provided 334 * buffer. 335 * 336 * @param buffer The buffer to which the string representation should be 337 * appended. 338 */ 339 public void toString(@NotNull final StringBuilder buffer) 340 { 341 buffer.append("FormattableColumn(width="); 342 buffer.append(width); 343 buffer.append(", alignment="); 344 buffer.append(alignment); 345 buffer.append(", label=\""); 346 buffer.append(getSingleLabelLine()); 347 buffer.append("\")"); 348 } 349}