Skip to content

Commit

Permalink
Fix #495: support value decoration on writing too
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Aug 27, 2024
1 parent bb06df1 commit c44fb3e
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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();
}

/*
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -681,6 +701,12 @@ public void writeString(String text) throws IOException
@Override
public void writeString(char[] text, int offset, int len) throws IOException
{
// 26-Aug-2024, tatu: [dataformats-text#495] Decorations!
if (_nextColumnDecorator != null) {
writeString(new String(text, offset, len));
return;
}

_verifyValueWrite("write String value");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
Expand All @@ -699,7 +725,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);
}
}
}
Expand Down Expand Up @@ -780,6 +811,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...
Expand All @@ -791,6 +823,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);
}
}
Expand All @@ -810,7 +846,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);
}
}
}
}
Expand All @@ -824,6 +866,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
Expand All @@ -833,6 +884,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());
}

Expand All @@ -852,6 +911,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);
}
Expand All @@ -870,6 +933,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);
}
Expand All @@ -887,6 +954,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);

Expand All @@ -901,6 +972,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);
}
Expand All @@ -914,6 +989,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);
}
Expand All @@ -932,6 +1011,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);
}
Expand All @@ -949,6 +1033,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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,37 @@ 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})
*/
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}.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.fasterxml.jackson.dataformat.csv.failing;
package com.fasterxml.jackson.dataformat.csv.ser;

import java.io.StringWriter;

Expand All @@ -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;
}
}
Expand Down Expand Up @@ -64,6 +66,10 @@ private CsvSchema _automaticSchema(boolean required)
.withHeader()
.withArrayElementSeparator(",")
.withColumn("embeddings",
col -> col.withValueDecorator(_bracketDecorator(required)))
.withColumn("extra",
col -> col.withValueDecorator(_bracketDecorator(required)))
.withColumn("title",
col -> col.withValueDecorator(_bracketDecorator(required)));
}

Expand All @@ -72,11 +78,14 @@ private CsvSchema _manualSchema(ColumnType ct, boolean required)
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)
.addColumn("title", ColumnType.STRING,
col -> col.withValueDecorator(_bracketDecorator(required)))
.addColumn("extra", ColumnType.NUMBER,
col -> col.withValueDecorator(_bracketDecorator(required)))
.build();
}

Expand All @@ -93,11 +102,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());
}
}
2 changes: 1 addition & 1 deletion release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down

0 comments on commit c44fb3e

Please sign in to comment.