Skip to content

Commit bb06df1

Browse files
authored
Fix #442: allow registering handlers for "decorating" values (like Arrays) (#443)
1 parent 2d784b2 commit bb06df1

File tree

7 files changed

+629
-5
lines changed

7 files changed

+629
-5
lines changed

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -1017,12 +1017,20 @@ protected JsonToken _handleNextEntry() throws IOException
10171017
}
10181018
return _handleObjectRowEnd();
10191019
}
1020-
_currentValue = next;
10211020
if (_columnIndex >= _columnCount) {
1021+
_currentValue = next;
10221022
return _handleExtraColumn(next);
10231023
}
1024+
final CsvSchema.Column column = _schema.column(_columnIndex);
10241025
_state = STATE_NAMED_VALUE;
1025-
_currentName = _schema.columnName(_columnIndex);
1026+
_currentName = column.getName();
1027+
// 25-Aug-2024, tatu: [dataformats-text#442] May have value decorator
1028+
CsvValueDecorator dec = column.getValueDecorator();
1029+
if (dec == null) {
1030+
_currentValue = next;
1031+
} else {
1032+
_currentValue = dec.undecorateValue(this, next);
1033+
}
10261034
return JsonToken.FIELD_NAME;
10271035
}
10281036

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

+166-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package com.fasterxml.jackson.dataformat.csv;
33

44
import java.util.*;
5+
import java.util.function.UnaryOperator;
56

67
import com.fasterxml.jackson.core.FormatSchema;
78

@@ -263,6 +264,15 @@ public static class Column implements java.io.Serializable // since 2.4.3
263264
*/
264265
private final String _arrayElementSeparator;
265266

267+
/**
268+
* Value decorator used for this column, if any; {@code null} if none.
269+
* Used to add decoration on serialization (writing) and remove decoration
270+
* on deserialization (reading).
271+
*
272+
* @since 2.18
273+
*/
274+
private final CsvValueDecorator _valueDecorator;
275+
266276
/**
267277
* Link to the next column within schema, if one exists;
268278
* null for the last column.
@@ -285,22 +295,39 @@ public Column(int index, String name, ColumnType type, String arrayElementSep)
285295
_name = name;
286296
_type = type;
287297
_arrayElementSeparator = _validArrayElementSeparator(arrayElementSep);
298+
_valueDecorator = null;
288299
_next = null;
289300
}
290301

291302
public Column(Column src, Column next) {
292-
this(src, src._index, next);
303+
this(src, src._index, src._valueDecorator, next);
304+
}
305+
306+
protected Column(Column src, int index, Column next) {
307+
this(src, index, src._valueDecorator, next);
308+
}
309+
310+
/**
311+
* @since 2.18
312+
*/
313+
protected Column(Column src, CsvValueDecorator valueDecorator) {
314+
this(src, src._index, valueDecorator, src._next);
293315
}
294316

295-
protected Column(Column src, int index, Column next)
317+
/**
318+
* @since 2.18
319+
*/
320+
protected Column(Column src, int index, CsvValueDecorator valueDecorator,
321+
Column next)
296322
{
297323
_index = index;
298324
_name = src._name;
299325
_type = src._type;
300326
_arrayElementSeparator = src._arrayElementSeparator;
327+
_valueDecorator = valueDecorator;
301328
_next = next;
302329
}
303-
330+
304331
public Column withName(String newName) {
305332
if (_name == newName) {
306333
return this;
@@ -323,6 +350,16 @@ public Column withArrayElementSeparator(String separator) {
323350
return new Column(_index, _name, _type, sep);
324351
}
325352

353+
/**
354+
* @since 2.18
355+
*/
356+
public Column withValueDecorator(CsvValueDecorator valueDecorator) {
357+
if (valueDecorator == _valueDecorator) {
358+
return this;
359+
}
360+
return new Column(this, valueDecorator);
361+
}
362+
326363
public Column withNext(Column next) {
327364
if (_next == next) {
328365
return this;
@@ -366,6 +403,11 @@ public boolean hasName(String n) {
366403
*/
367404
public String getArrayElementSeparator() { return _arrayElementSeparator; }
368405

406+
/**
407+
* @since 2.18
408+
*/
409+
public CsvValueDecorator getValueDecorator() { return _valueDecorator; }
410+
369411
public boolean isArray() {
370412
return (_type == ColumnType.ARRAY);
371413
}
@@ -445,6 +487,22 @@ public Builder addColumn(String name) {
445487
return addColumn(new Column(index, name));
446488
}
447489

490+
/**
491+
* Add column with given name, and with changes to apply (as specified
492+
* by second argument, {@code transformer}).
493+
* NOTE: does NOT check for duplicate column names so it is possibly to
494+
* accidentally add duplicates.
495+
*
496+
* @param name Name of column to add
497+
* @param transformer Changes to apply to column definition
498+
*
499+
* @since 2.18
500+
*/
501+
public Builder addColumn(String name, UnaryOperator<Column> transformer) {
502+
Column col = transformer.apply(new Column(_columns.size(), name));
503+
return addColumn(col);
504+
}
505+
448506
/**
449507
* NOTE: does NOT check for duplicate column names so it is possibly to
450508
* accidentally add duplicates.
@@ -454,6 +512,24 @@ public Builder addColumn(String name, ColumnType type) {
454512
return addColumn(new Column(index, name, type));
455513
}
456514

515+
/**
516+
* Add column with given name, and with changes to apply (as specified
517+
* by second argument, {@code transformer}).
518+
* NOTE: does NOT check for duplicate column names so it is possibly to
519+
* accidentally add duplicates.
520+
*
521+
* @param name Name of column to add
522+
* @param type Type of the column to add
523+
* @param transformer Changes to apply to column definition
524+
*
525+
* @since 2.18
526+
*/
527+
public Builder addColumn(String name, ColumnType type,
528+
UnaryOperator<Column> transformer) {
529+
Column col = transformer.apply(new Column(_columns.size(), name, type));
530+
return addColumn(col);
531+
}
532+
457533
/**
458534
* NOTE: does NOT check for duplicate column names so it is possibly to
459535
* accidentally add duplicates.
@@ -1175,6 +1251,9 @@ public CsvSchema withoutColumns() {
11751251
* returns it unmodified (if no new columns found from `toAppend`), or constructs
11761252
* a new instance and returns that.
11771253
*
1254+
* @return Either this schema (if nothing changed), or newly constructed {@link CsvSchema}
1255+
* with appended columns.
1256+
*
11781257
* @since 2.9
11791258
*/
11801259
public CsvSchema withColumnsFrom(CsvSchema toAppend) {
@@ -1192,6 +1271,77 @@ public CsvSchema withColumnsFrom(CsvSchema toAppend) {
11921271
return b.build();
11931272
}
11941273

1274+
/**
1275+
* Mutant factory method that will try to replace specified column with
1276+
* changed definition (but same name), leaving other columns as-is.
1277+
*<p>
1278+
* As with all `withXxx()` methods this method never modifies `this` but either
1279+
* returns it unmodified (if no change to column), or constructs
1280+
* a new schema instance and returns that.
1281+
*
1282+
* @param columnName Name of column to replace
1283+
* @param transformer Transformation to apply to the column
1284+
*
1285+
* @return Either this schema (if column did not change), or newly constructed {@link CsvSchema}
1286+
* with changed column
1287+
*
1288+
* @since 2.18
1289+
*/
1290+
public CsvSchema withColumn(String columnName, UnaryOperator<Column> transformer) {
1291+
Column old = column(columnName);
1292+
if (old == null) {
1293+
throw new IllegalArgumentException("No column '"+columnName+"' in CsvSchema (known columns: "
1294+
+getColumnNames()+")");
1295+
}
1296+
Column newColumn = transformer.apply(old);
1297+
if (newColumn == old) {
1298+
return this;
1299+
}
1300+
return _withColumn(old.getIndex(), newColumn);
1301+
}
1302+
1303+
/**
1304+
* Mutant factory method that will try to replace specified column with
1305+
* changed definition (but same name), leaving other columns as-is.
1306+
*<p>
1307+
* As with all `withXxx()` methods this method never modifies `this` but either
1308+
* returns it unmodified (if no change to column), or constructs
1309+
* a new schema instance and returns that.
1310+
*
1311+
* @param columnIndex Index of column to replace
1312+
* @param transformer Transformation to apply to the column
1313+
*
1314+
* @return Either this schema (if column did not change), or newly constructed {@link CsvSchema}
1315+
* with changed column
1316+
*
1317+
* @since 2.18
1318+
*/
1319+
public CsvSchema withColumn(int columnIndex, UnaryOperator<Column> transformer) {
1320+
if (columnIndex < 0 || columnIndex >= size()) {
1321+
throw new IllegalArgumentException("Illegal index "+columnIndex+"; `CsvSchema` has "+size()+" columns");
1322+
}
1323+
Column old = _columns[columnIndex];
1324+
Column newColumn = transformer.apply(old);
1325+
if (newColumn == old) {
1326+
return this;
1327+
}
1328+
return _withColumn(old.getIndex(), newColumn);
1329+
}
1330+
1331+
/**
1332+
* @since 2.18
1333+
*/
1334+
protected CsvSchema _withColumn(int ix, Column toReplace) {
1335+
Objects.requireNonNull(toReplace);
1336+
if (ix < 0 || ix >= size()) {
1337+
throw new IllegalArgumentException("Illegal index for column '"+toReplace.getName()+"': "
1338+
+ix+" (column count: "+size()+")");
1339+
}
1340+
return rebuild()
1341+
.replaceColumn(ix, toReplace)
1342+
.build();
1343+
}
1344+
11951345
/**
11961346
* @since 2.7
11971347
*/
@@ -1352,6 +1502,19 @@ public Column column(int index) {
13521502
return _columns[index];
13531503
}
13541504

1505+
/**
1506+
* Method for finding index of a named column within this schema.
1507+
*
1508+
* @param name Name of column to find
1509+
* @return Index of the specified column, if one exists; {@code -1} if not
1510+
*
1511+
* @since 2.18
1512+
*/
1513+
public int columnIndex(String name) {
1514+
Column col = column(name);
1515+
return (col == null) ? -1 : col.getIndex();
1516+
}
1517+
13551518
/**
13561519
* @since 2.6
13571520
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.fasterxml.jackson.dataformat.csv;
2+
3+
import java.io.IOException;
4+
5+
/**
6+
* Interface defining API for handlers that can add and remove "decorations"
7+
* to CSV values: for example, brackets around Array (List) values encoded
8+
* in a single physical String column.
9+
*<p>
10+
* Decorations are handled after handling other encoding aspects such as
11+
* optional quoting and/or escaping.
12+
*<p>
13+
* Decorators can be registered on specific columns of {@link CsvSchema}.
14+
*
15+
* @since 2.18
16+
*/
17+
public interface CsvValueDecorator
18+
{
19+
/**
20+
* Method called during serialization when encoding a value,
21+
* to produce "decorated" value to include in output (possibly
22+
* escaped and/or quoted).
23+
* Note that possible escaping and/or quoting (as per configuration
24+
* of {@link CsvSchema} is applied on decorated value.
25+
*
26+
* @param gen Generator that will be used for actual serialization
27+
* @param plainValue Value to decorate
28+
*
29+
* @return Decorated value (which may be {@code plainValue} as-is)
30+
*
31+
* @throws IOException if attempt to decorate the value somehow fails
32+
* (typically a {@link com.fasterxml.jackson.core.exc.StreamWriteException})
33+
*/
34+
public String decorateValue(CsvGenerator gen, String plainValue)
35+
throws IOException;
36+
37+
/**
38+
* Method called during deserialization, to remove possible decoration
39+
* applied with {@link #decorateValue}.
40+
* Call is made after textual value for a cell (column
41+
* value) has been read using {@code parser} and after removing (decoding)
42+
* possible quoting and/or escaping of the value. Value passed in
43+
* has no escaping or quoting left.
44+
*
45+
* @param parser Parser that was used to decode textual value from input
46+
* @param decoratedValue Value from which to remove decorations, if any
47+
* (some decorators can allow optional decorations; others may fail
48+
* if none found)
49+
*
50+
* @return Value after removing decorations, if any.
51+
*
52+
* @throws IOException if attempt to un-decorate the value fails
53+
* (typically a {@link com.fasterxml.jackson.core.exc.StreamReadException})
54+
*/
55+
public String undecorateValue(CsvParser parser, String decoratedValue)
56+
throws IOException;
57+
}

0 commit comments

Comments
 (0)