From 4e8a3955b6ddfaf52ad6f471ab3edbfbc2142540 Mon Sep 17 00:00:00 2001 From: "petar.tahchiev" Date: Sun, 29 Jul 2018 23:11:48 +0300 Subject: [PATCH 1/2] Add serialization of object members. - Keep track of object nesting, similar to jackson-dataformat-properties --- .../jackson/dataformat/csv/CsvEscapes.java | 99 +++ .../jackson/dataformat/csv/CsvGenerator.java | 97 ++- .../jackson/dataformat/csv/CsvSchema.java | 671 +++++++++++------- .../dataformat/csv/CsvWriteContext.java | 124 ++++ .../dataformat/csv/ModuleTestBase.java | 109 +++ .../csv/ser/GeneratorIgnoreUnknown51Test.java | 16 +- .../dataformat/csv/ser/TestGenerator.java | 159 ++++- 7 files changed, 966 insertions(+), 309 deletions(-) create mode 100644 csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvEscapes.java create mode 100644 csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvWriteContext.java diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvEscapes.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvEscapes.java new file mode 100644 index 00000000..e8ed8ff8 --- /dev/null +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvEscapes.java @@ -0,0 +1,99 @@ +package com.fasterxml.jackson.dataformat.csv; + +import java.util.Arrays; + +/** + * @author Petar Tahchiev + * @since 2.0.1 + */ +public class CsvEscapes { + private final static char[] HEX = "0123456789ABCDEF".toCharArray(); + + private final static int UNICODE_ESCAPE = -1; + + private final static int[] sValueEscapes; + static { + final int[] table = new int[256]; + // For values, fewer escapes needed, but most control chars need them + for (int i = 0; i < 32; ++i) { + table[i] = UNICODE_ESCAPE; + // also high-bit ones + table[128+i] = UNICODE_ESCAPE; + } + // also, that one weird character needs escaping: + table[0x7F] = UNICODE_ESCAPE; + + // except for "well-known" ones + table['\t'] = 't'; + table['\r'] = 'r'; + table['\n'] = 'n'; + + // Beyond that, just backslash + table['\\'] = '\\'; + sValueEscapes = table; + } + + private final static int[] sKeyEscapes; + static { + // with keys, start with value escapes, and add the rest + final int[] table = Arrays.copyOf(sValueEscapes, 256); + + // comment line starters (could get by with just start char but whatever) + table['#'] = '#'; + table['!'] = '!'; + // and then equals (and equivalents) that mark end of key + table['='] = '='; + table[':'] = ':'; + // plus space chars are escapes too + table[' '] = ' '; + + sKeyEscapes = table; + } + + public static void appendKey(StringBuilder sb, String key) { + final int end = key.length(); + if (end == 0) { + return; + } + final int[] esc = sKeyEscapes; + // first quick loop for common case of no escapes + int i = 0; + + while (true) { + char c = key.charAt(i); + if ((c > 0xFF) || esc[c] != 0) { + break; + } + sb.append(c); + if (++i == end) { + return; + } + } + _appendWithEscapes(sb, key, esc, i); + } + + private static void _appendWithEscapes(StringBuilder sb, String key, + int[] esc, int i) + { + final int end = key.length(); + do { + char c = key.charAt(i); + int type = (c > 0xFF) ? UNICODE_ESCAPE : esc[c]; + if (type == 0) { + sb.append(c); + continue; + } + if (type == UNICODE_ESCAPE) { + sb.append('\\'); + sb.append('u'); + sb.append(HEX[c >>> 12]); + sb.append(HEX[(c >> 8) & 0xF]); + sb.append(HEX[(c >> 4) & 0xF]); + sb.append(HEX[c & 0xF]); + } else { + sb.append('\\'); + sb.append((char) type); + } + } while (++i < end); + } +} 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 710f9bc3..8d03f45d 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 @@ -138,6 +138,10 @@ private Feature(boolean defaultState) { */ protected int _formatFeatures; + protected final StringBuilder _basePath = new StringBuilder(50); + + protected int _indentLength; + /** * Definition of columns being written, if available. */ @@ -146,6 +150,12 @@ 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) + */ + protected CsvWriteContext _writeContext; + /* /********************************************************** /* Output state @@ -201,7 +211,7 @@ private Feature(boolean defaultState) { * * @since 2.7 */ - protected JsonWriteContext _skipWithin; + protected CsvWriteContext _skipWithin; /* /********************************************************** @@ -220,6 +230,7 @@ public CsvGenerator(IOContext ctxt, int jsonFeatures, int csvFeatures, _formatFeatures = csvFeatures; _schema = schema; _writer = new CsvEncoder(ctxt, csvFeatures, out, schema); + _writeContext = CsvWriteContext.createRootContext(); } public CsvGenerator(IOContext ctxt, int jsonFeatures, int csvFeatures, @@ -229,6 +240,7 @@ public CsvGenerator(IOContext ctxt, int jsonFeatures, int csvFeatures, _ioContext = ctxt; _formatFeatures = csvFeatures; _writer = csvWriter; + _writeContext = CsvWriteContext.createRootContext(); } /* @@ -281,12 +293,21 @@ public int getOutputBuffered() { return _writer.getOutputBuffered(); } + @Override + public CsvWriteContext getOutputContext() { + return _writeContext; + } + @Override public void setSchema(FormatSchema schema) { if (schema instanceof CsvSchema) { if (_schema != schema) { _schema = (CsvSchema) schema; + if (_writeContext.inRoot()) { + _basePath.setLength(0); + } + _writer = _writer.withSchema(_schema); } } else { @@ -345,7 +366,7 @@ public boolean canOmitFields() { @Override public final void writeFieldName(String name) throws IOException { - if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) { + if (!_writeContext.writeFieldName(name)) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(name); @@ -355,7 +376,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 (!_writeContext.writeFieldName(name.getValue())) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(name.getValue()); @@ -364,7 +385,7 @@ public final void writeFieldName(SerializableString name) throws IOException @Override public final void writeStringField(String fieldName, String value) throws IOException { - if (_writeContext.writeFieldName(fieldName) == JsonWriteContext.STATUS_EXPECT_VALUE) { + if (!_writeContext.writeFieldName(fieldName)) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(fieldName); @@ -378,16 +399,36 @@ private final void _writeFieldName(String name) throws IOException // not a low-level error, so: _reportMappingError("Unrecognized column '"+name+"', can not resolve without CsvSchema"); } - if (_skipWithin != null) { // new in 2.7 - _skipValue = true; - _nextColumnByName = -1; - return; +// if (_skipWithin != null) { // new in 2.7 +// _skipValue = true; +// _nextColumnByName = -1; +// return; +// } + + boolean internal = false; + _writeContext.truncatePath(_basePath); + if (_basePath.length() > _indentLength) { + String sep = _schema.pathSeparator(); + if (!sep.isEmpty()) { + _basePath.append(sep); + internal = true; + } } + // note: we are likely to get next column name, so pass it as hint + CsvEscapes.appendKey(_basePath, name); + + name = _basePath.toString(); + CsvSchema.Column col = _schema.column(name, _nextColumnByName+1); if (col == null) { if (isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { - _skipValue = true; + if (internal) { + _skipWithin = _writeContext; + _skipValue = true; + } else { + _skipValue = true; + } _nextColumnByName = -1; return; } @@ -397,6 +438,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(); + } /* @@ -499,7 +541,7 @@ && _skipValue && isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { _reportError("CSV generator does not support nested Array values"); } } - _writeContext = _writeContext.createChildArrayContext(); + _writeContext = _writeContext.createChildArrayContext(_basePath.length()); // and that's about it, really } @@ -534,25 +576,26 @@ 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() || - // 07-Nov-2017, tatu: But we may actually be nested indirectly; so check - (_writeContext.inArray() && !_writeContext.getParent().inRoot())) { - if (_skipWithin == null) { // new in 2.7 - if (_skipValue && isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { - _skipWithin = _writeContext; - } else { - _reportMappingError("CSV generator does not support Object values for properties (nested Objects)"); - } - } - } - _writeContext = _writeContext.createChildObjectContext(); +// if (_writeContext.inObject() || +// // 07-Nov-2017, tatu: But we may actually be nested indirectly; so check +// (_writeContext.inArray() && !_writeContext.getParent().inRoot())) { +// if (_skipWithin == null) { // new in 2.7 +// if (_skipValue && isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) { +// _skipWithin = _writeContext; +// } else { +// +// _reportMappingError("CSV generator does not support Object values for properties (nested Objects)"); +// } +// } +// } + _writeContext = _writeContext.createChildObjectContext(_basePath.length()); } @Override public final void writeEndObject() throws IOException { if (!_writeContext.inObject()) { - _reportError("Current context not Object but "+_writeContext.typeDesc()); + _reportError("Current context not Object but " + _writeContext.typeDesc()); } _writeContext = _writeContext.getParent(); // 14-Dec-2015, tatu: To complete skipping of ignored structured value, need this: @@ -560,6 +603,7 @@ public final void writeEndObject() throws IOException if (_writeContext == _skipWithin) { _skipWithin = null; } + return; } // not 100% fool-proof, but chances are row should be done now @@ -883,7 +927,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 (!_writeContext.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 @@ -903,8 +947,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 (!_writeContext.writeValue()) { _reportError("Can not "+typeMsg+", expecting field name"); } if (_handleFirstLine) { @@ -959,6 +1002,8 @@ protected void finishRow() throws IOException { _writer.endRow(); _nextColumnByName = -1; + _skipValue = false; + _basePath.setLength(0); } protected void _handleFirstLine() throws IOException diff --git a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java index 59ef9bc4..16f2be35 100644 --- a/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java +++ b/csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java @@ -1,83 +1,84 @@ - package com.fasterxml.jackson.dataformat.csv; -import java.util.*; - import com.fasterxml.jackson.core.FormatSchema; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + /** * Simple {@link FormatSchema} sub-type that defines properties of * a CSV document to read or write. * Properties supported currently are: - *