Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ public enum CsvWriteFeature
*/
ALWAYS_QUOTE_EMPTY_STRINGS(false),

/**
* Feature that determines whether String values with leading or trailing
* whitespace (any character {@code <= 0x0020}, including space and tab)
* should be forced to be quoted.
* This is useful for interoperability with CSV parsers that trim unquoted
* whitespace.
*<p>
* Default value is {@code false} so that leading/trailing whitespace
* does not by itself trigger quoting.
*
* @since 3.2
*/
QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE(false),

/**
* Feature that determines whether values written as Nymbers (from {@code java.lang.Number}
* valued POJO properties) should be forced to be quoted, regardless of whether they
Expand Down
50 changes: 23 additions & 27 deletions csv/src/main/java/tools/jackson/dataformat/csv/impl/CsvEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,33 +91,36 @@ public class CsvEncoder
*/
protected final int _cfgMinSafeChar;

protected int _csvFeatures;
protected final int _csvFeatures;

/**
* Marker flag used to determine if to do optimal (aka "strict") quoting
* checks or not (looser conservative check)
*/
protected boolean _cfgOptimalQuoting;
protected final boolean _cfgOptimalQuoting;

protected final boolean _cfgAllowsComments;

protected boolean _cfgIncludeMissingTail;
protected final boolean _cfgIncludeMissingTail;

protected boolean _cfgAlwaysQuoteStrings;
protected final boolean _cfgAlwaysQuoteStrings;

protected boolean _cfgAlwaysQuoteEmptyStrings;
protected final boolean _cfgAlwaysQuoteEmptyStrings;

// @since 2.16
protected boolean _cfgAlwaysQuoteNumbers;
protected final boolean _cfgAlwaysQuoteNumbers;

protected boolean _cfgEscapeQuoteCharWithEscapeChar;
// @since 3.2
protected final boolean _cfgQuoteLeadingTrailingWhitespace;

protected boolean _cfgEscapeControlCharWithEscapeChar;
protected final boolean _cfgEscapeQuoteCharWithEscapeChar;

protected final boolean _cfgEscapeControlCharWithEscapeChar;

/**
* @since 2.14
*/
protected boolean _cfgUseFastDoubleWriter;
protected final boolean _cfgUseFastDoubleWriter;

protected final char _cfgQuoteCharEscapeChar;

Expand Down Expand Up @@ -207,6 +210,7 @@ public CsvEncoder(IOContext ctxt, int csvFeatures, Writer out, CsvSchema schema,
_cfgAlwaysQuoteStrings = CsvWriteFeature.ALWAYS_QUOTE_STRINGS.enabledIn(csvFeatures);
_cfgAlwaysQuoteEmptyStrings = CsvWriteFeature.ALWAYS_QUOTE_EMPTY_STRINGS.enabledIn(csvFeatures);
_cfgAlwaysQuoteNumbers = CsvWriteFeature.ALWAYS_QUOTE_NUMBERS.enabledIn(csvFeatures);
_cfgQuoteLeadingTrailingWhitespace = CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE.enabledIn(csvFeatures);
_cfgEscapeQuoteCharWithEscapeChar = CsvWriteFeature.ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR.enabledIn(csvFeatures);
_cfgEscapeControlCharWithEscapeChar = CsvWriteFeature.ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR.enabledIn(csvFeatures);

Expand Down Expand Up @@ -259,6 +263,7 @@ public CsvEncoder(CsvEncoder base, CsvSchema newSchema)
_cfgAlwaysQuoteStrings = base._cfgAlwaysQuoteStrings;
_cfgAlwaysQuoteEmptyStrings = base._cfgAlwaysQuoteEmptyStrings;
_cfgAlwaysQuoteNumbers = base._cfgAlwaysQuoteNumbers;
_cfgQuoteLeadingTrailingWhitespace = base._cfgQuoteLeadingTrailingWhitespace;

_cfgEscapeQuoteCharWithEscapeChar = base._cfgEscapeQuoteCharWithEscapeChar;
_cfgEscapeControlCharWithEscapeChar = base._cfgEscapeControlCharWithEscapeChar;
Expand Down Expand Up @@ -302,8 +307,7 @@ private void _verifyConfiguration(CsvSchema schema)
}
}
}



private final char _getQuoteCharEscapeChar(
final boolean escapeQuoteCharWithEscapeChar,
final int quoteCharacter,
Expand Down Expand Up @@ -342,22 +346,6 @@ public CsvEncoder withSchema(CsvSchema schema) {
return new CsvEncoder(this, schema);
}

/*
public CsvEncoder overrideFormatFeatures(int feat) {
if (feat != _csvFeatures) {
_csvFeatures = feat;
_cfgOptimalQuoting = CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING.enabledIn(feat);
_cfgIncludeMissingTail = !CsvGenerator.Feature.OMIT_MISSING_TAIL_COLUMNS.enabledIn(feat);
_cfgAlwaysQuoteStrings = CsvGenerator.Feature.ALWAYS_QUOTE_STRINGS.enabledIn(feat);
_cfgAlwaysQuoteEmptyStrings = CsvGenerator.Feature.ALWAYS_QUOTE_EMPTY_STRINGS.enabledIn(feat);
_cfgAlwaysQuoteNumbers = CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS.enabledIn(feat);
_cfgEscapeQuoteCharWithEscapeChar = CsvGenerator.Feature.ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR.enabledIn(feat);
_cfgEscapeControlCharWithEscapeChar = Feature.ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR.enabledIn(feat);
}
return this;
}
*/

public CsvEncoder setOutputEscapes(int[] esc) {
_outputEscapes = (esc != null) ? esc : sOutputEscapes;
return this;
Expand Down Expand Up @@ -1156,6 +1144,14 @@ protected boolean _mayNeedQuotes(String value, int length, int columnIndex)
if (_cfgQuoteCharacter < 0) {
return false;
}
// [dataformats-text#210]: check for leading/trailing whitespace
if (_cfgQuoteLeadingTrailingWhitespace && length > 0) {
char first = value.charAt(0);
char last = value.charAt(length - 1);
if (first <= ' ' || last <= ' ') {
return true;
}
}
// may skip checks unless we want exact checking
if (_cfgOptimalQuoting) {
// 31-Dec-2014, tatu: Comment lines start with # so quote if starts with #
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package tools.jackson.dataformat.csv.ser;

import org.junit.jupiter.api.Test;

import tools.jackson.dataformat.csv.*;

import static org.junit.jupiter.api.Assertions.assertEquals;

// [dataformats-text#210]: Quote strings with leading/trailing whitespace
public class QuoteLeadingTrailingWhitespace210Test extends ModuleTestBase
{
private final CsvMapper MAPPER = mapperForCsv();

@Test
public void testLeadingSpaceQuoted() throws Exception
{
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
.withLineSeparator("\n");
String csv = MAPPER.writer(schema)
.with(CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE)
.writeValueAsString(new IdDesc(" hello", "world"));
assertEquals("\" hello\",world\n", csv);
}

@Test
public void testTrailingSpaceQuoted() throws Exception
{
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
.withLineSeparator("\n");
String csv = MAPPER.writer(schema)
.with(CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE)
.writeValueAsString(new IdDesc("hello", "world "));
assertEquals("hello,\"world \"\n", csv);
}

@Test
public void testBothLeadingAndTrailingSpaceQuoted() throws Exception
{
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
.withLineSeparator("\n");
String csv = MAPPER.writer(schema)
.with(CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE)
.writeValueAsString(new IdDesc(" both ", " ends "));
assertEquals("\" both \",\" ends \"\n", csv);
}

@Test
public void testTabCountsAsWhitespace() throws Exception
{
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
.withLineSeparator("\n");
String csv = MAPPER.writer(schema)
.with(CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE)
.writeValueAsString(new IdDesc("\thello", "world"));
assertEquals("\"\thello\",world\n", csv);
}

@Test
public void testNoQuotingWithoutFeature() throws Exception
{
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
.withLineSeparator("\n");
// With strict quoting (optimal), leading spaces should NOT trigger quoting
// unless the new feature is enabled
String csv = MAPPER.writer(schema)
.with(CsvWriteFeature.STRICT_CHECK_FOR_QUOTING)
.without(CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE)
.writeValueAsString(new IdDesc(" hello", "world "));
assertEquals(" hello,world \n", csv);
}

@Test
public void testNoWhitespaceNoQuoting() throws Exception
{
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
.withLineSeparator("\n");
// Strings without leading/trailing whitespace should not be affected
String csv = MAPPER.writer(schema)
.with(CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE)
.with(CsvWriteFeature.STRICT_CHECK_FOR_QUOTING)
.writeValueAsString(new IdDesc("hello", "world"));
assertEquals("hello,world\n", csv);
}
}
4 changes: 4 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ implementations)
#109: (yaml) `YAMLMapper` doesn't close output file if an error occurs
(reported by Martin W)
(fix by @cowtowncoder, w/ Claude code)
#210: (csv) Quote strings with leading/trailing spaces in csv
(`CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE`)
(requested by @sfzhi)
(implemented by @cowtowncoder, w/ Claude code)
#214: (yaml) Expose custom tags via `YAMLParser.getRawTag()`
(requested by Alexander T)
(fix by @cowtowncoder, w/ Claude code)
Expand Down