diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvGenerator.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvGenerator.java index 0f47ce5e..6d17bde4 100644 --- a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvGenerator.java +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvGenerator.java @@ -108,7 +108,23 @@ public enum Feature * * @since 2.9.9 */ - ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR(false) + ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR(false), + + /** + * Feature that determines whether a line-feed will be written at the end of content, + * after the last row of output. + *

+ * NOTE! When disabling this feature it is important that + * {@link #flush()} is NOT called before {@link #close()} is called; + * the current implementation relies on ability to essentially remove the + * last linefeed that was appended in the output buffer. + *

+ * Default value is {@code true} so all rows, including the last, are terminated by + * a line feed. + * + * @since 2.17 + */ + WRITE_LINEFEED_AFTER_LAST_ROW(true) ; protected final boolean _defaultState; diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvEncoder.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvEncoder.java index d37188a5..2abb6a43 100644 --- a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvEncoder.java +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvEncoder.java @@ -162,7 +162,9 @@ public class CsvEncoder */ protected int _lastBuffered = -1; - /* + // @since 2.17 (dataformats-csv#45) + protected boolean _trailingLFRemoved = false; + /********************************************************** /* Output buffering, low-level /********************************************************** @@ -1097,6 +1099,11 @@ public void flush(boolean flushStream) throws IOException public void close(boolean autoClose, boolean flushStream) throws IOException { + // May need to remove the linefeed appended after the last row written + // (if not yet done) + if (!CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW.enabledIn(_csvFeatures)) { + _removeTrailingLF(); + } _flushBuffer(); if (autoClose) { _out.close(); @@ -1108,6 +1115,15 @@ public void close(boolean autoClose, boolean flushStream) throws IOException _releaseBuffers(); } + private void _removeTrailingLF() throws IOException { + if (!_trailingLFRemoved) { + _trailingLFRemoved = true; + // Remove trailing LF if (but only if) it appears to be in output + // buffer (may not be possible if `flush()` has been called) + _outputTail = Math.max(0, _outputTail - _cfgLineSeparatorLength); + } + } + /* /********************************************************** /* Internal methods diff --git a/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/CSVGeneratorTest.java b/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/CSVGeneratorTest.java index c82f7094..64723bed 100644 --- a/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/CSVGeneratorTest.java +++ b/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/CSVGeneratorTest.java @@ -10,7 +10,8 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.StreamWriteFeature; -import com.fasterxml.jackson.databind.JsonMappingException; + +import com.fasterxml.jackson.databind.DatabindException; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.csv.*; @@ -88,8 +89,14 @@ public void testSimpleExplicit() throws Exception FiveMinuteUser user = new FiveMinuteUser("Silu", "Seppala", false, Gender.MALE, new byte[] { 1, 2, 3, 4, 5}); - String csv = MAPPER.writer(schema).writeValueAsString(user); - assertEquals("Silu,Seppala,MALE,AQIDBAU=,false\n", csv); + assertEquals("Silu,Seppala,MALE,AQIDBAU=,false\n", + MAPPER.writer(schema).writeValueAsString(user)); + + // 14-Jan-2024, tatu: [dataformats-text#45] allow suppressing trailing LF: + assertEquals("Silu,Seppala,MALE,AQIDBAU=,false", + MAPPER.writer(schema) + .without(CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW) + .writeValueAsString(user)); } public void testSimpleWithAutoSchema() throws Exception @@ -102,10 +109,17 @@ public void testWriteHeaders() throws Exception { CsvSchema schema = MAPPER.schemaFor(FiveMinuteUser.class).withHeader(); FiveMinuteUser user = new FiveMinuteUser("Barbie", "Benton", false, Gender.FEMALE, null); - String result = MAPPER.writer(schema).writeValueAsString(user); assertEquals("firstName,lastName,gender,verified,userImage\n" - +"Barbie,Benton,FEMALE,false,\n", result); - } + +"Barbie,Benton,FEMALE,false,\n", + MAPPER.writer(schema).writeValueAsString(user)); + + // 14-Jan-2024, tatu: [dataformats-text#45] allow suppressing trailing LF: + assertEquals("firstName,lastName,gender,verified,userImage\n" + +"Barbie,Benton,FEMALE,false,", + MAPPER.writer(schema) + .without(CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW) + .writeValueAsString(user)); +} /** * Test that verifies that if a header line is needed, configured schema @@ -118,7 +132,7 @@ public void testFailedWriteHeaders() throws Exception try { MAPPER.writer(schema).writeValueAsString(user); fail("Should fail without columns"); - } catch (JsonMappingException e) { + } catch (DatabindException e) { verifyException(e, "contains no column names"); } } diff --git a/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/MultipleWritesTest.java b/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/MultipleWritesTest.java index 89b1444a..a31a91e7 100644 --- a/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/MultipleWritesTest.java +++ b/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/MultipleWritesTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.dataformat.csv.CsvGenerator; import com.fasterxml.jackson.dataformat.csv.CsvMapper; import com.fasterxml.jackson.dataformat.csv.CsvSchema; import com.fasterxml.jackson.dataformat.csv.ModuleTestBase; @@ -68,16 +69,30 @@ public void testMultipleListWrites() throws Exception public void testWriteValuesWithPOJOs() throws Exception { - CsvSchema schema = MAPPER.schemaFor(Pojo.class).withUseHeader(true); + final CsvSchema schema = MAPPER.schemaFor(Pojo.class).withUseHeader(true); + ObjectWriter writer = MAPPER.writer(schema); StringWriter sw = new StringWriter(); - SequenceWriter seqw = writer.writeValues(sw); - seqw.write(new Pojo(1, 2, 3)); - seqw.write(new Pojo(0, 15, 9)); - seqw.write(new Pojo(7, 8, 9)); - seqw.flush(); + try (SequenceWriter seqw = writer.writeValues(sw)) { + seqw.write(new Pojo(1, 2, 3)); + seqw.write(new Pojo(0, 15, 9)); + seqw.write(new Pojo(7, 8, 9)); + } assertEquals("a,b,c\n1,2,3\n0,15,9\n7,8,9\n", sw.toString()); - seqw.close(); + + // 14-Jan-2024, tatu: [dataformats-text#45] allow suppressing trailing LF. + // NOTE! Any form of `flush()` will prevent ability to "remove" trailing LF so... + writer = writer + .without(SerializationFeature.FLUSH_AFTER_WRITE_VALUE) + .without(CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW); + sw = new StringWriter(); + try (SequenceWriter seqw = writer.writeValues(sw)) { + seqw.write(new Pojo(1, 2, 3)); + seqw.write(new Pojo(0, 15, 9)); + seqw.write(new Pojo(7, 8, 9)); + } + assertEquals("a,b,c\n1,2,3\n0,15,9\n7,8,9", + sw.toString()); } } diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 52ea55d7..e46c14a5 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -250,3 +250,9 @@ Arthur Chan (arthurscchan@github) * Contributed fix for #445: `YAMLParser` throws unexpected `NullPointerException` in certain number parsing cases (2.16.1) + +Mathieu Lavigne (@mathieu-lavigne) + +* Proposed #45 (and suggested implementation): (csv) Allow skipping ending line break + (`CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW`) + (2.17.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 3511167d..eabd5c71 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -16,7 +16,9 @@ Active Maintainers: 2.17.0 (not yet released) -- +#45: (csv) Allow skipping ending line break + (`CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW`) + (proposed by Mathieu L) 2.16.1 (24-Dec-2023)