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 6d17bde4..f8f8aff8 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 @@ -221,6 +221,14 @@ private Feature(boolean defaultState) { */ protected int _nextColumnByName = -1; + /** + * Decorator to use for decorating the column value to follow, if any; + * {@code null} if none. + * + * @since 2.18 + */ + protected CsvValueDecorator _nextColumnDecorator; + /** * Flag set when property to write is unknown, and the matching value * is to be skipped quietly. @@ -461,14 +469,16 @@ private final void _writeFieldName(String name) throws IOException if (_skipWithin != null) { // new in 2.7 _skipValue = true; _nextColumnByName = -1; + _nextColumnDecorator = null; return; } // note: we are likely to get next column name, so pass it as hint CsvSchema.Column col = _schema.column(name, _nextColumnByName+1); if (col == null) { + _nextColumnByName = -1; + _nextColumnDecorator = null; if (isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { _skipValue = true; - _nextColumnByName = -1; return; } // not a low-level error, so: @@ -477,6 +487,7 @@ private final void _writeFieldName(String name) throws IOException _skipValue = false; // and all we do is just note index to use for following value write _nextColumnByName = col.getIndex(); + _nextColumnDecorator = col.getValueDecorator(); } /* @@ -606,8 +617,13 @@ public final void writeEndArray() throws IOException return; } if (!_arraySeparator.isEmpty()) { + String value = _arrayContents.toString(); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations! + if (_nextColumnDecorator != null) { + value = _nextColumnDecorator.decorateValue(this, value); + } _arraySeparator = CsvSchema.NO_ARRAY_ELEMENT_SEPARATOR; - _writer.write(_columnIndex(), _arrayContents.toString()); + _writer.write(_columnIndex(), value); } // 20-Nov-2014, tatu: When doing "untyped"/"raw" output, this means that row // is now done. But not if writing such an array field, so: @@ -673,6 +689,10 @@ public void writeString(String text) throws IOException if (!_arraySeparator.isEmpty()) { _addToArray(text); } else { + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations! + if (_nextColumnDecorator != null) { + text = _nextColumnDecorator.decorateValue(this, text); + } _writer.write(_columnIndex(), text); } } @@ -685,6 +705,11 @@ public void writeString(char[] text, int offset, int len) throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(new String(text, offset, len)); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations! + } else if (_nextColumnDecorator != null) { + String str = new String(text, offset, len); + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, str)); } else { _writer.write(_columnIndex(), text, offset, len); } @@ -699,7 +724,12 @@ public final void writeString(SerializableString sstr) throws IOException if (!_arraySeparator.isEmpty()) { _addToArray(sstr.getValue()); } else { - _writer.write(_columnIndex(), sstr.getValue()); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations! + String text = sstr.getValue(); + if (_nextColumnDecorator != null) { + text = _nextColumnDecorator.decorateValue(this, text); + } + _writer.write(_columnIndex(), text); } } } @@ -780,6 +810,7 @@ public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int l writeNull(); return; } + _verifyValueWrite("write Binary value"); if (!_skipValue) { // ok, better just Base64 encode as a String... @@ -791,6 +822,10 @@ public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int l if (!_arraySeparator.isEmpty()) { _addToArray(encoded); } else { + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations! + if (_nextColumnDecorator != null) { + encoded = _nextColumnDecorator.decorateValue(this, encoded); + } _writer.write(_columnIndex(), encoded); } } @@ -810,7 +845,13 @@ public void writeBoolean(boolean state) throws IOException if (!_arraySeparator.isEmpty()) { _addToArray(state ? "true" : "false"); } else { - _writer.write(_columnIndex(), state); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations! + if (_nextColumnDecorator != null) { + String text = _nextColumnDecorator.decorateValue(this, state ? "true" : "false"); + _writer.write(_columnIndex(), text); + } else { + _writer.write(_columnIndex(), state); + } } } } @@ -824,6 +865,15 @@ public void writeNull() throws IOException if (!_arraySeparator.isEmpty()) { _addToArray(_schema.getNullValueOrEmpty()); } else if (_tokenWriteContext.inObject()) { + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + if (_nextColumnDecorator != null) { + String nvl = _nextColumnDecorator.decorateNull(this); + if (nvl != null) { + _writer.write(_columnIndex(), nvl); + return; + } + } + _writer.writeNull(_columnIndex()); } else if (_tokenWriteContext.inArray()) { // [dataformat-csv#106]: Need to make sure we don't swallow nulls in arrays either @@ -833,6 +883,14 @@ public void writeNull() throws IOException // based on either schema property, or CsvGenerator.Feature. // Note: if nulls are to be written that way, would need to call `finishRow()` right after `writeNull()` if (!_tokenWriteContext.getParent().inRoot()) { + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + if (_nextColumnDecorator != null) { + String nvl = _nextColumnDecorator.decorateNull(this); + if (nvl != null) { + _writer.write(_columnIndex(), nvl); + return; + } + } _writer.writeNull(_columnIndex()); } @@ -852,6 +910,10 @@ public void writeNumber(int v) throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(String.valueOf(v)); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + } else if (_nextColumnDecorator != null) { + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, String.valueOf(v))); } else { _writer.write(_columnIndex(), v); } @@ -870,6 +932,10 @@ public void writeNumber(long v) throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(String.valueOf(v)); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + } else if (_nextColumnDecorator != null) { + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, String.valueOf(v))); } else { _writer.write(_columnIndex(), v); } @@ -887,6 +953,10 @@ public void writeNumber(BigInteger v) throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(String.valueOf(v)); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + } else if (_nextColumnDecorator != null) { + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, String.valueOf(v))); } else { _writer.write(_columnIndex(), v); @@ -901,6 +971,10 @@ public void writeNumber(double v) throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(String.valueOf(v)); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + } else if (_nextColumnDecorator != null) { + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, String.valueOf(v))); } else { _writer.write(_columnIndex(), v); } @@ -914,6 +988,10 @@ public void writeNumber(float v) throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(String.valueOf(v)); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + } else if (_nextColumnDecorator != null) { + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, String.valueOf(v))); } else { _writer.write(_columnIndex(), v); } @@ -932,6 +1010,11 @@ public void writeNumber(BigDecimal v) throws IOException boolean plain = isEnabled(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); if (!_arraySeparator.isEmpty()) { _addToArray(plain ? v.toPlainString() : v.toString()); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + } else if (_nextColumnDecorator != null) { + String numStr = plain ? v.toPlainString() : v.toString(); + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, numStr)); } else { _writer.write(_columnIndex(), v, plain); } @@ -949,6 +1032,10 @@ public void writeNumber(String encodedValue) throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(encodedValue); + // 26-Aug-2024, tatu: [dataformats-text#495] Decorations? + } else if (_nextColumnDecorator != null) { + _writer.write(_columnIndex(), + _nextColumnDecorator.decorateValue(this, encodedValue)); } else { _writer.write(_columnIndex(), encodedValue); } diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorator.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorator.java index 7f28924d..d7df0146 100644 --- a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorator.java +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorator.java @@ -26,7 +26,8 @@ public interface CsvValueDecorator * @param gen Generator that will be used for actual serialization * @param plainValue Value to decorate * - * @return Decorated value (which may be {@code plainValue} as-is) + * @return Decorated value (which may be {@code plainValue} as-is) but + * Must Not be {@code null} * * @throws IOException if attempt to decorate the value somehow fails * (typically a {@link com.fasterxml.jackson.core.exc.StreamWriteException}) @@ -34,6 +35,28 @@ public interface CsvValueDecorator public String decorateValue(CsvGenerator gen, String plainValue) throws IOException; + /** + * Method called instead of {@link #decorateValue} in case where value being + * written is from Java {@code null} value: this is often left as-is, without + * decoration (and this is the default implementation), but may be + * decorated. + * To let default Null Value Replacement be used, should return {@code null}: + * this is the default implementation. + * + * @param gen Generator that will be used for actual serialization + * + * @return Decorated value to use, IF NOT {@code null}: if {@code null} will use + * default null replacement value. + * + * @throws IOException if attempt to decorate the value somehow fails + * (typically a {@link com.fasterxml.jackson.core.exc.StreamWriteException}) + */ + public default String decorateNull(CsvGenerator gen) + throws IOException + { + return null; + } + /** * Method called during deserialization, to remove possible decoration * applied with {@link #decorateValue}. diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorators.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorators.java index 75dbd635..2828ea22 100644 --- a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorators.java +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorators.java @@ -25,7 +25,23 @@ public class CsvValueDecorators */ public final static CsvValueDecorator STRICT_BRACKETS_DECORATOR = new StringPrefixSuffixDecorator("[", "]", false); - + + /** + * Factory method for constructing a {@link StringPrefixSuffixDecorator} with + * given prefix and suffix, both optional. + */ + public static CsvValueDecorator optionalPrefixSuffixDecorator(String prefix, String suffix) { + return new StringPrefixSuffixDecorator(prefix, suffix, true); + } + + /** + * Factory method for constructing a {@link StringPrefixSuffixDecorator} with + * given prefix and suffix, both required. + */ + public static CsvValueDecorator requiredPrefixSuffixDecorator(String prefix, String suffix) { + return new StringPrefixSuffixDecorator(prefix, suffix, false); + } + /** * Decorated that adds static prefix and suffix around value to decorate value; * removes the same when un-decorating. Handling of the case where decoration diff --git a/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/failing/WriteBracketedArray495Test.java b/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/WriteBracketedArray495Test.java similarity index 60% rename from csv/src/test/java/com/fasterxml/jackson/dataformat/csv/failing/WriteBracketedArray495Test.java rename to csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/WriteBracketedArray495Test.java index e119e9ca..2a0309a3 100644 --- a/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/failing/WriteBracketedArray495Test.java +++ b/csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/WriteBracketedArray495Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.dataformat.csv.failing; +package com.fasterxml.jackson.dataformat.csv.ser; import java.io.StringWriter; @@ -16,17 +16,19 @@ // [dataformats-text#495] public class WriteBracketedArray495Test extends ModuleTestBase { - // [dataformats-text#495] - @JsonPropertyOrder({"id", "embeddings", "title" }) + // [dataformats-text#495]: + @JsonPropertyOrder({"id", "embeddings", "title", "extra" }) static class Article { public int id; public String title; public double[] embeddings; + public int extra; protected Article() { } - public Article(int id, String title, double[] embeddings) { + public Article(int id, String title, int extra, double[] embeddings) { this.id = id; this.title = title; + this.extra = extra; this.embeddings = embeddings; } } @@ -42,48 +44,61 @@ public Article(int id, String title, double[] embeddings) { // [dataformats-text#495] public void testBracketsWriteAutoSchema() throws Exception { - final CsvSchema schema = _automaticSchema(true); + final CsvSchema schema = _automaticSchema(); _testArrayWithBracketsWrite(schema); } public void testBracketsManualSchemaArray() throws Exception { - final CsvSchema schema = _manualSchema(ColumnType.ARRAY, true); + final CsvSchema schema = _manualSchema(ColumnType.ARRAY); _testArrayWithBracketsWrite(schema); } public void testBracketsManualSchemaString() throws Exception { - final CsvSchema schema = _manualSchema(ColumnType.STRING, true); + final CsvSchema schema = _manualSchema(ColumnType.STRING); _testArrayWithBracketsWrite(schema); } - private CsvSchema _automaticSchema(boolean required) + private CsvSchema _automaticSchema() { return MAPPER.schemaFor(Article.class) .withHeader() .withArrayElementSeparator(",") .withColumn("embeddings", - col -> col.withValueDecorator(_bracketDecorator(required))); + col -> col.withValueDecorator(_bracketDecorator())) + .withColumn("title", + col -> col.withValueDecorator(_parenthesisDecorator())) + .withColumn("extra", + col -> col.withValueDecorator(_curlyDecorator())); } - private CsvSchema _manualSchema(ColumnType ct, boolean required) + private CsvSchema _manualSchema(ColumnType ct) { return CsvSchema.builder() .setUseHeader(true) .setArrayElementSeparator(",") - .addColumn("id", ColumnType.STRING) + .addColumn("id", ColumnType.NUMBER) // and then the interesting one; may mark as "String" or "Array" .addColumn("embeddings", ct, - col -> col.withValueDecorator(_bracketDecorator(required))) - .addColumn("title", ColumnType.STRING) + col -> col.withValueDecorator(_bracketDecorator())) + .addColumn("title", ColumnType.STRING, + col -> col.withValueDecorator(_parenthesisDecorator())) + .addColumn("extra", ColumnType.NUMBER, + col -> col.withValueDecorator(_curlyDecorator())) .build(); } - private CsvValueDecorator _bracketDecorator(boolean required) { - return required - ? CsvValueDecorators.STRICT_BRACKETS_DECORATOR - : CsvValueDecorators.OPTIONAL_BRACKETS_DECORATOR; + private CsvValueDecorator _bracketDecorator() { + return CsvValueDecorators.STRICT_BRACKETS_DECORATOR; + } + + private CsvValueDecorator _parenthesisDecorator() { + return CsvValueDecorators.requiredPrefixSuffixDecorator("(", ")"); + } + + private CsvValueDecorator _curlyDecorator() { + return CsvValueDecorators.requiredPrefixSuffixDecorator("{", "}"); } private void _testArrayWithBracketsWrite(CsvSchema schema) throws Exception @@ -93,11 +108,11 @@ private void _testArrayWithBracketsWrite(CsvSchema schema) throws Exception .with(schema) .writeValues(stringW); - sw.write(new Article(123, "Title!", new double[] { 0.5, -0.25, 2.5 })); + sw.write(new Article(123, "Title!", 42, new double[] { 0.5, -0.25, 2.5 })); sw.close(); - assertEquals("id,embeddings,title\n" - +"123,\"[0.5,-0.25,2.5]\",\"Title!\"", + assertEquals("id,embeddings,title,extra\n" + +"123,\"[0.5,-0.25,2.5]\",\"(Title!)\",{42}", stringW.toString().trim()); } } diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 262cf795..8c199e36 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -18,7 +18,6 @@ Active Maintainers: #442: (csv) Allow use of "value decorators" (like `[` and `]` for arrays) for reading `CsvSchema` columns - #468: (csv) Remove synchronization from `CsvMapper` (contributed by @pjfanning) #469: (csv) Allow CSV to differentiate between `null` and empty @@ -30,6 +29,7 @@ Active Maintainers: (reported by @RafeArnold) #485: (csv) CSVDecoder: No Long and Int out of range exceptions (reported by Burdyug P) +#495: (csv) Support use of `CsvValueDecorator` for writing CSV column values 2.17.2 (05-Jul-2024)