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}