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 cc75e8fe..e2e4ea06 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 @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.json.JsonWriteContext; import com.fasterxml.jackson.core.io.IOContext; import com.fasterxml.jackson.dataformat.csv.impl.CsvEncoder; +import com.fasterxml.jackson.dataformat.csv.impl.SimpleTokenWriteContext; public class CsvGenerator extends GeneratorBase { @@ -159,6 +160,14 @@ private Feature(boolean defaultState) { // note: can not be final since we may need to re-create it for new schema protected CsvEncoder _writer; + /** + * Current context, in form we can use it (GeneratorBase has + * untyped reference; left as null) + * + * @since 2.11 + */ + protected SimpleTokenWriteContext _csvWriteContext; + protected CharacterEscapes _characterEscapes = null; /* @@ -216,7 +225,7 @@ private Feature(boolean defaultState) { * * @since 2.7 */ - protected JsonWriteContext _skipWithin; + protected SimpleTokenWriteContext _skipWithin; /* /********************************************************** @@ -235,7 +244,7 @@ public CsvGenerator(IOContext ctxt, int jsonFeatures, int csvFeatures, _formatFeatures = csvFeatures; _schema = schema; _writer = new CsvEncoder(ctxt, csvFeatures, out, schema); - + _csvWriteContext = SimpleTokenWriteContext.createRootContext(null); _writer.setOutputEscapes(CsvCharacterEscapes.fromCsvFeatures(csvFeatures).getEscapeCodesForAscii()); } @@ -246,6 +255,7 @@ public CsvGenerator(IOContext ctxt, int jsonFeatures, int csvFeatures, _ioContext = ctxt; _formatFeatures = csvFeatures; _writer = csvWriter; + _csvWriteContext = SimpleTokenWriteContext.createRootContext(null); } /* @@ -298,6 +308,11 @@ public int getOutputBuffered() { return _writer.getOutputBuffered(); } + @Override + public SimpleTokenWriteContext getOutputContext() { + return _csvWriteContext; + } + @Override public void setSchema(FormatSchema schema) { @@ -380,7 +395,7 @@ public boolean canOmitFields() { @Override public final void writeFieldName(String name) throws IOException { - if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) { + if (!_csvWriteContext.writeFieldName(name)) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(name); @@ -390,7 +405,7 @@ public final void writeFieldName(String name) throws IOException public final void writeFieldName(SerializableString name) throws IOException { // Object is a value, need to verify it's allowed - if (_writeContext.writeFieldName(name.getValue()) == JsonWriteContext.STATUS_EXPECT_VALUE) { + if (!_csvWriteContext.writeFieldName(name.getValue())) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(name.getValue()); @@ -492,10 +507,10 @@ public final void writeStartArray() throws IOException _verifyValueWrite("start an array"); // Ok to create root-level array to contain Objects/Arrays, but // can not nest arrays in objects - if (_writeContext.inObject()) { + if (_csvWriteContext.inObject()) { if ((_skipWithin == null) && _skipValue && isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { - _skipWithin = _writeContext; + _skipWithin = _csvWriteContext; } else if (!_skipValue) { // First: column may have its own separator String sep; @@ -525,20 +540,20 @@ && _skipValue && isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { _reportError("CSV generator does not support nested Array values"); } } - _writeContext = _writeContext.createChildArrayContext(); + _csvWriteContext = _csvWriteContext.createChildArrayContext(null); // and that's about it, really } @Override public final void writeEndArray() throws IOException { - if (!_writeContext.inArray()) { - _reportError("Current context not Array but "+_writeContext.typeDesc()); + if (!_csvWriteContext.inArray()) { + _reportError("Current context not Array but "+_csvWriteContext.typeDesc()); } - _writeContext = _writeContext.getParent(); + _csvWriteContext = _csvWriteContext.getParent(); // 14-Dec-2015, tatu: To complete skipping of ignored structured value, need this: if (_skipWithin != null) { - if (_writeContext == _skipWithin) { + if (_csvWriteContext == _skipWithin) { _skipWithin = null; } return; @@ -549,7 +564,7 @@ public final void writeEndArray() throws IOException } // 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: - if (!_writeContext.inObject()) { + if (!_csvWriteContext.inObject()) { finishRow(); } } @@ -560,30 +575,30 @@ public final void writeStartObject() throws IOException _verifyValueWrite("start an object"); // No nesting for objects; can write Objects inside logical root-level arrays. // 14-Dec-2015, tatu: ... except, should be fine if we are ignoring the property - if (_writeContext.inObject() || + if (_csvWriteContext.inObject() || // 07-Nov-2017, tatu: But we may actually be nested indirectly; so check - (_writeContext.inArray() && !_writeContext.getParent().inRoot())) { + (_csvWriteContext.inArray() && !_csvWriteContext.getParent().inRoot())) { if (_skipWithin == null) { // new in 2.7 if (_skipValue && isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { - _skipWithin = _writeContext; + _skipWithin = _csvWriteContext; } else { _reportMappingError("CSV generator does not support Object values for properties (nested Objects)"); } } } - _writeContext = _writeContext.createChildObjectContext(); + _csvWriteContext = _csvWriteContext.createChildObjectContext(null); } @Override public final void writeEndObject() throws IOException { - if (!_writeContext.inObject()) { - _reportError("Current context not Object but "+_writeContext.typeDesc()); + if (!_csvWriteContext.inObject()) { + _reportError("Current context not Object but "+_csvWriteContext.typeDesc()); } - _writeContext = _writeContext.getParent(); + _csvWriteContext = _csvWriteContext.getParent(); // 14-Dec-2015, tatu: To complete skipping of ignored structured value, need this: if (_skipWithin != null) { - if (_writeContext == _skipWithin) { + if (_csvWriteContext == _skipWithin) { _skipWithin = null; } return; @@ -760,16 +775,16 @@ public void writeNull() throws IOException if (!_skipValue) { if (!_arraySeparator.isEmpty()) { _addToArray(_schema.getNullValueOrEmpty()); - } else if (_writeContext.inObject()) { + } else if (_csvWriteContext.inObject()) { _writer.writeNull(_columnIndex()); - } else if (_writeContext.inArray()) { + } else if (_csvWriteContext.inArray()) { // [dataformat-csv#106]: Need to make sure we don't swallow nulls in arrays either // 04-Jan-2016, tatu: but check for case of array-wrapping, in which case null stands for absence // of Object. In this case, could either add an empty row, or skip -- for now, we'll // just skip; can change, if so desired, to expose "root null" as empty rows, possibly // 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 (!_writeContext.getParent().inRoot()) { + if (!_csvWriteContext.getParent().inRoot()) { _writer.writeNull(_columnIndex()); } @@ -909,7 +924,7 @@ public void writeOmittedField(String fieldName) throws IOException // assumed to have been removed from schema too } else { // basically combination of "writeFieldName()" and "writeNull()" - if (_writeContext.writeFieldName(fieldName) == JsonWriteContext.STATUS_EXPECT_VALUE) { + if (!_csvWriteContext.writeFieldName(fieldName)) { _reportError("Can not skip a field, expecting a value"); } // and all we do is just note index to use for following value write @@ -929,8 +944,7 @@ public void writeOmittedField(String fieldName) throws IOException @Override protected final void _verifyValueWrite(String typeMsg) throws IOException { - int status = _writeContext.writeValue(); - if (status == JsonWriteContext.STATUS_EXPECT_NAME) { + if (!_csvWriteContext.writeValue()) { _reportError("Can not "+typeMsg+", expecting field name"); } if (_handleFirstLine) { diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/SimpleTokenWriteContext.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/SimpleTokenWriteContext.java new file mode 100644 index 00000000..0c270fec --- /dev/null +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/SimpleTokenWriteContext.java @@ -0,0 +1,197 @@ +package com.fasterxml.jackson.dataformat.csv.impl; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.json.DupDetector; + +/** + * Intermediate base context; for 2.x copied within CSV format codebase, + * in 3.0 will be part of {@code jackson-core} + * + * @since 2.11 + */ +public final class SimpleTokenWriteContext extends JsonStreamContext +{ + /** + * Parent context for this context; null for root context. + */ + protected final SimpleTokenWriteContext _parent; + + // // // Optional duplicate detection + + protected DupDetector _dups; + + /* + /********************************************************************** + /* Simple instance reuse slots; speed up things a bit (10-15%) + /* for docs with lots of small arrays/objects + /********************************************************************** + */ + + protected SimpleTokenWriteContext _childToRecycle; + + /* + /********************************************************************** + /* Location/state information (minus source reference) + /********************************************************************** + */ + + /** + * Name of the field of which value is to be written; only + * used for OBJECT contexts + */ + protected String _currentName; + + protected Object _currentValue; + + /** + * Marker used to indicate that we just wrote a field name + * and now expect a value to write + */ + protected boolean _gotFieldId; + + /* + /********************************************************************** + /* Life-cycle + /********************************************************************** + */ + + protected SimpleTokenWriteContext(int type, SimpleTokenWriteContext parent, DupDetector dups, + Object currentValue) { + super(); + _type = type; + _parent = parent; + _dups = dups; + _index = -1; + _currentValue = currentValue; + } + + private SimpleTokenWriteContext reset(int type, Object currentValue) { + _type = type; + _index = -1; + _currentName = null; + _gotFieldId = false; + _currentValue = currentValue; + if (_dups != null) { _dups.reset(); } + return this; + } + + public SimpleTokenWriteContext withDupDetector(DupDetector dups) { + _dups = dups; + return this; + } + + @Override + public Object getCurrentValue() { + return _currentValue; + } + + @Override + public void setCurrentValue(Object v) { + _currentValue = v; + } + + /* + /********************************************************************** + /* Factory methods + /********************************************************************** + */ + + public static SimpleTokenWriteContext createRootContext(DupDetector dd) { + return new SimpleTokenWriteContext(TYPE_ROOT, null, dd, null); + } + + public SimpleTokenWriteContext createChildArrayContext(Object currentValue) { + SimpleTokenWriteContext ctxt = _childToRecycle; + if (ctxt == null) { + _childToRecycle = ctxt = new SimpleTokenWriteContext(TYPE_ARRAY, this, + (_dups == null) ? null : _dups.child(), currentValue); + return ctxt; + } + return ctxt.reset(TYPE_ARRAY, currentValue); + } + + public SimpleTokenWriteContext createChildObjectContext(Object currentValue) { + SimpleTokenWriteContext ctxt = _childToRecycle; + if (ctxt == null) { + _childToRecycle = ctxt = new SimpleTokenWriteContext(TYPE_OBJECT, this, + (_dups == null) ? null : _dups.child(), currentValue); + return ctxt; + } + return ctxt.reset(TYPE_OBJECT, currentValue); + } + + /* + /********************************************************************** + /* Accessors + /********************************************************************** + */ + + @Override public final SimpleTokenWriteContext getParent() { return _parent; } + @Override public final String getCurrentName() { + // 15-Aug-2019, tatu: Should NOT check this status because otherwise name + // in parent context is not accessible after new structured scope started +// if (_gotFieldId) { ... } + return _currentName; + } + + @Override public boolean hasCurrentName() { return _gotFieldId; } + + /** + * Method that can be used to both clear the accumulated references + * (specifically value set with {@link #setCurrentValue(Object)}) + * that should not be retained, and returns parent (as would + * {@link #getParent()} do). Typically called when closing the active + * context when encountering {@link JsonToken#END_ARRAY} or + * {@link JsonToken#END_OBJECT}. + */ + public SimpleTokenWriteContext clearAndGetParent() { + _currentValue = null; + // could also clear the current name, but seems cheap enough to leave? + return _parent; + } + + public DupDetector getDupDetector() { + return _dups; + } + + /* + /********************************************************************** + /* State changing + /********************************************************************** + */ + + /** + * Method that writer is to call before it writes a field name. + * + * @return Ok if name writing should proceed + */ + public boolean writeFieldName(String name) throws JsonProcessingException { + if ((_type != TYPE_OBJECT) || _gotFieldId) { + return false; + } + _gotFieldId = true; + _currentName = name; + if (_dups != null) { _checkDup(_dups, name); } + return true; + } + + private final void _checkDup(DupDetector dd, String name) throws JsonProcessingException { + if (dd.isDup(name)) { + Object src = dd.getSource(); + throw new JsonGenerationException("Duplicate field '"+name+"'", + ((src instanceof JsonGenerator) ? ((JsonGenerator) src) : null)); + } + } + + public boolean writeValue() { + // Only limitation is with OBJECTs: + if (_type == TYPE_OBJECT) { + if (!_gotFieldId) { + return false; + } + _gotFieldId = false; + } + ++_index; + return true; + } +}