Skip to content

Commit

Permalink
[CSV-63] CSVPrinter always quotes empty string if it is the first on a
Browse files Browse the repository at this point in the history
line
  • Loading branch information
tha2015 committed Dec 9, 2016
1 parent 9afec3e commit a65b233
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 20 deletions.
76 changes: 69 additions & 7 deletions src/main/java/org/apache/commons/csv/CSVFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
Expand Down Expand Up @@ -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
Expand All @@ -926,6 +927,11 @@ 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 {
final CharSequence charSequence = toPrintString(value);
this.print(value, charSequence, 0, charSequence.length(), out, newRecord);
}

private CharSequence toPrintString(final Object value) {
// null values are considered empty
// Only call CharSequence.toString() if you have to, helps GC-free use cases.
CharSequence charSequence;
Expand All @@ -935,7 +941,7 @@ 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);
return charSequence;
}

private void print(final Object object, final CharSequence value, final int offset, final int len,
Expand Down Expand Up @@ -1009,10 +1015,7 @@ private void printAndQuote(final Object object, final CharSequence value, final
final char delimChar = getDelimiter();
final char quoteChar = getQuoteCharacter().charValue();

QuoteMode quoteModePolicy = getQuoteMode();
if (quoteModePolicy == null) {
quoteModePolicy = QuoteMode.MINIMAL;
}
QuoteMode quoteModePolicy = getQuoteModeWithDefaultValue();
switch (quoteModePolicy) {
case ALL:
quote = true;
Expand Down Expand Up @@ -1106,6 +1109,14 @@ private void printAndQuote(final Object object, final CharSequence value, final
out.append(quoteChar);
}

private QuoteMode getQuoteModeWithDefaultValue() {
QuoteMode quoteModePolicy = getQuoteMode();
if (quoteModePolicy == null) {
quoteModePolicy = QuoteMode.MINIMAL;
}
return quoteModePolicy;
}

/**
* Outputs the record separator.
*
Expand Down Expand Up @@ -1142,12 +1153,63 @@ public void println(final Appendable out) throws IOException {
* @since 1.4
*/
public void printRecord(final Appendable out, final Object... values) throws IOException {
for (int i = 0; i < values.length; i++) {

// First value should be handled specially
if (values.length >= 1) {
printFirstValue(out, values[0], (values.length > 1));
}

// Print remaining values
for (int i = 1; i < values.length; i++) {
print(values[i], out, i == 0);
}
println(out);
}

/**
* Prints the given {@code values} to {@code out} as a single record of delimiter separated values followed by the
* record separator.
*
* <p>
* 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)}.
* </p>
*
* @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 {
final Iterator<?> it = values.iterator();

// First value should be handled specially
if (it.hasNext()) {
final Object value = it.next();
printFirstValue(out, value, it.hasNext());
}

// Print remaining values
while (it.hasNext()) {
print(it.next(), out, false);
}

println(out);
}

private void printFirstValue(final Appendable out, final Object value, final boolean hasSecondValue) throws IOException {
// When the first value is empty and the record has more than one element and quote mode is MINIMAL,
// do not print the quoted empty value (CSV-63)
final CharSequence charSequence = toPrintString(value);
if (!hasSecondValue || getQuoteModeWithDefaultValue() != QuoteMode.MINIMAL || !Constants.EMPTY.equals(charSequence)) {
// Reuse the created string to avoid re-creating objects unnecessarily
this.print(charSequence, charSequence, 0, charSequence.length(), out, true);
}
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
Expand Down
44 changes: 31 additions & 13 deletions src/main/java/org/apache/commons/csv/CSVPrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
}
85 changes: 85 additions & 0 deletions src/test/java/org/apache/commons/csv/CSVPrinterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit a65b233

Please sign in to comment.