From 16b9e2c79122f5a76863854cc4b33939da63710d Mon Sep 17 00:00:00 2001 From: "Gary D. Gregory" Date: Mon, 18 Apr 2016 06:16:51 +0000 Subject: [PATCH] [CSV-175] Support for ignoring trailing delimiter. [CSV-177] Support trimming leading and trailing blanks. [CSV-178] Create default formats for Informix UNLOAD and UNLOAD CSV. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/csv/trunk@1739694 13f79535-47bb-0310-9956-ffa450edef68 --- src/changes/changes.xml | 5 +- .../org/apache/commons/csv/CSVFormat.java | 280 +++++-- .../org/apache/commons/csv/CSVParser.java | 14 +- .../org/apache/commons/csv/CSVPrinter.java | 4 + .../org/apache/commons/csv/CSVParserTest.java | 24 + .../apache/commons/csv/CSVPrinterTest.java | 691 +++++++++--------- 6 files changed, 615 insertions(+), 403 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 58942940..200d5049 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -43,9 +43,12 @@ CSVPrinter doesn't skip creation of header record if skipHeaderRecord is set to true Add IgnoreCase option for accessing header names The null string should be case-sensitive when reading records - CsvFormat.nullString should not be escaped + CSVFormat.nullString should not be escaped CSVFormat.MYSQL nullString should be "\N" Fix Javadoc to say CSVFormat with() methods return a new CSVFormat + Support for ignoring trailing delimiter. + Support trimming leading and trailing blanks. + Create default formats for Informix UNLOAD and UNLOAD CSV. CSVFormat.with* methods clear the header comments diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 8985379f..739d2e7e 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -23,6 +23,7 @@ import static org.apache.commons.csv.Constants.CR; import static org.apache.commons.csv.Constants.CRLF; import static org.apache.commons.csv.Constants.DOUBLE_QUOTE_CHAR; import static org.apache.commons.csv.Constants.LF; +import static org.apache.commons.csv.Constants.PIPE; import static org.apache.commons.csv.Constants.TAB; import java.io.IOException; @@ -157,17 +158,29 @@ public final class CSVFormat implements Serializable { /** * @see CSVFormat#DEFAULT */ - Default(CSVFormat.DEFAULT), + Default(CSVFormat.DEFAULT), /** * @see CSVFormat#EXCEL */ - Excel(CSVFormat.EXCEL), + Excel(CSVFormat.EXCEL), + + /** + * @see CSVFormat#INFORMIX_UNLOAD + * @since 1.3 + */ + InformixUnload(CSVFormat.INFORMIX_UNLOAD), + + /** + * @see CSVFormat#INFORMIX_UNLOAD_CSV + * @since 1.3 + */ + InformixUnloadCsv(CSVFormat.INFORMIX_UNLOAD_CSV), /** * @see CSVFormat#MYSQL */ - MySQL(CSVFormat.MYSQL), + MySQL(CSVFormat.MYSQL), /** * @see CSVFormat#RFC4180 @@ -184,7 +197,7 @@ public final class CSVFormat implements Serializable { private Predefined(final CSVFormat format) { this.format = format; } - + /** * Gets the format. * @@ -194,7 +207,7 @@ public final class CSVFormat implements Serializable { return format; } }; - + /** * Standard comma separated format, as for {@link #RFC4180} but allowing empty lines. * @@ -207,10 +220,11 @@ public final class CSVFormat implements Serializable { *
  • withRecordSeparator("\r\n")
  • *
  • withIgnoreEmptyLines(true)
  • * + * * @see Predefined#Default */ - public static final CSVFormat DEFAULT = new CSVFormat(COMMA, DOUBLE_QUOTE_CHAR, null, null, null, false, true, - CRLF, null, null, null, false, false, false); + public static final CSVFormat DEFAULT = new CSVFormat(COMMA, DOUBLE_QUOTE_CHAR, null, null, null, false, true, CRLF, + null, null, null, false, false, false, false, false); /** * Excel file format (using a comma as the value delimiter). Note that the actual value delimiter used by Excel is @@ -238,10 +252,64 @@ public final class CSVFormat implements Serializable { * Note: this is currently like {@link #RFC4180} plus {@link #withAllowMissingColumnNames(boolean) * withAllowMissingColumnNames(true)}. *

    + * * @see Predefined#Excel */ public static final CSVFormat EXCEL = DEFAULT.withIgnoreEmptyLines(false).withAllowMissingColumnNames(); + /** + * Default Informix CSV UNLOAD format used by the {@code UNLOAD TO file_name} operation. + * + *

    + * This is a comma-delimited format with a LF character as the line separator. Values are not quoted and special + * characters are escaped with {@code '\'}. The default NULL string is {@code "\\N"}. + *

    + * + *

    + * Settings are: + *

    + * + * + * @see Predefined#MySQL + * @see + * http://www.ibm.com/support/knowledgecenter/SSBJG3_2.5.0/com.ibm.gen_busug.doc/c_fgl_InOutSql_UNLOAD.htm + * @since 1.3 + */ + public static final CSVFormat INFORMIX_UNLOAD = DEFAULT.withDelimiter(PIPE).withEscape(BACKSLASH) + .withQuote(DOUBLE_QUOTE_CHAR).withRecordSeparator(LF); + + /** + * Default Informix CSV UNLOAD format used by the {@code UNLOAD TO file_name} operation. + * + *

    + * This is a comma-delimited format with a LF character as the line separator. Values are not quoted and special + * characters are escaped with {@code '\'}. The default NULL string is {@code "\\N"}. + *

    + * + *

    + * Settings are: + *

    + * + * + * @see Predefined#MySQL + * @see + * http://www.ibm.com/support/knowledgecenter/SSBJG3_2.5.0/com.ibm.gen_busug.doc/c_fgl_InOutSql_UNLOAD.htm + * @since 1.3 + */ + public static final CSVFormat INFORMIX_UNLOAD_CSV = DEFAULT.withDelimiter(COMMA).withQuote(DOUBLE_QUOTE_CHAR) + .withRecordSeparator(LF); + /** * Default MySQL format used by the {@code SELECT INTO OUTFILE} and {@code LOAD DATA INFILE} operations. * @@ -263,12 +331,12 @@ public final class CSVFormat implements Serializable { * * * @see Predefined#MySQL - * @see - * http://dev.mysql.com/doc/refman/5.1/en/load-data.html + * @see http://dev.mysql.com/doc/refman/5.1/en/load + * -data.html */ public static final CSVFormat MYSQL = DEFAULT.withDelimiter(TAB).withEscape(BACKSLASH).withIgnoreEmptyLines(false) .withQuote(null).withRecordSeparator(LF).withNullString("\\N"); - + /** * Comma separated format as defined by RFC 4180. * @@ -281,12 +349,13 @@ public final class CSVFormat implements Serializable { *
  • withRecordSeparator("\r\n")
  • *
  • withIgnoreEmptyLines(false)
  • * + * * @see Predefined#RFC4180 */ public static final CSVFormat RFC4180 = DEFAULT.withIgnoreEmptyLines(false); - + private static final long serialVersionUID = 1L; - + /** * Tab-delimited format. * @@ -299,10 +368,11 @@ public final class CSVFormat implements Serializable { *
  • withRecordSeparator("\r\n")
  • *
  • withIgnoreSurroundingSpaces(true)
  • * + * * @see Predefined#TDF */ public static final CSVFormat TDF = DEFAULT.withDelimiter(TAB).withIgnoreSurroundingSpaces(); - + /** * Returns true if the given character is a line break character. * @@ -314,7 +384,7 @@ public final class CSVFormat implements Serializable { private static boolean isLineBreak(final char c) { return c == LF || c == CR; } - + /** * Returns true if the given character is a line break character. * @@ -326,7 +396,7 @@ public final class CSVFormat implements Serializable { private static boolean isLineBreak(final Character c) { return c != null && isLineBreak(c.charValue()); } - + /** * Creates a new CSV format with the specified delimiter. * @@ -348,9 +418,10 @@ public final class CSVFormat implements Serializable { * @see #TDF */ public static CSVFormat newFormat(final char delimiter) { - return new CSVFormat(delimiter, null, null, null, null, false, false, null, null, null, null, false, false, false); + return new CSVFormat(delimiter, null, null, null, null, false, false, null, null, null, null, false, false, + false, false, false); } - + /** * Gets one of the predefined formats from {@link CSVFormat.Predefined}. * @@ -362,15 +433,15 @@ public final class CSVFormat implements Serializable { public static CSVFormat valueOf(final String format) { return CSVFormat.Predefined.valueOf(format).getFormat(); } - + private final boolean allowMissingColumnNames; - + private final Character commentMarker; // null if commenting is disabled - + private final char delimiter; - + private final Character escapeCharacter; // null if escaping is disabled - + private final String[] header; // array of header column names private final String[] headerComments; // array of header comment lines @@ -391,6 +462,10 @@ public final class CSVFormat implements Serializable { private final boolean skipHeaderRecord; + private final boolean trailingDelimiter; + + private final boolean trim; + /** * Creates a customized CSV format. * @@ -422,6 +497,9 @@ public final class CSVFormat implements Serializable { * TODO * @param ignoreHeaderCase * TODO + * @param trim + * TODO + * @param trailingDelimiter TODO * @throws IllegalArgumentException * if the delimiter is a line break character */ @@ -429,7 +507,7 @@ public final class CSVFormat implements Serializable { final Character commentStart, final Character escape, final boolean ignoreSurroundingSpaces, final boolean ignoreEmptyLines, final String recordSeparator, final String nullString, final Object[] headerComments, final String[] header, final boolean skipHeaderRecord, - final boolean allowMissingColumnNames, final boolean ignoreHeaderCase) { + final boolean allowMissingColumnNames, final boolean ignoreHeaderCase, boolean trim, boolean trailingDelimiter) { this.delimiter = delimiter; this.quoteCharacter = quoteChar; this.quoteMode = quoteMode; @@ -444,6 +522,8 @@ public final class CSVFormat implements Serializable { this.header = header == null ? null : header.clone(); this.skipHeaderRecord = skipHeaderRecord; this.ignoreHeaderCase = ignoreHeaderCase; + this.trailingDelimiter = trailingDelimiter; + this.trim = trim; validate(); } @@ -620,11 +700,9 @@ public final class CSVFormat implements Serializable { /** * Gets the String to convert to and from {@code null}. * * * @return the String to convert to and from {@code null}. No substitution occurs if {@code null} @@ -669,6 +747,24 @@ public final class CSVFormat implements Serializable { return skipHeaderRecord; } + /** + * Returns whether to add a trailing delimiter. + * + * @return whether to add a trailing delimiter. + */ + public boolean getTrailingDelimiter() { + return trailingDelimiter; + } + + /** + * Returns whether to trim leading and trailing blanks. + * + * @return whether to trim leading and trailing blanks. + */ + public boolean getTrim() { + return trim; + } + @Override public int hashCode() { final int prime = 31; @@ -827,43 +923,43 @@ public final class CSVFormat implements Serializable { if (isLineBreak(delimiter)) { throw new IllegalArgumentException("The delimiter cannot be a line break"); } - + if (quoteCharacter != null && delimiter == quoteCharacter.charValue()) { - throw new IllegalArgumentException("The quoteChar character and the delimiter cannot be the same ('" + - quoteCharacter + "')"); + throw new IllegalArgumentException( + "The quoteChar character and the delimiter cannot be the same ('" + quoteCharacter + "')"); } if (escapeCharacter != null && delimiter == escapeCharacter.charValue()) { - throw new IllegalArgumentException("The escape character and the delimiter cannot be the same ('" + - escapeCharacter + "')"); + throw new IllegalArgumentException( + "The escape character and the delimiter cannot be the same ('" + escapeCharacter + "')"); } if (commentMarker != null && delimiter == commentMarker.charValue()) { - throw new IllegalArgumentException("The comment start character and the delimiter cannot be the same ('" + - commentMarker + "')"); + throw new IllegalArgumentException( + "The comment start character and the delimiter cannot be the same ('" + commentMarker + "')"); } if (quoteCharacter != null && quoteCharacter.equals(commentMarker)) { - throw new IllegalArgumentException("The comment start character and the quoteChar cannot be the same ('" + - commentMarker + "')"); + throw new IllegalArgumentException( + "The comment start character and the quoteChar cannot be the same ('" + commentMarker + "')"); } if (escapeCharacter != null && escapeCharacter.equals(commentMarker)) { - throw new IllegalArgumentException("The comment start and the escape character cannot be the same ('" + - commentMarker + "')"); + throw new IllegalArgumentException( + "The comment start and the escape character cannot be the same ('" + commentMarker + "')"); } if (escapeCharacter == null && quoteMode == QuoteMode.NONE) { throw new IllegalArgumentException("No quotes mode set but no escape character is set"); } - + // validate header if (header != null) { final Set dupCheck = new HashSet(); for (final String hdr : header) { if (!dupCheck.add(hdr)) { - throw new IllegalArgumentException("The header contains a duplicate entry: '" + hdr + "' in " + - Arrays.toString(header)); + throw new IllegalArgumentException( + "The header contains a duplicate entry: '" + hdr + "' in " + Arrays.toString(header)); } } } @@ -891,7 +987,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withAllowMissingColumnNames(final boolean allowMissingColumnNames) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** @@ -926,7 +1022,7 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** @@ -944,7 +1040,7 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** @@ -975,12 +1071,12 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escape, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, skipHeaderRecord, - allowMissingColumnNames, ignoreHeaderCase); + allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** - * Returns a new {@code CSVFormat} with the header of the format set from the result set metadata. - * The header can either be parsed automatically from the input file with: + * Returns a new {@code CSVFormat} with the header of the format set from the result set metadata. The header can + * either be parsed automatically from the input file with: * *
          * CSVFormat format = aformat.withHeader();
    @@ -1009,8 +1105,8 @@ public final class CSVFormat implements Serializable {
         }
     
         /**
    -     * Returns a new {@code CSVFormat} with the header of the format set from the result set metadata.
    -     *  The header can either be parsed automatically from the input file with:
    +     * Returns a new {@code CSVFormat} with the header of the format set from the result set metadata. The header can
    +     * either be parsed automatically from the input file with:
          *
          * 
          * CSVFormat format = aformat.withHeader();
    @@ -1045,12 +1141,12 @@ public final class CSVFormat implements Serializable {
             }
             return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
                     ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, labels,
    -                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase);
    +                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
         }
     
         /**
    -     * Returns a new {@code CSVFormat} with the header of the format set to the given values. 
    -     * The header can either be parsed automatically from the input file with:
    +     * Returns a new {@code CSVFormat} with the header of the format set to the given values. The header can either be
    +     * parsed automatically from the input file with:
          *
          * 
          * CSVFormat format = aformat.withHeader();
    @@ -1074,12 +1170,12 @@ public final class CSVFormat implements Serializable {
         public CSVFormat withHeader(final String... header) {
             return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
                     ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
    -                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase);
    +                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
         }
     
         /**
    -     * Returns a new {@code CSVFormat} with the header comments of the format set to the given values. 
    -     * The comments will be printed first, before the headers. This setting is ignored by the parser.
    +     * Returns a new {@code CSVFormat} with the header comments of the format set to the given values. The comments will
    +     * be printed first, before the headers. This setting is ignored by the parser.
          *
          * 
          * CSVFormat format = aformat.withHeaderComments("Generated by Apache Commons CSV 1.1.", new Date());
    @@ -1095,7 +1191,7 @@ public final class CSVFormat implements Serializable {
         public CSVFormat withHeaderComments(final Object... headerComments) {
             return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
                     ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
    -                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase);
    +                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
         }
     
         /**
    @@ -1120,7 +1216,7 @@ public final class CSVFormat implements Serializable {
         public CSVFormat withIgnoreEmptyLines(final boolean ignoreEmptyLines) {
             return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
                     ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
    -                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase);
    +                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
         }
     
         /**
    @@ -1138,14 +1234,14 @@ public final class CSVFormat implements Serializable {
          * Returns a new {@code CSVFormat} with whether header names should be accessed ignoring case.
          *
          * @param ignoreHeaderCase
    -     *            the case mapping behavior, {@code true} to access name/values, {@code false} to leave the
    -     *            mapping as is.
    +     *            the case mapping behavior, {@code true} to access name/values, {@code false} to leave the mapping as
    +     *            is.
          * @return A new CSVFormat that will ignore case header name if specified as {@code true}
          */
         public CSVFormat withIgnoreHeaderCase(final boolean ignoreHeaderCase) {
             return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
                     ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
    -                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase);
    +                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
         }
     
         /**
    @@ -1170,17 +1266,15 @@ public final class CSVFormat implements Serializable {
         public CSVFormat withIgnoreSurroundingSpaces(final boolean ignoreSurroundingSpaces) {
             return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
                     ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
    -                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase);
    +                skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
         }
     
         /**
          * Returns a new {@code CSVFormat} with conversions to and from null for strings on input and output.
          * 
      - *
    • - * Reading: Converts strings equal to the given {@code nullString} to {@code null} when reading + *
    • Reading: Converts strings equal to the given {@code nullString} to {@code null} when reading * records.
    • - *
    • - * Writing: Writes {@code null} as the given {@code nullString} when writing records.
    • + *
    • Writing: Writes {@code null} as the given {@code nullString} when writing records.
    • *
    * * @param nullString @@ -1191,7 +1285,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withNullString(final String nullString) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** @@ -1222,7 +1316,7 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteChar, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, skipHeaderRecord, - allowMissingColumnNames, ignoreHeaderCase); + allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** @@ -1236,7 +1330,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withQuoteMode(final QuoteMode quoteModePolicy) { return new CSVFormat(delimiter, quoteCharacter, quoteModePolicy, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** @@ -1274,7 +1368,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withRecordSeparator(final String recordSeparator) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); } /** @@ -1301,6 +1395,52 @@ public final class CSVFormat implements Serializable { public CSVFormat withSkipHeaderRecord(final boolean skipHeaderRecord) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + } + + /** + * Returns a new {@code CSVFormat} with whether to trim leading and trailing blanks. + * + * @param trim + * whether to trim leading and trailing blanks. + * + * @return A new CSVFormat that is equal to this but with the specified trim setting. + */ + public CSVFormat withTrim(final boolean trim) { + return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, + ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + } + + /** + * Returns a new {@code CSVFormat} to add a trailing delimiter. + * * + * @return A new CSVFormat that is equal to this but with the trailing delimiter setting. + */ + public CSVFormat withTrailingDelimiter() { + return withTrailingDelimiter(true); + } + + /** + * Returns a new {@code CSVFormat} with whether to add a trailing delimiter. + * + * @param trim + * whether to add a trailing delimiter. + * + * @return A new CSVFormat that is equal to this but with the specified trailing delimiter setting. + */ + public CSVFormat withTrailingDelimiter(final boolean trailingDelimiter) { + return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, + ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + } + + /** + * Returns a new {@code CSVFormat} to trim leading and trailing blanks. + * + * @return A new CSVFormat that is equal to this but with the trim setting on. + */ + public CSVFormat withTrim() { + return withTrim(true); } } diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java b/src/main/java/org/apache/commons/csv/CSVParser.java index c0f28c5e..0129cfc5 100644 --- a/src/main/java/org/apache/commons/csv/CSVParser.java +++ b/src/main/java/org/apache/commons/csv/CSVParser.java @@ -286,10 +286,14 @@ public final class CSVParser implements Iterable, Closeable { this.recordNumber = recordNumber - 1; } - private void addRecordValue() { + private void addRecordValue(boolean lastRecord) { final String input = this.reusableToken.content.toString(); + final String inputClean = this.format.getTrim() ? input.trim() : input; + if (lastRecord && inputClean.isEmpty() && this.format.getTrailingDelimiter()) { + return; + } final String nullString = this.format.getNullString(); - this.record.add(input.equals(nullString) ? null : input); + this.record.add(inputClean.equals(nullString) ? null : inputClean); } /** @@ -497,14 +501,14 @@ public final class CSVParser implements Iterable, Closeable { this.lexer.nextToken(this.reusableToken); switch (this.reusableToken.type) { case TOKEN: - this.addRecordValue(); + this.addRecordValue(false); break; case EORECORD: - this.addRecordValue(); + this.addRecordValue(true); break; case EOF: if (this.reusableToken.isReady) { - this.addRecordValue(); + this.addRecordValue(true); } break; case INVALID: diff --git a/src/main/java/org/apache/commons/csv/CSVPrinter.java b/src/main/java/org/apache/commons/csv/CSVPrinter.java index 0e9a2dee..cb500c42 100644 --- a/src/main/java/org/apache/commons/csv/CSVPrinter.java +++ b/src/main/java/org/apache/commons/csv/CSVPrinter.java @@ -128,6 +128,7 @@ public final class CSVPrinter implements Flushable, Closeable { } else { strValue = value.toString(); } + strValue = format.getTrim() ? strValue.trim() : strValue; this.print(value, strValue, 0, strValue.length()); } @@ -351,6 +352,9 @@ public final class CSVPrinter implements Flushable, Closeable { * If an I/O error occurs */ public void println() throws IOException { + if (format.getTrailingDelimiter()) { + out.append(format.getDelimiter()); + } final String recordSeparator = format.getRecordSeparator(); if (recordSeparator != null) { out.append(recordSeparator); diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 32be45de..1c68867c 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -911,6 +911,30 @@ public class CSVParserTest { } } + @Test + public void testTrailingDelimiter() throws Exception { + final Reader in = new StringReader("a,a,a,\n\"1\",\"2\",\"3\",\nx,y,z,"); + final Iterator records = CSVFormat.DEFAULT.withHeader("X", "Y", "Z").withSkipHeaderRecord().withTrailingDelimiter() + .parse(in).iterator(); + final CSVRecord record = records.next(); + assertEquals("1", record.get("X")); + assertEquals("2", record.get("Y")); + assertEquals("3", record.get("Z")); + Assert.assertEquals(3, record.size()); + } + + @Test + public void testTrim() throws Exception { + final Reader in = new StringReader("a,a,a\n\" 1 \",\" 2 \",\" 3 \"\nx,y,z"); + final Iterator records = CSVFormat.DEFAULT.withHeader("X", "Y", "Z").withSkipHeaderRecord().withTrim() + .parse(in).iterator(); + final CSVRecord record = records.next(); + assertEquals("1", record.get("X")); + assertEquals("2", record.get("Y")); + assertEquals("3", record.get("Z")); + Assert.assertEquals(3, record.size()); + } + private void validateLineNumbers(final String lineSeparator) throws IOException { final CSVParser parser = CSVParser.parse("a" + lineSeparator + "b" + lineSeparator + "c", CSVFormat.DEFAULT.withRecordSeparator(lineSeparator)); diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index 82b829d2..d7837883 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -50,8 +50,6 @@ public class CSVPrinterTest { private static final int ITERATIONS_FOR_RANDOM_TEST = 50000; - private final String recordSeparator = CSVFormat.DEFAULT.getRecordSeparator(); - private static String printable(final String s) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { @@ -65,6 +63,8 @@ public class CSVPrinterTest { return sb.toString(); } + private final String recordSeparator = CSVFormat.DEFAULT.getRecordSeparator(); + private void doOneRandom(final CSVFormat format) throws Exception { final Random r = new Random(); @@ -97,6 +97,31 @@ public class CSVPrinterTest { parser.close(); } + private void doRandom(final CSVFormat format, final int iter) throws Exception { + for (int i = 0; i < iter; i++) { + doOneRandom(format); + } + } + + /** + * Converts an input CSV array into expected output values WRT NULLs. NULL strings are converted to null values + * because the parser will convert these strings to null. + */ + private T[] expectNulls(final T[] original, final CSVFormat csvFormat) { + final T[] fixed = original.clone(); + for (int i = 0; i < fixed.length; i++) { + if (ObjectUtils.equals(csvFormat.getNullString(), fixed[i])) { + fixed[i] = null; + } + } + return fixed; + } + + private Connection geH2Connection() throws SQLException, ClassNotFoundException { + Class.forName("org.h2.Driver"); + return DriverManager.getConnection("jdbc:h2:mem:my_test;", "sa", ""); + } + private String[][] generateLines(final int nLines, final int nCol) { final String[][] lines = new String[nLines][]; for (int i = 0; i < nLines; i++) { @@ -109,10 +134,18 @@ public class CSVPrinterTest { return lines; } - private void doRandom(final CSVFormat format, final int iter) throws Exception { - for (int i = 0; i < iter; i++) { - doOneRandom(format); - } + private CSVPrinter printWithHeaderComments(final StringWriter sw, final Date now, final CSVFormat baseFormat) + throws IOException { + CSVFormat format = baseFormat; + // Use withHeaderComments first to test CSV-145 + format = format.withHeaderComments("Generated by Apache Commons CSV 1.1", now); + format = format.withCommentMarker('#'); + format = format.withHeader("Col1", "Col2"); + final CSVPrinter csvPrinter = format.print(sw); + csvPrinter.printRecord("A", "B"); + csvPrinter.printRecord("C", "D"); + csvPrinter.close(); + return csvPrinter; } private String randStr() { @@ -163,6 +196,58 @@ public class CSVPrinterTest { return new String(buf); } + private void setUpTable(final Connection connection) throws SQLException { + final Statement statement = connection.createStatement(); + try { + statement.execute("CREATE TABLE TEST(ID INT PRIMARY KEY, NAME VARCHAR(255))"); + statement.execute("insert into TEST values(1, 'r1')"); + statement.execute("insert into TEST values(2, 'r2')"); + } finally { + statement.close(); + } + } + + @Test + public void testDelimeterQuoted() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\'')); + printer.print("a,b,c"); + printer.print("xyz"); + assertEquals("'a,b,c',xyz", sw.toString()); + printer.close(); + } + + @Test + public void testDelimeterQuoteNONE() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVFormat format = CSVFormat.DEFAULT.withEscape('!').withQuoteMode(QuoteMode.NONE); + final CSVPrinter printer = new CSVPrinter(sw, format); + printer.print("a,b,c"); + printer.print("xyz"); + assertEquals("a!,b!,c,xyz", sw.toString()); + printer.close(); + } + + @Test + public void testDelimiterEscaped() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape('!').withQuote(null)); + printer.print("a,b,c"); + printer.print("xyz"); + assertEquals("a!,b!,c,xyz", sw.toString()); + printer.close(); + } + + @Test + public void testDelimiterPlain() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); + printer.print("a,b,c"); + printer.print("xyz"); + assertEquals("a,b,c,xyz", sw.toString()); + printer.close(); + } + @Test public void testDisabledComment() throws IOException { final StringWriter sw = new StringWriter(); @@ -172,6 +257,72 @@ public class CSVPrinterTest { assertEquals("", sw.toString()); printer.close(); } + + @Test + public void testEOLEscaped() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withEscape('!')); + printer.print("a\rb\nc"); + printer.print("x\fy\bz"); + assertEquals("a!rb!nc,x\fy\bz", sw.toString()); + printer.close(); + } + + @Test + public void testEOLPlain() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); + printer.print("a\rb\nc"); + printer.print("x\fy\bz"); + assertEquals("a\rb\nc,x\fy\bz", sw.toString()); + printer.close(); + } + + @Test + public void testEOLQuoted() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\'')); + printer.print("a\rb\nc"); + printer.print("x\by\fz"); + assertEquals("'a\rb\nc',x\by\fz", sw.toString()); + printer.close(); + } + + @Test + public void testEscapeBackslash() throws IOException { + StringWriter sw = new StringWriter(); + final char quoteChar = '\''; + final String eol = "\r\n"; + CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); + printer.print("\\"); + printer.close(); + assertEquals("'\\'", sw.toString()); + + sw = new StringWriter(); + printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); + printer.print("\\\r"); + printer.close(); + assertEquals("'\\\r'", sw.toString()); + + sw = new StringWriter(); + printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); + printer.print("X\\\r"); + printer.close(); + assertEquals("'X\\\r'", sw.toString()); + + sw = new StringWriter(); + printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); + printer.printRecord(new Object[] { "\\\r" }); + printer.close(); + assertEquals("'\\\r'" + eol, sw.toString()); + + sw = new StringWriter(); + printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); + printer.print("\\\\"); + printer.close(); + assertEquals("'\\\\'", sw.toString()); + + } @Test public void testExcelPrintAllArrayOfArrays() throws IOException { @@ -228,77 +379,54 @@ public class CSVPrinterTest { printer.close(); } - private Connection geH2Connection() throws SQLException, ClassNotFoundException { - Class.forName("org.h2.Driver"); - return DriverManager.getConnection("jdbc:h2:mem:my_test;", "sa", ""); + @Test + public void testHeader() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withHeader("C1", "C2", "C3")); + printer.printRecord("a", "b", "c"); + printer.printRecord("x", "y", "z"); + assertEquals("C1,C2,C3\r\na,b,c\r\nx,y,z\r\n", sw.toString()); + printer.close(); } @Test - @Ignore - public void testJira135All() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); + public void testHeaderCommentExcel() throws IOException { final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, format); - final List list = new LinkedList(); - list.add("\""); - list.add("\n"); - list.add("\\"); - printer.printRecord(list); - printer.close(); - final String expected = "\"\\\"\",\"\\n\",\"\\\"" + format.getRecordSeparator(); - assertEquals(expected, sw.toString()); - final String[] record0 = toFirstRecordValues(expected, format); - assertArrayEquals(expectNulls(list.toArray(), format), record0); + final Date now = new Date(); + final CSVFormat format = CSVFormat.EXCEL; + final CSVPrinter csvPrinter = printWithHeaderComments(sw, now, format); + assertEquals("# Generated by Apache Commons CSV 1.1\r\n# " + now + "\r\nCol1,Col2\r\nA,B\r\nC,D\r\n", + sw.toString()); + csvPrinter.close(); } - + @Test - @Ignore - public void testJira135_part3() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); + public void testHeaderCommentTdf() throws IOException { final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, format); - final List list = new LinkedList(); - list.add("\\"); - printer.printRecord(list); - printer.close(); - final String expected = "\"\\\\\"" + format.getRecordSeparator(); - assertEquals(expected, sw.toString()); - final String[] record0 = toFirstRecordValues(expected, format); - assertArrayEquals(expectNulls(list.toArray(), format), record0); + final Date now = new Date(); + final CSVFormat format = CSVFormat.TDF; + final CSVPrinter csvPrinter = printWithHeaderComments(sw, now, format); + assertEquals("# Generated by Apache Commons CSV 1.1\r\n# " + now + "\r\nCol1\tCol2\r\nA\tB\r\nC\tD\r\n", + sw.toString()); + csvPrinter.close(); } - + @Test - @Ignore - public void testJira135_part2() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); + public void testHeaderNotSet() throws IOException { final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, format); - final List list = new LinkedList(); - list.add("\n"); - printer.printRecord(list); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); + printer.printRecord("a", "b", "c"); + printer.printRecord("x", "y", "z"); + assertEquals("a,b,c\r\nx,y,z\r\n", sw.toString()); printer.close(); - final String expected = "\"\\n\"" + format.getRecordSeparator(); - assertEquals(expected, sw.toString()); - final String[] record0 = toFirstRecordValues(expected, format); - assertArrayEquals(expectNulls(list.toArray(), format), record0); } - - @Test - @Ignore - public void testJira135_part1() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, format); - final List list = new LinkedList(); - list.add("\""); - printer.printRecord(list); - printer.close(); - final String expected = "\"\\\"\"" + format.getRecordSeparator(); - assertEquals(expected, sw.toString()); - final String[] record0 = toFirstRecordValues(expected, format); - assertArrayEquals(expectNulls(list.toArray(), format), record0); + + @Test(expected = IllegalArgumentException.class) + public void testInvalidFormat() throws Exception { + final CSVFormat invalidFormat = CSVFormat.DEFAULT.withDelimiter(CR); + new CSVPrinter(new StringWriter(), invalidFormat).close(); } - + @Test public void testJdbcPrinter() throws IOException, ClassNotFoundException, SQLException { final StringWriter sw = new StringWriter(); @@ -361,15 +489,70 @@ public class CSVPrinterTest { } } - private void setUpTable(final Connection connection) throws SQLException { - final Statement statement = connection.createStatement(); - try { - statement.execute("CREATE TABLE TEST(ID INT PRIMARY KEY, NAME VARCHAR(255))"); - statement.execute("insert into TEST values(1, 'r1')"); - statement.execute("insert into TEST values(2, 'r2')"); - } finally { - statement.close(); - } + @Test + @Ignore + public void testJira135_part1() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, format); + final List list = new LinkedList(); + list.add("\""); + printer.printRecord(list); + printer.close(); + final String expected = "\"\\\"\"" + format.getRecordSeparator(); + assertEquals(expected, sw.toString()); + final String[] record0 = toFirstRecordValues(expected, format); + assertArrayEquals(expectNulls(list.toArray(), format), record0); + } + + @Test + @Ignore + public void testJira135_part2() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, format); + final List list = new LinkedList(); + list.add("\n"); + printer.printRecord(list); + printer.close(); + final String expected = "\"\\n\"" + format.getRecordSeparator(); + assertEquals(expected, sw.toString()); + final String[] record0 = toFirstRecordValues(expected, format); + assertArrayEquals(expectNulls(list.toArray(), format), record0); + } + + @Test + @Ignore + public void testJira135_part3() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, format); + final List list = new LinkedList(); + list.add("\\"); + printer.printRecord(list); + printer.close(); + final String expected = "\"\\\\\"" + format.getRecordSeparator(); + assertEquals(expected, sw.toString()); + final String[] record0 = toFirstRecordValues(expected, format); + assertArrayEquals(expectNulls(list.toArray(), format), record0); + } + + @Test + @Ignore + public void testJira135All() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote('"').withEscape('\\'); + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, format); + final List list = new LinkedList(); + list.add("\""); + list.add("\n"); + list.add("\\"); + printer.printRecord(list); + printer.close(); + final String expected = "\"\\\"\",\"\\n\",\"\\\"" + format.getRecordSeparator(); + assertEquals(expected, sw.toString()); + final String[] record0 = toFirstRecordValues(expected, format); + assertArrayEquals(expectNulls(list.toArray(), format), record0); } @Test @@ -382,11 +565,6 @@ public class CSVPrinterTest { printer.close(); } - @Test - public void testMySqlNullStringDefault() throws IOException { - assertEquals("\\N", CSVFormat.MYSQL.getNullString()); - } - @Test public void testMySqlNullOutput() throws IOException { Object[] s = new String[] { "NULL", null }; @@ -489,22 +667,85 @@ public class CSVPrinterTest { assertArrayEquals(expectNulls(s, format), record0); } - /** - * Converts an input CSV array into expected output values WRT NULLs. NULL strings are converted to null values - * because the parser will convert these strings to null. - */ - private T[] expectNulls(final T[] original, final CSVFormat csvFormat) { - final T[] fixed = original.clone(); - for (int i = 0; i < fixed.length; i++) { - if (ObjectUtils.equals(csvFormat.getNullString(), fixed[i])) { - fixed[i] = null; - } - } - return fixed; + @Test + public void testMySqlNullStringDefault() throws IOException { + assertEquals("\\N", CSVFormat.MYSQL.getNullString()); } - private String[] toFirstRecordValues(final String expected, final CSVFormat format) throws IOException { - return CSVParser.parse(expected, format).getRecords().get(0).values(); + @Test(expected = IllegalArgumentException.class) + public void testNewCsvPrinterAppendableNullFormat() throws Exception { + new CSVPrinter(new StringWriter(), null).close(); + } + + @Test(expected = IllegalArgumentException.class) + public void testNewCSVPrinterNullAppendableFormat() throws Exception { + new CSVPrinter(null, CSVFormat.DEFAULT).close(); + } + + @Test + public void testParseCustomNullValues() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVFormat format = CSVFormat.DEFAULT.withNullString("NULL"); + final CSVPrinter printer = new CSVPrinter(sw, format); + printer.printRecord("a", null, "b"); + printer.close(); + final String csvString = sw.toString(); + assertEquals("a,NULL,b" + recordSeparator, csvString); + final Iterable iterable = format.parse(new StringReader(csvString)); + final Iterator iterator = iterable.iterator(); + final CSVRecord record = iterator.next(); + assertEquals("a", record.get(0)); + assertEquals(null, record.get(1)); + assertEquals("b", record.get(2)); + assertFalse(iterator.hasNext()); + ((CSVParser) iterable).close(); + } + + @Test + public void testPlainEscaped() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withEscape('!')); + printer.print("abc"); + printer.print("xyz"); + assertEquals("abc,xyz", sw.toString()); + printer.close(); + } + + @Test + public void testPlainPlain() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); + printer.print("abc"); + printer.print("xyz"); + assertEquals("abc,xyz", sw.toString()); + printer.close(); + } + + @Test + public void testPlainQuoted() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\'')); + printer.print("abc"); + assertEquals("abc", sw.toString()); + printer.close(); + } + + @Test + public void testPrint() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = CSVFormat.DEFAULT.print(sw); + printer.printRecord("a", "b\\c"); + assertEquals("a,b\\c" + recordSeparator, sw.toString()); + printer.close(); + } + + @Test + public void testPrintCustomNullValues() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withNullString("NULL")); + printer.printRecord("a", null, "b"); + assertEquals("a,NULL,b" + recordSeparator, sw.toString()); + printer.close(); } @Test @@ -570,15 +811,6 @@ public class CSVPrinterTest { printer.close(); } - @Test - public void testPrint() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = CSVFormat.DEFAULT.print(sw); - printer.printRecord("a", "b\\c"); - assertEquals("a,b\\c" + recordSeparator, sw.toString()); - printer.close(); - } - @Test public void testPrintNullValues() throws IOException { final StringWriter sw = new StringWriter(); @@ -588,34 +820,6 @@ public class CSVPrinterTest { printer.close(); } - @Test - public void testPrintCustomNullValues() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withNullString("NULL")); - printer.printRecord("a", null, "b"); - assertEquals("a,NULL,b" + recordSeparator, sw.toString()); - printer.close(); - } - - @Test - public void testParseCustomNullValues() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVFormat format = CSVFormat.DEFAULT.withNullString("NULL"); - final CSVPrinter printer = new CSVPrinter(sw, format); - printer.printRecord("a", null, "b"); - printer.close(); - final String csvString = sw.toString(); - assertEquals("a,NULL,b" + recordSeparator, csvString); - final Iterable iterable = format.parse(new StringReader(csvString)); - final Iterator iterator = iterable.iterator(); - final CSVRecord record = iterator.next(); - assertEquals("a", record.get(0)); - assertEquals(null, record.get(1)); - assertEquals("b", record.get(2)); - assertFalse(iterator.hasNext()); - ((CSVParser) iterable).close(); - } - @Test public void testQuoteAll() throws IOException { final StringWriter sw = new StringWriter(); @@ -649,23 +853,14 @@ public class CSVPrinterTest { doRandom(CSVFormat.MYSQL, ITERATIONS_FOR_RANDOM_TEST); } - @Test - public void testRandomTdf() throws Exception { - doRandom(CSVFormat.TDF, ITERATIONS_FOR_RANDOM_TEST); - } - @Test public void testRandomRfc4180() throws Exception { doRandom(CSVFormat.RFC4180, ITERATIONS_FOR_RANDOM_TEST); } @Test - public void testPlainQuoted() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\'')); - printer.print("abc"); - assertEquals("abc", sw.toString()); - printer.close(); + public void testRandomTdf() throws Exception { + doRandom(CSVFormat.TDF, ITERATIONS_FOR_RANDOM_TEST); } @Test @@ -689,142 +884,17 @@ public class CSVPrinterTest { } @Test - public void testDelimeterQuoted() throws IOException { + public void testSkipHeaderRecordFalse() throws IOException { + // functionally identical to testHeader, used to test CSV-153 final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\'')); - printer.print("a,b,c"); - printer.print("xyz"); - assertEquals("'a,b,c',xyz", sw.toString()); - printer.close(); - } - - @Test - public void testDelimeterQuoteNONE() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVFormat format = CSVFormat.DEFAULT.withEscape('!').withQuoteMode(QuoteMode.NONE); - final CSVPrinter printer = new CSVPrinter(sw, format); - printer.print("a,b,c"); - printer.print("xyz"); - assertEquals("a!,b!,c,xyz", sw.toString()); - printer.close(); - } - - @Test - public void testEOLQuoted() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\'')); - printer.print("a\rb\nc"); - printer.print("x\by\fz"); - assertEquals("'a\rb\nc',x\by\fz", sw.toString()); - printer.close(); - } - - @Test - public void testEscapeBackslash() throws IOException { - StringWriter sw = new StringWriter(); - final char quoteChar = '\''; - final String eol = "\r\n"; - CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); - printer.print("\\"); - printer.close(); - assertEquals("'\\'", sw.toString()); - - sw = new StringWriter(); - printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); - printer.print("\\\r"); - printer.close(); - assertEquals("'\\\r'", sw.toString()); - - sw = new StringWriter(); - printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); - printer.print("X\\\r"); - printer.close(); - assertEquals("'X\\\r'", sw.toString()); - - sw = new StringWriter(); - printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); - printer.printRecord(new Object[] { "\\\r" }); - printer.close(); - assertEquals("'\\\r'" + eol, sw.toString()); - - sw = new StringWriter(); - printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(quoteChar)); - printer.print("\\\\"); - printer.close(); - assertEquals("'\\\\'", sw.toString()); - - } - - @Test - public void testPlainEscaped() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withEscape('!')); - printer.print("abc"); - printer.print("xyz"); - assertEquals("abc,xyz", sw.toString()); - printer.close(); - } - - @Test - public void testDelimiterEscaped() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape('!').withQuote(null)); - printer.print("a,b,c"); - printer.print("xyz"); - assertEquals("a!,b!,c,xyz", sw.toString()); - printer.close(); - } - - @Test - public void testEOLEscaped() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withEscape('!')); - printer.print("a\rb\nc"); - printer.print("x\fy\bz"); - assertEquals("a!rb!nc,x\fy\bz", sw.toString()); - printer.close(); - } - - @Test - public void testPlainPlain() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); - printer.print("abc"); - printer.print("xyz"); - assertEquals("abc,xyz", sw.toString()); - printer.close(); - } - - @Test - public void testDelimiterPlain() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); - printer.print("a,b,c"); - printer.print("xyz"); - assertEquals("a,b,c,xyz", sw.toString()); - printer.close(); - } - - @Test - public void testHeader() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withHeader("C1", "C2", "C3")); + final CSVPrinter printer = new CSVPrinter(sw, + CSVFormat.DEFAULT.withQuote(null).withHeader("C1", "C2", "C3").withSkipHeaderRecord(false)); printer.printRecord("a", "b", "c"); printer.printRecord("x", "y", "z"); assertEquals("C1,C2,C3\r\na,b,c\r\nx,y,z\r\n", sw.toString()); printer.close(); } - @Test - public void testHeaderNotSet() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); - printer.printRecord("a", "b", "c"); - printer.printRecord("x", "y", "z"); - assertEquals("a,b,c\r\nx,y,z\r\n", sw.toString()); - printer.close(); - } - @Test public void testSkipHeaderRecordTrue() throws IOException { // functionally identical to testHeaderNotSet, used to test CSV-153 @@ -838,76 +908,43 @@ public class CSVPrinterTest { } @Test - public void testSkipHeaderRecordFalse() throws IOException { - // functionally identical to testHeader, used to test CSV-153 + public void testTrimOnOneColumn() throws IOException { final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, - CSVFormat.DEFAULT.withQuote(null).withHeader("C1", "C2", "C3").withSkipHeaderRecord(false)); - printer.printRecord("a", "b", "c"); - printer.printRecord("x", "y", "z"); - assertEquals("C1,C2,C3\r\na,b,c\r\nx,y,z\r\n", sw.toString()); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrim()); + printer.print(" A "); + assertEquals("A", sw.toString()); printer.close(); } @Test - public void testHeaderCommentExcel() throws IOException { + public void testTrimOnTwoColumns() throws IOException { final StringWriter sw = new StringWriter(); - final Date now = new Date(); - final CSVFormat format = CSVFormat.EXCEL; - final CSVPrinter csvPrinter = printWithHeaderComments(sw, now, format); - assertEquals("# Generated by Apache Commons CSV 1.1\r\n# " + now + "\r\nCol1,Col2\r\nA,B\r\nC,D\r\n", - sw.toString()); - csvPrinter.close(); - } - - @Test - public void testHeaderCommentTdf() throws IOException { - final StringWriter sw = new StringWriter(); - final Date now = new Date(); - final CSVFormat format = CSVFormat.TDF; - final CSVPrinter csvPrinter = printWithHeaderComments(sw, now, format); - assertEquals("# Generated by Apache Commons CSV 1.1\r\n# " + now + "\r\nCol1\tCol2\r\nA\tB\r\nC\tD\r\n", - sw.toString()); - csvPrinter.close(); - } - - private CSVPrinter printWithHeaderComments(final StringWriter sw, final Date now, final CSVFormat baseFormat) - throws IOException { - CSVFormat format = baseFormat; - // Use withHeaderComments first to test CSV-145 - format = format.withHeaderComments("Generated by Apache Commons CSV 1.1", now); - format = format.withCommentMarker('#'); - format = format.withHeader("Col1", "Col2"); - final CSVPrinter csvPrinter = format.print(sw); - csvPrinter.printRecord("A", "B"); - csvPrinter.printRecord("C", "D"); - csvPrinter.close(); - return csvPrinter; - } - - @Test - public void testEOLPlain() throws IOException { - final StringWriter sw = new StringWriter(); - final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null)); - printer.print("a\rb\nc"); - printer.print("x\fy\bz"); - assertEquals("a\rb\nc,x\fy\bz", sw.toString()); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrim()); + printer.print(" A "); + printer.print(" B "); + assertEquals("A,B", sw.toString()); printer.close(); } - @Test(expected = IllegalArgumentException.class) - public void testInvalidFormat() throws Exception { - final CSVFormat invalidFormat = CSVFormat.DEFAULT.withDelimiter(CR); - new CSVPrinter(new StringWriter(), invalidFormat).close(); + @Test + public void testTrailingDelimiterOnTwoColumns() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrailingDelimiter()); + printer.printRecord("A", "B"); + assertEquals("A,B,\r\n", sw.toString()); + printer.close(); } - @Test(expected = IllegalArgumentException.class) - public void testNewCSVPrinterNullAppendableFormat() throws Exception { - new CSVPrinter(null, CSVFormat.DEFAULT).close(); + @Test + public void testTrimOffOneColumn() throws IOException { + final StringWriter sw = new StringWriter(); + final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrim(false)); + printer.print(" A "); + assertEquals("\" A \"", sw.toString()); + printer.close(); } - @Test(expected = IllegalArgumentException.class) - public void testNewCsvPrinterAppendableNullFormat() throws Exception { - new CSVPrinter(new StringWriter(), null).close(); + private String[] toFirstRecordValues(final String expected, final CSVFormat format) throws IOException { + return CSVParser.parse(expected, format).getRecords().get(0).values(); } }