Skip to content

Commit

Permalink
Fix #45: Add CsvGenerator.Feature.WRITE_LINEFEED_AFTER_LAST_ROW
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Jan 15, 2024
1 parent 61c2f1f commit 334e7ae
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* 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.
*<p>
* 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**********************************************************
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
6 changes: 6 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 3 additions & 1 deletion release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 334e7ae

Please sign in to comment.