Skip to content

Commit 334e7ae

Browse files
committed
Fix #45: Add CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW
1 parent 61c2f1f commit 334e7ae

File tree

6 files changed

+86
-17
lines changed

6 files changed

+86
-17
lines changed

Diff for: csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvGenerator.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,23 @@ public enum Feature
108108
*
109109
* @since 2.9.9
110110
*/
111-
ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR(false)
111+
ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR(false),
112+
113+
/**
114+
* Feature that determines whether a line-feed will be written at the end of content,
115+
* after the last row of output.
116+
*<p>
117+
* NOTE! When disabling this feature it is important that
118+
* {@link #flush()} is NOT called before {@link #close()} is called;
119+
* the current implementation relies on ability to essentially remove the
120+
* last linefeed that was appended in the output buffer.
121+
*<p>
122+
* Default value is {@code true} so all rows, including the last, are terminated by
123+
* a line feed.
124+
*
125+
* @since 2.17
126+
*/
127+
WRITE_LINEFEED_AFTER_LAST_ROW(true)
112128
;
113129

114130
protected final boolean _defaultState;

Diff for: csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvEncoder.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ public class CsvEncoder
162162
*/
163163
protected int _lastBuffered = -1;
164164

165-
/*
165+
// @since 2.17 (dataformats-csv#45)
166+
protected boolean _trailingLFRemoved = false;
167+
166168
/**********************************************************
167169
/* Output buffering, low-level
168170
/**********************************************************
@@ -1097,6 +1099,11 @@ public void flush(boolean flushStream) throws IOException
10971099

10981100
public void close(boolean autoClose, boolean flushStream) throws IOException
10991101
{
1102+
// May need to remove the linefeed appended after the last row written
1103+
// (if not yet done)
1104+
if (!CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW.enabledIn(_csvFeatures)) {
1105+
_removeTrailingLF();
1106+
}
11001107
_flushBuffer();
11011108
if (autoClose) {
11021109
_out.close();
@@ -1108,6 +1115,15 @@ public void close(boolean autoClose, boolean flushStream) throws IOException
11081115
_releaseBuffers();
11091116
}
11101117

1118+
private void _removeTrailingLF() throws IOException {
1119+
if (!_trailingLFRemoved) {
1120+
_trailingLFRemoved = true;
1121+
// Remove trailing LF if (but only if) it appears to be in output
1122+
// buffer (may not be possible if `flush()` has been called)
1123+
_outputTail = Math.max(0, _outputTail - _cfgLineSeparatorLength);
1124+
}
1125+
}
1126+
11111127
/*
11121128
/**********************************************************
11131129
/* Internal methods

Diff for: csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/CSVGeneratorTest.java

+21-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import com.fasterxml.jackson.core.JsonGenerator;
1111

1212
import com.fasterxml.jackson.core.StreamWriteFeature;
13-
import com.fasterxml.jackson.databind.JsonMappingException;
13+
14+
import com.fasterxml.jackson.databind.DatabindException;
1415
import com.fasterxml.jackson.databind.ObjectWriter;
1516
import com.fasterxml.jackson.databind.node.ObjectNode;
1617
import com.fasterxml.jackson.dataformat.csv.*;
@@ -88,8 +89,14 @@ public void testSimpleExplicit() throws Exception
8889

8990
FiveMinuteUser user = new FiveMinuteUser("Silu", "Seppala", false, Gender.MALE,
9091
new byte[] { 1, 2, 3, 4, 5});
91-
String csv = MAPPER.writer(schema).writeValueAsString(user);
92-
assertEquals("Silu,Seppala,MALE,AQIDBAU=,false\n", csv);
92+
assertEquals("Silu,Seppala,MALE,AQIDBAU=,false\n",
93+
MAPPER.writer(schema).writeValueAsString(user));
94+
95+
// 14-Jan-2024, tatu: [dataformats-text#45] allow suppressing trailing LF:
96+
assertEquals("Silu,Seppala,MALE,AQIDBAU=,false",
97+
MAPPER.writer(schema)
98+
.without(CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW)
99+
.writeValueAsString(user));
93100
}
94101

95102
public void testSimpleWithAutoSchema() throws Exception
@@ -102,10 +109,17 @@ public void testWriteHeaders() throws Exception
102109
{
103110
CsvSchema schema = MAPPER.schemaFor(FiveMinuteUser.class).withHeader();
104111
FiveMinuteUser user = new FiveMinuteUser("Barbie", "Benton", false, Gender.FEMALE, null);
105-
String result = MAPPER.writer(schema).writeValueAsString(user);
106112
assertEquals("firstName,lastName,gender,verified,userImage\n"
107-
+"Barbie,Benton,FEMALE,false,\n", result);
108-
}
113+
+"Barbie,Benton,FEMALE,false,\n",
114+
MAPPER.writer(schema).writeValueAsString(user));
115+
116+
// 14-Jan-2024, tatu: [dataformats-text#45] allow suppressing trailing LF:
117+
assertEquals("firstName,lastName,gender,verified,userImage\n"
118+
+"Barbie,Benton,FEMALE,false,",
119+
MAPPER.writer(schema)
120+
.without(CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW)
121+
.writeValueAsString(user));
122+
}
109123

110124
/**
111125
* Test that verifies that if a header line is needed, configured schema
@@ -118,7 +132,7 @@ public void testFailedWriteHeaders() throws Exception
118132
try {
119133
MAPPER.writer(schema).writeValueAsString(user);
120134
fail("Should fail without columns");
121-
} catch (JsonMappingException e) {
135+
} catch (DatabindException e) {
122136
verifyException(e, "contains no column names");
123137
}
124138
}

Diff for: csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/MultipleWritesTest.java

+22-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
77
import com.fasterxml.jackson.core.JsonGenerator;
88
import com.fasterxml.jackson.databind.*;
9+
import com.fasterxml.jackson.dataformat.csv.CsvGenerator;
910
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
1011
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
1112
import com.fasterxml.jackson.dataformat.csv.ModuleTestBase;
@@ -68,16 +69,30 @@ public void testMultipleListWrites() throws Exception
6869

6970
public void testWriteValuesWithPOJOs() throws Exception
7071
{
71-
CsvSchema schema = MAPPER.schemaFor(Pojo.class).withUseHeader(true);
72+
final CsvSchema schema = MAPPER.schemaFor(Pojo.class).withUseHeader(true);
73+
7274
ObjectWriter writer = MAPPER.writer(schema);
7375
StringWriter sw = new StringWriter();
74-
SequenceWriter seqw = writer.writeValues(sw);
75-
seqw.write(new Pojo(1, 2, 3));
76-
seqw.write(new Pojo(0, 15, 9));
77-
seqw.write(new Pojo(7, 8, 9));
78-
seqw.flush();
76+
try (SequenceWriter seqw = writer.writeValues(sw)) {
77+
seqw.write(new Pojo(1, 2, 3));
78+
seqw.write(new Pojo(0, 15, 9));
79+
seqw.write(new Pojo(7, 8, 9));
80+
}
7981
assertEquals("a,b,c\n1,2,3\n0,15,9\n7,8,9\n",
8082
sw.toString());
81-
seqw.close();
83+
84+
// 14-Jan-2024, tatu: [dataformats-text#45] allow suppressing trailing LF.
85+
// NOTE! Any form of `flush()` will prevent ability to "remove" trailing LF so...
86+
writer = writer
87+
.without(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
88+
.without(CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW);
89+
sw = new StringWriter();
90+
try (SequenceWriter seqw = writer.writeValues(sw)) {
91+
seqw.write(new Pojo(1, 2, 3));
92+
seqw.write(new Pojo(0, 15, 9));
93+
seqw.write(new Pojo(7, 8, 9));
94+
}
95+
assertEquals("a,b,c\n1,2,3\n0,15,9\n7,8,9",
96+
sw.toString());
8297
}
8398
}

Diff for: release-notes/CREDITS-2.x

+6
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,9 @@ Arthur Chan (arthurscchan@github)
250250
* Contributed fix for #445: `YAMLParser` throws unexpected `NullPointerException` in certain
251251
number parsing cases
252252
(2.16.1)
253+
254+
Mathieu Lavigne (@mathieu-lavigne)
255+
256+
* Proposed #45 (and suggested implementation): (csv) Allow skipping ending line break
257+
(`CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW`)
258+
(2.17.0)

Diff for: release-notes/VERSION-2.x

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ Active Maintainers:
1616

1717
2.17.0 (not yet released)
1818

19-
-
19+
#45: (csv) Allow skipping ending line break
20+
(`CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW`)
21+
(proposed by Mathieu L)
2022

2123
2.16.1 (24-Dec-2023)
2224

0 commit comments

Comments
 (0)