From e5f85a0521c34a7c3e8ab22bc73186701d7ba110 Mon Sep 17 00:00:00 2001 From: tha2015 Date: Thu, 8 Dec 2016 23:14:48 -0800 Subject: [PATCH] [CSV-63] CSVPrinter always quotes empty string if it is the first on a line --- .../org/apache/commons/csv/CSVFormat.java | 44 ++++++++-- .../org/apache/commons/csv/CSVPrinter.java | 44 +++++++--- .../apache/commons/csv/CSVPrinterTest.java | 85 +++++++++++++++++++ 3 files changed, 153 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 44801f907d..5a7f5886ab 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -43,6 +43,7 @@ import java.sql.SQLException; import java.util.Arrays; import java.util.HashSet; +import java.util.Iterator; import java.util.Set; /** @@ -900,7 +901,7 @@ public CSVPrinter print(final File out, Charset charset) throws IOException { * * @param out * the output. - * @param charset + * @param charset * A charset. * @return a printer to an output. * @throws IOException @@ -926,6 +927,10 @@ public CSVPrinter print(final Path out, Charset charset) throws IOException { * @since 1.4 */ public void print(final Object value, final Appendable out, final boolean newRecord) throws IOException { + print(value, out, newRecord, false); + } + + private void print(final Object value, final Appendable out, final boolean newRecord, final boolean hasMoreValues) throws IOException { // null values are considered empty // Only call CharSequence.toString() if you have to, helps GC-free use cases. CharSequence charSequence; @@ -935,11 +940,11 @@ public void print(final Object value, final Appendable out, final boolean newRec charSequence = value instanceof CharSequence ? (CharSequence) value : value.toString(); } charSequence = getTrim() ? trim(charSequence) : charSequence; - this.print(value, charSequence, 0, charSequence.length(), out, newRecord); + this.print(value, charSequence, 0, charSequence.length(), out, newRecord, hasMoreValues); } private void print(final Object object, final CharSequence value, final int offset, final int len, - final Appendable out, final boolean newRecord) throws IOException { + final Appendable out, final boolean newRecord, final boolean hasMoreValues) throws IOException { if (!newRecord) { out.append(getDelimiter()); } @@ -947,7 +952,7 @@ private void print(final Object object, final CharSequence value, final int offs out.append(value); } else if (isQuoteCharacterSet()) { // the original object is needed so can check for Number - printAndQuote(object, value, offset, len, out, newRecord); + printAndQuote(object, value, offset, len, out, newRecord, hasMoreValues); } else if (isEscapeCharacterSet()) { printAndEscape(value, offset, len, out); } else { @@ -1000,7 +1005,7 @@ private void printAndEscape(final CharSequence value, final int offset, final in */ // the original object is needed so can check for Number private void printAndQuote(final Object object, final CharSequence value, final int offset, final int len, - final Appendable out, final boolean newRecord) throws IOException { + final Appendable out, final boolean newRecord, final boolean hasMoreValues) throws IOException { boolean quote = false; int start = offset; int pos = offset; @@ -1030,7 +1035,7 @@ private void printAndQuote(final Object object, final CharSequence value, final // on the line, as it may be the only thing on the // line. If it were not quoted in that case, // an empty line has no tokens. - if (newRecord) { + if (newRecord && !hasMoreValues) { quote = true; } } else { @@ -1143,7 +1148,32 @@ public void println(final Appendable out) throws IOException { */ public void printRecord(final Appendable out, final Object... values) throws IOException { for (int i = 0; i < values.length; i++) { - print(values[i], out, i == 0); + print(values[i], out, i == 0, i < values.length - 1); + } + println(out); + } + + /** + * Prints the given {@code values} to {@code out} as a single record of delimiter separated values followed by the + * record separator. + * + *

+ * The values will be quoted if needed. Quotes and new-line characters will be escaped. This method adds the record + * separator to the output after printing the record, so there is no need to call {@link #println(Appendable)}. + *

+ * + * @param out + * where to write. + * @param values + * values to output. + * @throws IOException + * If an I/O error occurs. + * @since 1.5 + */ + public void printRecord(final Appendable out, final Iterable values) throws IOException { + boolean firstValue = true; + for (final Iterator it = values.iterator(); it.hasNext(); firstValue = false) { + print(it.next(), out, firstValue, it.hasNext()); } println(out); } diff --git a/src/main/java/org/apache/commons/csv/CSVPrinter.java b/src/main/java/org/apache/commons/csv/CSVPrinter.java index 265d11d0d8..414a423390 100644 --- a/src/main/java/org/apache/commons/csv/CSVPrinter.java +++ b/src/main/java/org/apache/commons/csv/CSVPrinter.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Iterator; /** * Prints values in a CSV format. @@ -193,10 +194,8 @@ public void println() throws IOException { * If an I/O error occurs */ public void printRecord(final Iterable values) throws IOException { - for (final Object value : values) { - print(value); - } - println(); + format.printRecord(out, values); + newRecord = true; } /** @@ -257,13 +256,24 @@ public void printRecord(final Object... values) throws IOException { * If an I/O error occurs */ public void printRecords(final Iterable values) throws IOException { - for (final Object value : values) { - if (value instanceof Object[]) { - this.printRecord((Object[]) value); - } else if (value instanceof Iterable) { - this.printRecord((Iterable) value); - } else { - this.printRecord(value); + final Iterator it = values.iterator(); + if (it.hasNext()) { + final Object firstValue = it.next(); + + // When printing a single record, use printRecord(Iterable) to handle first value correctly + if (!(firstValue instanceof Object[]) && !(firstValue instanceof Iterable)) { + this.printRecord(values); + return; + } + + for (final Object value : values) { + if (value instanceof Object[]) { + this.printRecord((Object[]) value); + } else if (value instanceof Iterable) { + this.printRecord((Iterable) value); + } else { + this.printRecord(value); + } } } } @@ -308,6 +318,12 @@ public void printRecords(final Iterable values) throws IOException { * If an I/O error occurs */ public void printRecords(final Object... values) throws IOException { + // When printing a single record, use printRecord(Object...) to handle first value correctly + if (values.length > 0 && !(values[0] instanceof Object[]) && !(values[0] instanceof Iterable)) { + this.printRecord(values); + return; + } + for (final Object value : values) { if (value instanceof Object[]) { this.printRecord((Object[]) value); @@ -331,11 +347,13 @@ public void printRecords(final Object... values) throws IOException { */ public void printRecords(final ResultSet resultSet) throws SQLException, IOException { final int columnCount = resultSet.getMetaData().getColumnCount(); + final Object[] values = new Object[columnCount]; while (resultSet.next()) { for (int i = 1; i <= columnCount; i++) { - print(resultSet.getObject(i)); + values[i - 1] = resultSet.getObject(i); } - println(); + // use printRecord(Object...) to handle first value correctly + this.printRecord(values); } } } diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index 3ee2438f5d..96009fd85f 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -518,6 +518,91 @@ public void testJdbcPrinterWithResultSet() throws IOException, ClassNotFoundExce assertEquals("ID,NAME" + recordSeparator + "1,r1" + recordSeparator + "2,r2" + recordSeparator, sw.toString()); } + @Test + public void testExcelPrintAllArrayOfArraysWithFirstEmptyValue1() throws IOException { + final StringWriter sw = new StringWriter(); + try (final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { + printer.printRecords((Object[]) new String[][] { { "", "r1c2" } }); + assertEquals(",r1c2" + recordSeparator, sw.toString()); + } + } + @Test + public void testExcelPrintAllArrayOfArraysWithFirstEmptyValue2() throws IOException { + final StringWriter sw = new StringWriter(); + try (final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { + printer.printRecords((Object[]) new String[][] { { "" } }); + assertEquals("\"\"" + recordSeparator, sw.toString()); + } + } + + @Test + public void testExcelPrintAllArrayOfListsWithFirstEmptyValue1() throws IOException { + final StringWriter sw = new StringWriter(); + try (final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { + printer.printRecords( + (Object[]) new List[] { Arrays.asList("", "r1c2") }); + assertEquals(",r1c2" + recordSeparator, sw.toString()); + } + } + + @Test + public void testExcelPrintAllArrayOfListsWithFirstEmptyValue2() throws IOException { + final StringWriter sw = new StringWriter(); + try (final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { + printer.printRecords( + (Object[]) new List[] { Arrays.asList("") }); + assertEquals("\"\"" + recordSeparator, sw.toString()); + } + } + + @Test + public void testExcelPrintAllIterableOfListsWithFirstEmptyValue1() throws IOException { + final StringWriter sw = new StringWriter(); + try (final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { + printer.printRecords( + Arrays.asList(new List[] { Arrays.asList("", "r1c2") })); + assertEquals(",r1c2" + recordSeparator, sw.toString()); + } + } + + @Test + public void testExcelPrintAllIterableOfArraysWithFirstEmptyValue2() throws IOException { + final StringWriter sw = new StringWriter(); + try (final CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { + printer.printRecords(Arrays.asList(new String[][] { { "" } })); + assertEquals("\"\"" + recordSeparator, sw.toString()); + } + } + + + @Test + public void testJdbcPrinterWithFirstEmptyValue1() throws IOException, ClassNotFoundException, SQLException { + final StringWriter sw = new StringWriter(); + Class.forName("org.h2.Driver"); + try (final Connection connection = geH2Connection();) { + try (final Statement stmt = connection.createStatement(); + final ResultSet resultSet = stmt.executeQuery("select '' AS EMPTYVALUE, 1 AS ID from DUAL"); + final CSVPrinter printer = CSVFormat.DEFAULT.withHeader(resultSet).print(sw)) { + printer.printRecords(resultSet); + } + } + assertEquals("EMPTYVALUE,ID" + recordSeparator + ",1" + recordSeparator, sw.toString()); + } + + @Test + public void testJdbcPrinterWithFirstEmptyValue2() throws IOException, ClassNotFoundException, SQLException { + final StringWriter sw = new StringWriter(); + Class.forName("org.h2.Driver"); + try (final Connection connection = geH2Connection();) { + try (final Statement stmt = connection.createStatement(); + final ResultSet resultSet = stmt.executeQuery("select '' AS EMPTYVALUE from DUAL"); + final CSVPrinter printer = CSVFormat.DEFAULT.withHeader(resultSet).print(sw)) { + printer.printRecords(resultSet); + } + } + assertEquals("EMPTYVALUE" + recordSeparator + "\"\"" + recordSeparator, sw.toString()); + } + @Test public void testJdbcPrinterWithResultSetMetaData() throws IOException, ClassNotFoundException, SQLException { final StringWriter sw = new StringWriter();