From e9fa1f0599ee6f74533c64000507ceddd327d46b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 18 Oct 2023 18:56:04 -0700 Subject: [PATCH] Fix #438: prevent quoting of BigInteger/BigDecimal when not expected --- .../jackson/dataformat/csv/CsvGenerator.java | 11 ++--- .../dataformat/csv/impl/BufferedValue.java | 27 ++++++++++++ .../dataformat/csv/impl/CsvEncoder.java | 44 ++++++++++++++++++- .../dataformat/csv/ser/CSVGeneratorTest.java | 34 ++++++++++++++ release-notes/VERSION-2.x | 2 + 5 files changed, 112 insertions(+), 6 deletions(-) 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 007854d5..90608a0e 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 @@ -63,6 +63,8 @@ public enum Feature * actually need this. * Note that this feature has precedence over {@link #STRICT_CHECK_FOR_QUOTING}, when * both would be applicable. + * Note that this setting does NOT affect quoting of typed values like {@code Number}s + * or {@code Boolean}s. * * @since 2.5 */ @@ -861,7 +863,7 @@ public void writeNumber(BigInteger v) throws IOException if (!_arraySeparator.isEmpty()) { _addToArray(String.valueOf(v)); } else { - _writer.write(_columnIndex(), v.toString()); + _writer.write(_columnIndex(), v); } } @@ -902,12 +904,11 @@ public void writeNumber(BigDecimal v) throws IOException } _verifyValueWrite("write number"); if (!_skipValue) { - String str = isEnabled(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN) - ? v.toPlainString() : v.toString(); + boolean plain = isEnabled(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); if (!_arraySeparator.isEmpty()) { - _addToArray(String.valueOf(v)); + _addToArray(plain ? v.toPlainString() : v.toString()); } else { - _writer.write(_columnIndex(), str); + _writer.write(_columnIndex(), v, plain); } } } diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/BufferedValue.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/BufferedValue.java index 0c3936ad..95596aa3 100644 --- a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/BufferedValue.java +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/BufferedValue.java @@ -16,7 +16,9 @@ protected BufferedValue() { } public static BufferedValue bufferedRaw(String v) { return new RawValue(v); } public static BufferedValue buffered(int v) { return new IntValue(v); } public static BufferedValue buffered(long v) { return new LongValue(v); } + public static BufferedValue buffered(float v) { return new FloatValue(v); } public static BufferedValue buffered(double v) { return new DoubleValue(v); } + public static BufferedValue bufferedNumber(String numStr) { return new BigNumberValue(numStr); } public static BufferedValue buffered(boolean v) { return v ? BooleanValue.TRUE : BooleanValue.FALSE; } @@ -76,6 +78,19 @@ public void write(CsvEncoder w) throws IOException { } } + // @since 2.16 + protected final static class FloatValue extends BufferedValue + { + private final float _value; + + public FloatValue(float v) { _value = v; } + + @Override + public void write(CsvEncoder w) throws IOException { + w.appendValue(_value); + } + } + protected final static class DoubleValue extends BufferedValue { private final double _value; @@ -88,6 +103,18 @@ public void write(CsvEncoder w) throws IOException { } } + protected final static class BigNumberValue extends BufferedValue + { + private final String _value; + + public BigNumberValue(String v) { _value = v; } + + @Override + public void write(CsvEncoder w) throws IOException { + w.appendNumberValue(_value); + } + } + protected final static class BooleanValue extends BufferedValue { public final static BooleanValue FALSE = new BooleanValue(false); 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 eee1f98f..ce52d25f 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 @@ -9,6 +9,8 @@ import java.io.IOException; import java.io.Writer; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Arrays; /** @@ -438,6 +440,19 @@ public final void write(int columnIndex, long value) throws IOException _buffer(columnIndex, BufferedValue.buffered(value)); } + // @since 2.16 + public final void write(int columnIndex, BigInteger value) throws IOException + { + // easy case: all in order + final String numStr = value.toString(); + if (columnIndex == _nextColumnToWrite) { + appendNumberValue(numStr); + ++_nextColumnToWrite; + return; + } + _buffer(columnIndex, BufferedValue.bufferedNumber(numStr)); + } + public final void write(int columnIndex, float value) throws IOException { // easy case: all in order @@ -460,6 +475,20 @@ public final void write(int columnIndex, double value) throws IOException _buffer(columnIndex, BufferedValue.buffered(value)); } + // @since 2.16 + public final void write(int columnIndex, BigDecimal value, boolean plain) throws IOException + { + final String numStr = plain ? value.toPlainString() : value.toString(); + + // easy case: all in order + if (columnIndex == _nextColumnToWrite) { + appendNumberValue(numStr); + ++_nextColumnToWrite; + return; + } + _buffer(columnIndex, BufferedValue.bufferedNumber(numStr)); + } + public final void write(int columnIndex, boolean value) throws IOException { // easy case: all in order @@ -599,7 +628,7 @@ protected void appendValue(long value) throws IOException } _outputTail = NumberOutput.outputLong(value, _outputBuffer, _outputTail); } - + protected void appendValue(float value) throws IOException { String str = NumberOutput.toString(value, _cfgUseFastDoubleWriter); @@ -626,6 +655,19 @@ protected void appendValue(double value) throws IOException writeRaw(str); } + // @since 2.16: pre-encoded BigInteger/BigDecimal value + protected void appendNumberValue(String numValue) throws IOException + { + // Same as "appendRawValue()", except may want quoting + if (_outputTail >= _outputEnd) { + _flushBuffer(); + } + if (_nextColumnToWrite > 0) { + appendColumnSeparator(); + } + writeRaw(numValue); + } + protected void appendValue(boolean value) throws IOException { _append(value ? TRUE_CHARS : FALSE_CHARS); } 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 7c817da6..e30e5e7e 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 @@ -2,6 +2,7 @@ import java.io.File; import java.io.StringWriter; +import java.math.BigDecimal; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -37,6 +38,19 @@ public Entry2(String id, float amount) { } } + @JsonPropertyOrder({"id", "amount", "enabled"}) + static class Entry3 { + public String id; + public BigDecimal amount; + public boolean enabled; + + public Entry3(String id, BigDecimal amount, boolean enabled) { + this.id = id; + this.amount = amount; + this.enabled = enabled; + } + } + /* /********************************************************************** /* Test methods @@ -209,6 +223,26 @@ public void testForcedQuoting60() throws Exception assertEquals("xyz,2.5\n", result); } + // [dataformats-csv#438]: Should not quote BigInteger/BigDecimal (or booleans) + public void testForcedQuotingOfBigDecimal() throws Exception + { + CsvSchema schema = CsvSchema.builder() + .addColumn("id") + .addColumn("amount") + .addColumn("enabled") + .build(); + String result = MAPPER.writer(schema) + .with(CsvGenerator.Feature.ALWAYS_QUOTE_STRINGS) + .writeValueAsString(new Entry3("abc", BigDecimal.valueOf(2.5), true)); + assertEquals("\"abc\",2.5,true\n", result); + + // Also, as per [dataformat-csv#81], should be possible to change dynamically + result = MAPPER.writer(schema) + .without(CsvGenerator.Feature.ALWAYS_QUOTE_STRINGS) + .writeValueAsString(new Entry3("xyz", BigDecimal.valueOf(1.5), false)); + assertEquals("xyz,1.5,false\n", result); + } + public void testForcedQuotingWithQuoteEscapedWithBackslash() throws Exception { CsvSchema schema = CsvSchema.builder() diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 8fd8726f..d07de3a7 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -20,6 +20,8 @@ Active Maintainers: #435: (yaml) Minor parsing validation miss: tagged as `int`, exception on underscore-only values #437: (yaml) Update SnakeYAML dependency to 2.2 +#438: (csv) `BigInteger` and `BigDecimal` are quoted if + `CsvGenerator.Feature.ALWAYS_QUOTE_STRINGS` enabled 2.15.3 (12-Oct-2023)