Skip to content

Commit

Permalink
Fix #442: allow registering handlers for "decorating" values (like Ar…
Browse files Browse the repository at this point in the history
…rays) (#443)
  • Loading branch information
cowtowncoder authored Aug 26, 2024
1 parent 2d784b2 commit bb06df1
Show file tree
Hide file tree
Showing 7 changed files with 629 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1017,12 +1017,20 @@ protected JsonToken _handleNextEntry() throws IOException
}
return _handleObjectRowEnd();
}
_currentValue = next;
if (_columnIndex >= _columnCount) {
_currentValue = next;
return _handleExtraColumn(next);
}
final CsvSchema.Column column = _schema.column(_columnIndex);
_state = STATE_NAMED_VALUE;
_currentName = _schema.columnName(_columnIndex);
_currentName = column.getName();
// 25-Aug-2024, tatu: [dataformats-text#442] May have value decorator
CsvValueDecorator dec = column.getValueDecorator();
if (dec == null) {
_currentValue = next;
} else {
_currentValue = dec.undecorateValue(this, next);
}
return JsonToken.FIELD_NAME;
}

Expand Down
169 changes: 166 additions & 3 deletions csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package com.fasterxml.jackson.dataformat.csv;

import java.util.*;
import java.util.function.UnaryOperator;

import com.fasterxml.jackson.core.FormatSchema;

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

/**
* Value decorator used for this column, if any; {@code null} if none.
* Used to add decoration on serialization (writing) and remove decoration
* on deserialization (reading).
*
* @since 2.18
*/
private final CsvValueDecorator _valueDecorator;

/**
* Link to the next column within schema, if one exists;
* null for the last column.
Expand All @@ -285,22 +295,39 @@ public Column(int index, String name, ColumnType type, String arrayElementSep)
_name = name;
_type = type;
_arrayElementSeparator = _validArrayElementSeparator(arrayElementSep);
_valueDecorator = null;
_next = null;
}

public Column(Column src, Column next) {
this(src, src._index, next);
this(src, src._index, src._valueDecorator, next);
}

protected Column(Column src, int index, Column next) {
this(src, index, src._valueDecorator, next);
}

/**
* @since 2.18
*/
protected Column(Column src, CsvValueDecorator valueDecorator) {
this(src, src._index, valueDecorator, src._next);
}

protected Column(Column src, int index, Column next)
/**
* @since 2.18
*/
protected Column(Column src, int index, CsvValueDecorator valueDecorator,
Column next)
{
_index = index;
_name = src._name;
_type = src._type;
_arrayElementSeparator = src._arrayElementSeparator;
_valueDecorator = valueDecorator;
_next = next;
}

public Column withName(String newName) {
if (_name == newName) {
return this;
Expand All @@ -323,6 +350,16 @@ public Column withArrayElementSeparator(String separator) {
return new Column(_index, _name, _type, sep);
}

/**
* @since 2.18
*/
public Column withValueDecorator(CsvValueDecorator valueDecorator) {
if (valueDecorator == _valueDecorator) {
return this;
}
return new Column(this, valueDecorator);
}

public Column withNext(Column next) {
if (_next == next) {
return this;
Expand Down Expand Up @@ -366,6 +403,11 @@ public boolean hasName(String n) {
*/
public String getArrayElementSeparator() { return _arrayElementSeparator; }

/**
* @since 2.18
*/
public CsvValueDecorator getValueDecorator() { return _valueDecorator; }

public boolean isArray() {
return (_type == ColumnType.ARRAY);
}
Expand Down Expand Up @@ -445,6 +487,22 @@ public Builder addColumn(String name) {
return addColumn(new Column(index, name));
}

/**
* Add column with given name, and with changes to apply (as specified
* by second argument, {@code transformer}).
* NOTE: does NOT check for duplicate column names so it is possibly to
* accidentally add duplicates.
*
* @param name Name of column to add
* @param transformer Changes to apply to column definition
*
* @since 2.18
*/
public Builder addColumn(String name, UnaryOperator<Column> transformer) {
Column col = transformer.apply(new Column(_columns.size(), name));
return addColumn(col);
}

/**
* NOTE: does NOT check for duplicate column names so it is possibly to
* accidentally add duplicates.
Expand All @@ -454,6 +512,24 @@ public Builder addColumn(String name, ColumnType type) {
return addColumn(new Column(index, name, type));
}

/**
* Add column with given name, and with changes to apply (as specified
* by second argument, {@code transformer}).
* NOTE: does NOT check for duplicate column names so it is possibly to
* accidentally add duplicates.
*
* @param name Name of column to add
* @param type Type of the column to add
* @param transformer Changes to apply to column definition
*
* @since 2.18
*/
public Builder addColumn(String name, ColumnType type,
UnaryOperator<Column> transformer) {
Column col = transformer.apply(new Column(_columns.size(), name, type));
return addColumn(col);
}

/**
* NOTE: does NOT check for duplicate column names so it is possibly to
* accidentally add duplicates.
Expand Down Expand Up @@ -1175,6 +1251,9 @@ public CsvSchema withoutColumns() {
* returns it unmodified (if no new columns found from `toAppend`), or constructs
* a new instance and returns that.
*
* @return Either this schema (if nothing changed), or newly constructed {@link CsvSchema}
* with appended columns.
*
* @since 2.9
*/
public CsvSchema withColumnsFrom(CsvSchema toAppend) {
Expand All @@ -1192,6 +1271,77 @@ public CsvSchema withColumnsFrom(CsvSchema toAppend) {
return b.build();
}

/**
* Mutant factory method that will try to replace specified column with
* changed definition (but same name), leaving other columns as-is.
*<p>
* As with all `withXxx()` methods this method never modifies `this` but either
* returns it unmodified (if no change to column), or constructs
* a new schema instance and returns that.
*
* @param columnName Name of column to replace
* @param transformer Transformation to apply to the column
*
* @return Either this schema (if column did not change), or newly constructed {@link CsvSchema}
* with changed column
*
* @since 2.18
*/
public CsvSchema withColumn(String columnName, UnaryOperator<Column> transformer) {
Column old = column(columnName);
if (old == null) {
throw new IllegalArgumentException("No column '"+columnName+"' in CsvSchema (known columns: "
+getColumnNames()+")");
}
Column newColumn = transformer.apply(old);
if (newColumn == old) {
return this;
}
return _withColumn(old.getIndex(), newColumn);
}

/**
* Mutant factory method that will try to replace specified column with
* changed definition (but same name), leaving other columns as-is.
*<p>
* As with all `withXxx()` methods this method never modifies `this` but either
* returns it unmodified (if no change to column), or constructs
* a new schema instance and returns that.
*
* @param columnIndex Index of column to replace
* @param transformer Transformation to apply to the column
*
* @return Either this schema (if column did not change), or newly constructed {@link CsvSchema}
* with changed column
*
* @since 2.18
*/
public CsvSchema withColumn(int columnIndex, UnaryOperator<Column> transformer) {
if (columnIndex < 0 || columnIndex >= size()) {
throw new IllegalArgumentException("Illegal index "+columnIndex+"; `CsvSchema` has "+size()+" columns");
}
Column old = _columns[columnIndex];
Column newColumn = transformer.apply(old);
if (newColumn == old) {
return this;
}
return _withColumn(old.getIndex(), newColumn);
}

/**
* @since 2.18
*/
protected CsvSchema _withColumn(int ix, Column toReplace) {
Objects.requireNonNull(toReplace);
if (ix < 0 || ix >= size()) {
throw new IllegalArgumentException("Illegal index for column '"+toReplace.getName()+"': "
+ix+" (column count: "+size()+")");
}
return rebuild()
.replaceColumn(ix, toReplace)
.build();
}

/**
* @since 2.7
*/
Expand Down Expand Up @@ -1352,6 +1502,19 @@ public Column column(int index) {
return _columns[index];
}

/**
* Method for finding index of a named column within this schema.
*
* @param name Name of column to find
* @return Index of the specified column, if one exists; {@code -1} if not
*
* @since 2.18
*/
public int columnIndex(String name) {
Column col = column(name);
return (col == null) ? -1 : col.getIndex();
}

/**
* @since 2.6
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.fasterxml.jackson.dataformat.csv;

import java.io.IOException;

/**
* Interface defining API for handlers that can add and remove "decorations"
* to CSV values: for example, brackets around Array (List) values encoded
* in a single physical String column.
*<p>
* Decorations are handled after handling other encoding aspects such as
* optional quoting and/or escaping.
*<p>
* Decorators can be registered on specific columns of {@link CsvSchema}.
*
* @since 2.18
*/
public interface CsvValueDecorator
{
/**
* Method called during serialization when encoding a value,
* to produce "decorated" value to include in output (possibly
* escaped and/or quoted).
* Note that possible escaping and/or quoting (as per configuration
* of {@link CsvSchema} is applied on decorated value.
*
* @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)
*
* @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 during deserialization, to remove possible decoration
* applied with {@link #decorateValue}.
* Call is made after textual value for a cell (column
* value) has been read using {@code parser} and after removing (decoding)
* possible quoting and/or escaping of the value. Value passed in
* has no escaping or quoting left.
*
* @param parser Parser that was used to decode textual value from input
* @param decoratedValue Value from which to remove decorations, if any
* (some decorators can allow optional decorations; others may fail
* if none found)
*
* @return Value after removing decorations, if any.
*
* @throws IOException if attempt to un-decorate the value fails
* (typically a {@link com.fasterxml.jackson.core.exc.StreamReadException})
*/
public String undecorateValue(CsvParser parser, String decoratedValue)
throws IOException;
}
Loading

0 comments on commit bb06df1

Please sign in to comment.