Skip to content

Commit c44fb3e

Browse files
committed
Fix #495: support value decoration on writing too
1 parent bb06df1 commit c44fb3e

File tree

4 files changed

+135
-15
lines changed

4 files changed

+135
-15
lines changed

csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvGenerator.java

+92-4
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ private Feature(boolean defaultState) {
221221
*/
222222
protected int _nextColumnByName = -1;
223223

224+
/**
225+
* Decorator to use for decorating the column value to follow, if any;
226+
* {@code null} if none.
227+
*
228+
* @since 2.18
229+
*/
230+
protected CsvValueDecorator _nextColumnDecorator;
231+
224232
/**
225233
* Flag set when property to write is unknown, and the matching value
226234
* is to be skipped quietly.
@@ -461,14 +469,16 @@ private final void _writeFieldName(String name) throws IOException
461469
if (_skipWithin != null) { // new in 2.7
462470
_skipValue = true;
463471
_nextColumnByName = -1;
472+
_nextColumnDecorator = null;
464473
return;
465474
}
466475
// note: we are likely to get next column name, so pass it as hint
467476
CsvSchema.Column col = _schema.column(name, _nextColumnByName+1);
468477
if (col == null) {
478+
_nextColumnByName = -1;
479+
_nextColumnDecorator = null;
469480
if (isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) {
470481
_skipValue = true;
471-
_nextColumnByName = -1;
472482
return;
473483
}
474484
// not a low-level error, so:
@@ -477,6 +487,7 @@ private final void _writeFieldName(String name) throws IOException
477487
_skipValue = false;
478488
// and all we do is just note index to use for following value write
479489
_nextColumnByName = col.getIndex();
490+
_nextColumnDecorator = col.getValueDecorator();
480491
}
481492

482493
/*
@@ -606,8 +617,13 @@ public final void writeEndArray() throws IOException
606617
return;
607618
}
608619
if (!_arraySeparator.isEmpty()) {
620+
String value = _arrayContents.toString();
621+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations!
622+
if (_nextColumnDecorator != null) {
623+
value = _nextColumnDecorator.decorateValue(this, value);
624+
}
609625
_arraySeparator = CsvSchema.NO_ARRAY_ELEMENT_SEPARATOR;
610-
_writer.write(_columnIndex(), _arrayContents.toString());
626+
_writer.write(_columnIndex(), value);
611627
}
612628
// 20-Nov-2014, tatu: When doing "untyped"/"raw" output, this means that row
613629
// is now done. But not if writing such an array field, so:
@@ -673,6 +689,10 @@ public void writeString(String text) throws IOException
673689
if (!_arraySeparator.isEmpty()) {
674690
_addToArray(text);
675691
} else {
692+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations!
693+
if (_nextColumnDecorator != null) {
694+
text = _nextColumnDecorator.decorateValue(this, text);
695+
}
676696
_writer.write(_columnIndex(), text);
677697
}
678698
}
@@ -681,6 +701,12 @@ public void writeString(String text) throws IOException
681701
@Override
682702
public void writeString(char[] text, int offset, int len) throws IOException
683703
{
704+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations!
705+
if (_nextColumnDecorator != null) {
706+
writeString(new String(text, offset, len));
707+
return;
708+
}
709+
684710
_verifyValueWrite("write String value");
685711
if (!_skipValue) {
686712
if (!_arraySeparator.isEmpty()) {
@@ -699,7 +725,12 @@ public final void writeString(SerializableString sstr) throws IOException
699725
if (!_arraySeparator.isEmpty()) {
700726
_addToArray(sstr.getValue());
701727
} else {
702-
_writer.write(_columnIndex(), sstr.getValue());
728+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations!
729+
String text = sstr.getValue();
730+
if (_nextColumnDecorator != null) {
731+
text = _nextColumnDecorator.decorateValue(this, text);
732+
}
733+
_writer.write(_columnIndex(), text);
703734
}
704735
}
705736
}
@@ -780,6 +811,7 @@ public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int l
780811
writeNull();
781812
return;
782813
}
814+
783815
_verifyValueWrite("write Binary value");
784816
if (!_skipValue) {
785817
// ok, better just Base64 encode as a String...
@@ -791,6 +823,10 @@ public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int l
791823
if (!_arraySeparator.isEmpty()) {
792824
_addToArray(encoded);
793825
} else {
826+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations!
827+
if (_nextColumnDecorator != null) {
828+
encoded = _nextColumnDecorator.decorateValue(this, encoded);
829+
}
794830
_writer.write(_columnIndex(), encoded);
795831
}
796832
}
@@ -810,7 +846,13 @@ public void writeBoolean(boolean state) throws IOException
810846
if (!_arraySeparator.isEmpty()) {
811847
_addToArray(state ? "true" : "false");
812848
} else {
813-
_writer.write(_columnIndex(), state);
849+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations!
850+
if (_nextColumnDecorator != null) {
851+
String text = _nextColumnDecorator.decorateValue(this, state ? "true" : "false");
852+
_writer.write(_columnIndex(), text);
853+
} else {
854+
_writer.write(_columnIndex(), state);
855+
}
814856
}
815857
}
816858
}
@@ -824,6 +866,15 @@ public void writeNull() throws IOException
824866
if (!_arraySeparator.isEmpty()) {
825867
_addToArray(_schema.getNullValueOrEmpty());
826868
} else if (_tokenWriteContext.inObject()) {
869+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
870+
if (_nextColumnDecorator != null) {
871+
String nvl = _nextColumnDecorator.decorateNull(this);
872+
if (nvl != null) {
873+
_writer.write(_columnIndex(), nvl);
874+
return;
875+
}
876+
}
877+
827878
_writer.writeNull(_columnIndex());
828879
} else if (_tokenWriteContext.inArray()) {
829880
// [dataformat-csv#106]: Need to make sure we don't swallow nulls in arrays either
@@ -833,6 +884,14 @@ public void writeNull() throws IOException
833884
// based on either schema property, or CsvGenerator.Feature.
834885
// Note: if nulls are to be written that way, would need to call `finishRow()` right after `writeNull()`
835886
if (!_tokenWriteContext.getParent().inRoot()) {
887+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
888+
if (_nextColumnDecorator != null) {
889+
String nvl = _nextColumnDecorator.decorateNull(this);
890+
if (nvl != null) {
891+
_writer.write(_columnIndex(), nvl);
892+
return;
893+
}
894+
}
836895
_writer.writeNull(_columnIndex());
837896
}
838897

@@ -852,6 +911,10 @@ public void writeNumber(int v) throws IOException
852911
if (!_skipValue) {
853912
if (!_arraySeparator.isEmpty()) {
854913
_addToArray(String.valueOf(v));
914+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
915+
} else if (_nextColumnDecorator != null) {
916+
_writer.write(_columnIndex(),
917+
_nextColumnDecorator.decorateValue(this, String.valueOf(v)));
855918
} else {
856919
_writer.write(_columnIndex(), v);
857920
}
@@ -870,6 +933,10 @@ public void writeNumber(long v) throws IOException
870933
if (!_skipValue) {
871934
if (!_arraySeparator.isEmpty()) {
872935
_addToArray(String.valueOf(v));
936+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
937+
} else if (_nextColumnDecorator != null) {
938+
_writer.write(_columnIndex(),
939+
_nextColumnDecorator.decorateValue(this, String.valueOf(v)));
873940
} else {
874941
_writer.write(_columnIndex(), v);
875942
}
@@ -887,6 +954,10 @@ public void writeNumber(BigInteger v) throws IOException
887954
if (!_skipValue) {
888955
if (!_arraySeparator.isEmpty()) {
889956
_addToArray(String.valueOf(v));
957+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
958+
} else if (_nextColumnDecorator != null) {
959+
_writer.write(_columnIndex(),
960+
_nextColumnDecorator.decorateValue(this, String.valueOf(v)));
890961
} else {
891962
_writer.write(_columnIndex(), v);
892963

@@ -901,6 +972,10 @@ public void writeNumber(double v) throws IOException
901972
if (!_skipValue) {
902973
if (!_arraySeparator.isEmpty()) {
903974
_addToArray(String.valueOf(v));
975+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
976+
} else if (_nextColumnDecorator != null) {
977+
_writer.write(_columnIndex(),
978+
_nextColumnDecorator.decorateValue(this, String.valueOf(v)));
904979
} else {
905980
_writer.write(_columnIndex(), v);
906981
}
@@ -914,6 +989,10 @@ public void writeNumber(float v) throws IOException
914989
if (!_skipValue) {
915990
if (!_arraySeparator.isEmpty()) {
916991
_addToArray(String.valueOf(v));
992+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
993+
} else if (_nextColumnDecorator != null) {
994+
_writer.write(_columnIndex(),
995+
_nextColumnDecorator.decorateValue(this, String.valueOf(v)));
917996
} else {
918997
_writer.write(_columnIndex(), v);
919998
}
@@ -932,6 +1011,11 @@ public void writeNumber(BigDecimal v) throws IOException
9321011
boolean plain = isEnabled(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
9331012
if (!_arraySeparator.isEmpty()) {
9341013
_addToArray(plain ? v.toPlainString() : v.toString());
1014+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
1015+
} else if (_nextColumnDecorator != null) {
1016+
String numStr = plain ? v.toPlainString() : v.toString();
1017+
_writer.write(_columnIndex(),
1018+
_nextColumnDecorator.decorateValue(this, numStr));
9351019
} else {
9361020
_writer.write(_columnIndex(), v, plain);
9371021
}
@@ -949,6 +1033,10 @@ public void writeNumber(String encodedValue) throws IOException
9491033
if (!_skipValue) {
9501034
if (!_arraySeparator.isEmpty()) {
9511035
_addToArray(encodedValue);
1036+
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations?
1037+
} else if (_nextColumnDecorator != null) {
1038+
_writer.write(_columnIndex(),
1039+
_nextColumnDecorator.decorateValue(this, encodedValue));
9521040
} else {
9531041
_writer.write(_columnIndex(), encodedValue);
9541042
}

csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvValueDecorator.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,37 @@ public interface CsvValueDecorator
2626
* @param gen Generator that will be used for actual serialization
2727
* @param plainValue Value to decorate
2828
*
29-
* @return Decorated value (which may be {@code plainValue} as-is)
29+
* @return Decorated value (which may be {@code plainValue} as-is) but
30+
* Must Not be {@code null}
3031
*
3132
* @throws IOException if attempt to decorate the value somehow fails
3233
* (typically a {@link com.fasterxml.jackson.core.exc.StreamWriteException})
3334
*/
3435
public String decorateValue(CsvGenerator gen, String plainValue)
3536
throws IOException;
3637

38+
/**
39+
* Method called instead of {@link #decorateValue} in case where value being
40+
* written is from Java {@code null} value: this is often left as-is, without
41+
* decoration (and this is the default implementation), but may be
42+
* decorated.
43+
* To let default Null Value Replacement be used, should return {@code null}:
44+
* this is the default implementation.
45+
*
46+
* @param gen Generator that will be used for actual serialization
47+
*
48+
* @return Decorated value to use, IF NOT {@code null}: if {@code null} will use
49+
* default null replacement value.
50+
*
51+
* @throws IOException if attempt to decorate the value somehow fails
52+
* (typically a {@link com.fasterxml.jackson.core.exc.StreamWriteException})
53+
*/
54+
public default String decorateNull(CsvGenerator gen)
55+
throws IOException
56+
{
57+
return null;
58+
}
59+
3760
/**
3861
* Method called during deserialization, to remove possible decoration
3962
* applied with {@link #decorateValue}.

csv/src/test/java/com/fasterxml/jackson/dataformat/csv/failing/WriteBracketedArray495Test.java renamed to csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/WriteBracketedArray495Test.java

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.fasterxml.jackson.dataformat.csv.failing;
1+
package com.fasterxml.jackson.dataformat.csv.ser;
22

33
import java.io.StringWriter;
44

@@ -16,17 +16,19 @@
1616
// [dataformats-text#495]
1717
public class WriteBracketedArray495Test extends ModuleTestBase
1818
{
19-
// [dataformats-text#495]
20-
@JsonPropertyOrder({"id", "embeddings", "title" })
19+
// [dataformats-text#495]:
20+
@JsonPropertyOrder({"id", "embeddings", "title", "extra" })
2121
static class Article {
2222
public int id;
2323
public String title;
2424
public double[] embeddings;
25+
public int extra;
2526

2627
protected Article() { }
27-
public Article(int id, String title, double[] embeddings) {
28+
public Article(int id, String title, int extra, double[] embeddings) {
2829
this.id = id;
2930
this.title = title;
31+
this.extra = extra;
3032
this.embeddings = embeddings;
3133
}
3234
}
@@ -64,6 +66,10 @@ private CsvSchema _automaticSchema(boolean required)
6466
.withHeader()
6567
.withArrayElementSeparator(",")
6668
.withColumn("embeddings",
69+
col -> col.withValueDecorator(_bracketDecorator(required)))
70+
.withColumn("extra",
71+
col -> col.withValueDecorator(_bracketDecorator(required)))
72+
.withColumn("title",
6773
col -> col.withValueDecorator(_bracketDecorator(required)));
6874
}
6975

@@ -72,11 +78,14 @@ private CsvSchema _manualSchema(ColumnType ct, boolean required)
7278
return CsvSchema.builder()
7379
.setUseHeader(true)
7480
.setArrayElementSeparator(",")
75-
.addColumn("id", ColumnType.STRING)
81+
.addColumn("id", ColumnType.NUMBER)
7682
// and then the interesting one; may mark as "String" or "Array"
7783
.addColumn("embeddings", ct,
7884
col -> col.withValueDecorator(_bracketDecorator(required)))
79-
.addColumn("title", ColumnType.STRING)
85+
.addColumn("title", ColumnType.STRING,
86+
col -> col.withValueDecorator(_bracketDecorator(required)))
87+
.addColumn("extra", ColumnType.NUMBER,
88+
col -> col.withValueDecorator(_bracketDecorator(required)))
8089
.build();
8190
}
8291

@@ -93,11 +102,11 @@ private void _testArrayWithBracketsWrite(CsvSchema schema) throws Exception
93102
.with(schema)
94103
.writeValues(stringW);
95104

96-
sw.write(new Article(123, "Title!", new double[] { 0.5, -0.25, 2.5 }));
105+
sw.write(new Article(123, "Title!", 42, new double[] { 0.5, -0.25, 2.5 }));
97106
sw.close();
98107

99-
assertEquals("id,embeddings,title\n"
100-
+"123,\"[0.5,-0.25,2.5]\",\"Title!\"",
108+
assertEquals("id,embeddings,title,extra\n"
109+
+"123,\"[0.5,-0.25,2.5]\",\"[Title!]\",[42]",
101110
stringW.toString().trim());
102111
}
103112
}

release-notes/VERSION-2.x

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ Active Maintainers:
1818

1919
#442: (csv) Allow use of "value decorators" (like `[` and `]` for arrays)
2020
for reading `CsvSchema` columns
21-
2221
#468: (csv) Remove synchronization from `CsvMapper`
2322
(contributed by @pjfanning)
2423
#469: (csv) Allow CSV to differentiate between `null` and empty
@@ -30,6 +29,7 @@ Active Maintainers:
3029
(reported by @RafeArnold)
3130
#485: (csv) CSVDecoder: No Long and Int out of range exceptions
3231
(reported by Burdyug P)
32+
#495: (csv) Support use of `CsvValueDecorator` for writing CSV column values
3333

3434
2.17.2 (05-Jul-2024)
3535

0 commit comments

Comments
 (0)