diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index cc3192a02..fec1b8834 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -2264,6 +2264,11 @@ private void print(final InputStream inputStream, final Appendable out, final bo * Prints the {@code value} as the next value on the line to {@code out}. The value will be escaped or encapsulated as needed. Useful when one wants to * avoid creating CSVPrinters. Trims the value if {@link #getTrim()} is true. * + *

+ * If {@code value} is a {@link Reader} and {@link #getTrim()} is true, the reader is fully read into memory so the value can be trimmed before printing; + * otherwise the value is streamed without holding it in memory. The reader is never closed by this method. + *

+ * * @param value value to output. * @param out where to print the value. * @param newRecord if this a new record. @@ -2287,8 +2292,15 @@ public void print(final Object value, final Appendable out, final boolean newRec } else if (value instanceof CharSequence) { charSequence = (CharSequence) value; } else if (value instanceof Reader) { - print((Reader) value, out, newRecord); - return; + // CSV-331: the streaming path cannot apply trim(), so when trimming is enabled the reader + // is fully read into memory first; trim() is applied below on the regular path. + // The reader is not closed here: closing it remains the caller's responsibility. + if (getTrim()) { + charSequence = IOUtils.toString((Reader) value); + } else { + print((Reader) value, out, newRecord); + return; + } } else if (value instanceof InputStream) { print((InputStream) value, out, newRecord); return; @@ -2348,6 +2360,13 @@ public CSVPrinter print(final Path out, final Charset charset) throws IOExceptio return print(Files.newBufferedWriter(out, charset)); } + /** + * Prints a Reader value by streaming it without holding the whole value in memory. + *

+ * Trimming is NOT applied here: when {@link #getTrim()} is true, {@link #print(Object, Appendable, boolean)} + * buffers the reader in memory and takes the regular CharSequence path instead (CSV-331). + *

+ */ private void print(final Reader reader, final Appendable out, final boolean newRecord) throws IOException { // Reader is never null here if (!newRecord) { diff --git a/src/test/java/org/apache/commons/csv/CSVFormatTest.java b/src/test/java/org/apache/commons/csv/CSVFormatTest.java index 14067d13c..dac44f364 100644 --- a/src/test/java/org/apache/commons/csv/CSVFormatTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFormatTest.java @@ -1373,6 +1373,66 @@ void testTrim() throws IOException { assertEquals("header", out.toString()); } + /** + * Tests CSV-331: trim must also be applied when + * printing a Reader value. + */ + @Test + void testTrimReaderValue() throws IOException { + final CSVFormat formatWithTrim = CSVFormat.DEFAULT.withDelimiter(',').withTrim().withQuote(null).withRecordSeparator(CRLF); + + Reader in = new StringReader("a,b,c"); + final StringBuilder out = new StringBuilder(); + formatWithTrim.print(in, out, true); + assertEquals("a,b,c", out.toString()); + + in = new StringReader(" x,y,z "); + out.setLength(0); + formatWithTrim.print(in, out, true); + assertEquals("x,y,z", out.toString()); + + in = new StringReader(""); + out.setLength(0); + formatWithTrim.print(in, out, true); + assertEquals("", out.toString()); + + in = new StringReader("header\r\n"); + out.setLength(0); + formatWithTrim.print(in, out, true); + assertEquals("header", out.toString()); + + // CSV-331 reproduction: ORACLE enables trim. + final StringBuilder oracleOut = new StringBuilder(); + CSVFormat.ORACLE.print(new StringReader(" Alice "), oracleOut, true); + assertEquals("Alice", oracleOut.toString()); + } + + /** + * Tests CSV-331: a Reader value must still be + * encapsulated when needed after trimming. + */ + @Test + void testTrimReaderValueWithQuotes() throws IOException { + final CSVFormat formatWithTrim = CSVFormat.DEFAULT.withDelimiter(',').withTrim().withRecordSeparator(CRLF); + final Reader in = new StringReader(" \"a,b\" "); + final StringBuilder out = new StringBuilder(); + formatWithTrim.print(in, out, true); + assertEquals("\"\"\"a,b\"\"\"", out.toString()); + } + + /** + * Tests CSV-331: without trim a Reader value must + * keep streaming as-is. + */ + @Test + void testNoTrimReaderValue() throws IOException { + final CSVFormat formatWithoutTrim = CSVFormat.DEFAULT.withDelimiter(',').withQuote(null).withRecordSeparator(CRLF); + final Reader in = new StringReader(" Alice "); + final StringBuilder out = new StringBuilder(); + formatWithoutTrim.print(in, out, true); + assertEquals(" Alice ", out.toString()); + } + @Test void testWithCommentStart() { final CSVFormat formatWithCommentStart = CSVFormat.DEFAULT.withCommentMarker('#');