com.fasterxml.jackson.dataformat.csv.CsvGenerator Maven / Gradle / Ivy
package com.fasterxml.jackson.dataformat.csv;
import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.base.GeneratorBase;
import com.fasterxml.jackson.core.json.JsonWriteContext;
import com.fasterxml.jackson.core.io.IOContext;
import com.fasterxml.jackson.dataformat.csv.impl.CsvEncoder;
public class CsvGenerator extends GeneratorBase
{
/**
* Enumeration that defines all togglable features for CSV writers
* (if any: currently none)
*/
public enum Feature
implements FormatFeature // since 2.7
{
/**
* Feature that determines how much work is done before determining that
* a column value requires quoting: when set as true
, full
* check is made to only use quoting when it is strictly necessary;
* but when false
, a faster but more conservative check
* is made, and possibly quoting is used for values that might not need it.
* Trade-offs is basically between optimal/minimal quoting (true), and
* faster handling (false).
* Faster check involves only checking first N characters of value, as well
* as possible looser checks.
*
* Note, however, that regardless setting, all values that need to be quoted
* will be: it is just that when set to false
, other values may
* also be quoted (to avoid having to do more expensive checks).
*
* Default value is false
for "loose" (approximate, conservative)
* checking.
*
* @since 2.4
*/
STRICT_CHECK_FOR_QUOTING(false),
/**
* Feature that determines whether columns without matching value may be omitted,
* when they are the last values of the row.
* If true
, values and separators between values may be omitted, to slightly reduce
* length of the row; if false
, separators need to stay in place and values
* are indicated by empty Strings.
*
* @since 2.4
*/
OMIT_MISSING_TAIL_COLUMNS(false),
/**
* Feature that determines whether values written as Strings (from java.lang.String
* valued POJO properties) should be forced to be quoted, regardless of whether they
* actually need this.
* Note that this feature has precedence over {@link #STRICT_CHECK_FOR_QUOTING}, when
* both would be applicable.
*
* @since 2.5
*/
ALWAYS_QUOTE_STRINGS(false),
/**
* Feature that determines whether values written as empty Strings (from java.lang.String
* valued POJO properties) should be forced to be quoted.
*
* @since 2.9
*/
ALWAYS_QUOTE_EMPTY_STRINGS(false),
/**
* Feature that determines whether quote characters within quoted String values are escaped
* using configured escape character, instead of being "doubled up" (that is: a quote character
* is written twice in a row).
*
* Default value is false so that quotes are doubled as necessary, not escaped.
*
* @since 2.9.3
*/
ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR(false)
;
protected final boolean _defaultState;
protected final int _mask;
/**
* Method that calculates bit set (flags) of all features that
* are enabled by default.
*/
public static int collectDefaults()
{
int flags = 0;
for (Feature f : values()) {
if (f.enabledByDefault()) {
flags |= f.getMask();
}
}
return flags;
}
private Feature(boolean defaultState) {
_defaultState = defaultState;
_mask = (1 << ordinal());
}
@Override
public boolean enabledIn(int flags) { return (flags & _mask) != 0; }
@Override
public boolean enabledByDefault() { return _defaultState; }
@Override
public int getMask() { return _mask; }
}
protected final static long MIN_INT_AS_LONG = Integer.MIN_VALUE;
protected final static long MAX_INT_AS_LONG = Integer.MAX_VALUE;
/*
/**********************************************************
/* Configuration
/**********************************************************
*/
private final static CsvSchema EMPTY_SCHEMA;
static {
EMPTY_SCHEMA = CsvSchema.emptySchema();
}
final protected IOContext _ioContext;
/**
* Bit flag composed of bits that indicate which
* {@link CsvGenerator.Feature}s
* are enabled.
*/
protected int _formatFeatures;
/**
* Definition of columns being written, if available.
*/
protected CsvSchema _schema = EMPTY_SCHEMA;
// note: can not be final since we may need to re-create it for new schema
protected CsvEncoder _writer;
/*
/**********************************************************
/* Output state
/**********************************************************
*/
/**
* Flag that indicates that we need to write header line, if
* one is needed. Used because schema may be specified after
* instance is constructed.
*/
protected boolean _handleFirstLine = true;
/**
* Index of column that we will be getting next, based on
* field name call that was made.
*/
protected int _nextColumnByName = -1;
/**
* Flag set when property to write is unknown, and the matching value
* is to be skipped quietly.
*
* @since 2.5
*/
protected boolean _skipValue;
/**
* Separator to use during writing of (simple) array value, to be encoded as a
* single column value, if any.
*
* @since 2.5
*/
protected String _arraySeparator = CsvSchema.NO_ARRAY_ELEMENT_SEPARATOR;
/**
* Accumulated contents of an array cell, if any
*/
protected StringBuilder _arrayContents;
/**
* Additional counter that indicates number of value entries in the
* array. Needed because `null` entries do not add content, but need
* to be separated by array cell separator
*
* @since 2.7
*/
protected int _arrayElements;
/**
* When skipping output (for "unknown" output), outermost write context
* where skipping should occur
*
* @since 2.7
*/
protected JsonWriteContext _skipWithin;
/*
/**********************************************************
/* Life-cycle
/**********************************************************
*/
/**
* @since 2.4
*/
public CsvGenerator(IOContext ctxt, int jsonFeatures, int csvFeatures,
ObjectCodec codec, Writer out, CsvSchema schema)
{
super(jsonFeatures, codec);
_ioContext = ctxt;
_formatFeatures = csvFeatures;
_schema = schema;
_writer = new CsvEncoder(ctxt, csvFeatures, out, schema);
}
public CsvGenerator(IOContext ctxt, int jsonFeatures, int csvFeatures,
ObjectCodec codec, CsvEncoder csvWriter)
{
super(jsonFeatures, codec);
_ioContext = ctxt;
_formatFeatures = csvFeatures;
_writer = csvWriter;
}
/*
/**********************************************************
/* Versioned
/**********************************************************
*/
@Override
public Version version() {
return PackageVersion.VERSION;
}
/*
/**********************************************************
/* Overridden methods, configuration
/**********************************************************
*/
/**
* No way (or need) to indent anything, so let's block any attempts.
* (should we throw an exception instead?)
*/
@Override
public CsvGenerator useDefaultPrettyPrinter() {
return this;
}
/**
* No way (or need) to indent anything, so let's block any attempts.
* (should we throw an exception instead?)
*/
@Override
public CsvGenerator setPrettyPrinter(PrettyPrinter pp) {
return this;
}
@Override
public Object getOutputTarget() {
return _writer.getOutputTarget();
}
/**
* NOTE: while this method will return some information on amount of data buffered, it
* may be an incomplete view as some buffering happens at a higher level, as not-yet-serialized
* values.
*/
@Override
public int getOutputBuffered() {
return _writer.getOutputBuffered();
}
@Override
public void setSchema(FormatSchema schema)
{
if (schema instanceof CsvSchema) {
if (_schema != schema) {
_schema = (CsvSchema) schema;
_writer = _writer.withSchema(_schema);
}
} else {
super.setSchema(schema);
}
}
@Override
public int getFormatFeatures() {
return _formatFeatures;
}
@Override
public JsonGenerator overrideFormatFeatures(int values, int mask)
{
int oldF = _formatFeatures;
int newF = (_formatFeatures & ~mask) | (values & mask);
if (oldF != newF) {
_formatFeatures = newF;
_writer.overrideFormatFeatures(newF);
}
return this;
}
/*
/**********************************************************
/* Public API, capability introspection methods
/**********************************************************
*/
@Override
public boolean canUseSchema(FormatSchema schema) {
return (schema instanceof CsvSchema);
}
@Override
public boolean canOmitFields() {
// Nope: CSV requires at least a placeholder
return false;
}
@Override
public boolean canWriteFormattedNumbers() { return true; }
/*
/**********************************************************************
/* Overridden methods; writing field names
/**********************************************************************
*/
/* And then methods overridden to make final, streamline some
* aspects...
*/
@Override
public final void writeFieldName(String name) throws IOException
{
if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) {
_reportError("Can not write a field name, expecting a value");
}
_writeFieldName(name);
}
@Override
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) {
_reportError("Can not write a field name, expecting a value");
}
_writeFieldName(name.getValue());
}
@Override
public final void writeStringField(String fieldName, String value) throws IOException
{
if (_writeContext.writeFieldName(fieldName) == JsonWriteContext.STATUS_EXPECT_VALUE) {
_reportError("Can not write a field name, expecting a value");
}
_writeFieldName(fieldName);
writeString(value);
}
private final void _writeFieldName(String name) throws IOException
{
// just find the matching index -- must have schema for that
if (_schema == null) {
// 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;
}
// 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) {
if (isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) {
_skipValue = true;
_nextColumnByName = -1;
return;
}
// not a low-level error, so:
_reportMappingError("Unrecognized column '"+name+"': known columns: "+_schema.getColumnDesc());
}
_skipValue = false;
// and all we do is just note index to use for following value write
_nextColumnByName = col.getIndex();
}
/*
/**********************************************************
/* Extended API, configuration
/**********************************************************
*/
public final boolean isEnabled(Feature f) {
return (_formatFeatures & f.getMask()) != 0;
}
public CsvGenerator configure(Feature f, boolean state) {
if (state) {
return enable(f);
}
return disable(f);
}
public CsvGenerator enable(Feature f) {
_formatFeatures |= f.getMask();
_writer.overrideFormatFeatures(_formatFeatures);
return this;
}
public CsvGenerator disable(Feature f) {
_formatFeatures &= ~f.getMask();
_writer.overrideFormatFeatures(_formatFeatures);
return this;
}
/*
/**********************************************************
/* Public API: low-level I/O
/**********************************************************
*/
@Override
public final void flush() throws IOException {
_writer.flush(isEnabled(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM));
}
@Override
public void close() throws IOException
{
super.close();
// Let's mark row as closed, if we had any...
finishRow();
// Write the header if necessary, occurs when no rows written
if (_handleFirstLine) {
_handleFirstLine();
}
_writer.close(_ioContext.isResourceManaged() || isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET));
}
/*
/**********************************************************
/* Public API: structural output
/**********************************************************
*/
@Override
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 ((_skipWithin == null)
&& _skipValue && isEnabled(JsonGenerator.Feature.IGNORE_UNKNOWN)) {
_skipWithin = _writeContext;
} else if (!_skipValue) {
// First: column may have its own separator
String sep;
if (_nextColumnByName >= 0) {
CsvSchema.Column col = _schema.column(_nextColumnByName);
sep = col.isArray() ? col.getArrayElementSeparator() : CsvSchema.NO_ARRAY_ELEMENT_SEPARATOR;
} else {
sep = CsvSchema.NO_ARRAY_ELEMENT_SEPARATOR;
}
if (sep.isEmpty()) {
if (!_schema.hasArrayElementSeparator()) {
_reportError("CSV generator does not support Array values for properties without setting 'arrayElementSeparator' in schema");
}
sep = _schema.getArrayElementSeparator();
}
_arraySeparator = sep;
if (_arrayContents == null) {
_arrayContents = new StringBuilder();
} else {
_arrayContents.setLength(0);
}
_arrayElements = 0;
}
} else {
if (!_arraySeparator.isEmpty()) {
// also: no nested arrays, yet
_reportError("CSV generator does not support nested Array values");
}
}
_writeContext = _writeContext.createChildArrayContext();
// and that's about it, really
}
@Override
public final void writeEndArray() throws IOException
{
if (!_writeContext.inArray()) {
_reportError("Current context not Array but "+_writeContext.typeDesc());
}
_writeContext = _writeContext.getParent();
// 14-Dec-2015, tatu: To complete skipping of ignored structured value, need this:
if (_skipWithin != null) {
if (_writeContext == _skipWithin) {
_skipWithin = null;
}
return;
}
if (!_arraySeparator.isEmpty()) {
_arraySeparator = CsvSchema.NO_ARRAY_ELEMENT_SEPARATOR;
_writer.write(_columnIndex(), _arrayContents.toString());
}
// 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()) {
finishRow();
}
}
@Override
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();
}
@Override
public final void writeEndObject() throws IOException
{
if (!_writeContext.inObject()) {
_reportError("Current context not Object but "+_writeContext.typeDesc());
}
_writeContext = _writeContext.getParent();
// 14-Dec-2015, tatu: To complete skipping of ignored structured value, need this:
if (_skipWithin != null) {
if (_writeContext == _skipWithin) {
_skipWithin = null;
}
return;
}
// not 100% fool-proof, but chances are row should be done now
finishRow();
}
/*
/**********************************************************
/* Output method implementations, textual
/**********************************************************
*/
@Override
public void writeString(String text) throws IOException
{
if (text == null) {
writeNull();
return;
}
_verifyValueWrite("write String value");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(text);
} else {
_writer.write(_columnIndex(), text);
}
}
}
@Override
public void writeString(char[] text, int offset, int len) throws IOException
{
_verifyValueWrite("write String value");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(new String(text, offset, len));
} else {
_writer.write(_columnIndex(), text, offset, len);
}
}
}
@Override
public final void writeString(SerializableString sstr) throws IOException
{
_verifyValueWrite("write String value");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(sstr.getValue());
} else {
_writer.write(_columnIndex(), sstr.getValue());
}
}
}
@Override
public void writeRawUTF8String(byte[] text, int offset, int len) throws IOException {
_reportUnsupportedOperation();
}
@Override
public void writeUTF8String(byte[] text, int offset, int len) throws IOException {
writeString(new String(text, offset, len, "UTF-8"));
}
/*
/**********************************************************
/* Output method implementations, unprocessed ("raw")
/**********************************************************
*/
@Override
public void writeRaw(String text) throws IOException {
_writer.writeRaw(text);
}
@Override
public void writeRaw(String text, int offset, int len) throws IOException {
_writer.writeRaw(text, offset, len);
}
@Override
public void writeRaw(char[] text, int offset, int len) throws IOException {
_writer.writeRaw(text, offset, len);
}
@Override
public void writeRaw(char c) throws IOException {
_writer.writeRaw(c);
}
@Override
public void writeRawValue(String text) throws IOException {
_verifyValueWrite("write Raw value");
if (!_skipValue) {
// NOTE: ignore array stuff
_writer.writeNonEscaped(_columnIndex(), text);
}
}
@Override
public void writeRawValue(String text, int offset, int len) throws IOException {
_verifyValueWrite("write Raw value");
if (!_skipValue) {
// NOTE: ignore array stuff
_writer.writeNonEscaped(_columnIndex(), text.substring(offset, offset+len));
}
}
@Override
public void writeRawValue(char[] text, int offset, int len) throws IOException {
_verifyValueWrite("write Raw value");
if (!_skipValue) {
// NOTE: ignore array stuff
_writer.writeNonEscaped(_columnIndex(), new String(text, offset, len));
}
}
/*
/**********************************************************
/* Output method implementations, base64-encoded binary
/**********************************************************
*/
@Override
public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws IOException, JsonGenerationException
{
if (data == null) {
writeNull();
return;
}
_verifyValueWrite("write Binary value");
if (!_skipValue) {
// ok, better just Base64 encode as a String...
if (offset > 0 || (offset+len) != data.length) {
data = Arrays.copyOfRange(data, offset, offset+len);
}
String encoded = b64variant.encode(data);
if (!_arraySeparator.isEmpty()) {
_addToArray(encoded);
} else {
_writer.write(_columnIndex(), encoded);
}
}
}
/*
/**********************************************************
/* Output method implementations, primitive
/**********************************************************
*/
@Override
public void writeBoolean(boolean state) throws IOException
{
_verifyValueWrite("write boolean value");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(state ? "true" : "false");
} else {
_writer.write(_columnIndex(), state);
}
}
}
@Override
public void writeNull() throws IOException
{
_verifyValueWrite("write null value");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(_schema.getNullValueOrEmpty());
} else if (_writeContext.inObject()) {
_writer.writeNull(_columnIndex());
} else if (_writeContext.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()) {
_writer.writeNull(_columnIndex());
}
// ... so, for "root-level nulls" (with or without array-wrapping), we would do:
/*
_writer.writeNull(_columnIndex());
finishRow();
*/
}
}
}
@Override
public void writeNumber(int v) throws IOException
{
_verifyValueWrite("write number");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(String.valueOf(v));
} else {
_writer.write(_columnIndex(), v);
}
}
}
@Override
public void writeNumber(long v) throws IOException
{
// First: maybe 32 bits is enough?
if (v <= MAX_INT_AS_LONG && v >= MIN_INT_AS_LONG) {
writeNumber((int) v);
return;
}
_verifyValueWrite("write number");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(String.valueOf(v));
} else {
_writer.write(_columnIndex(), v);
}
}
}
@Override
public void writeNumber(BigInteger v) throws IOException
{
if (v == null) {
writeNull();
return;
}
_verifyValueWrite("write number");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(String.valueOf(v));
} else {
_writer.write(_columnIndex(), v.toString());
}
}
}
@Override
public void writeNumber(double v) throws IOException
{
_verifyValueWrite("write number");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(String.valueOf(v));
} else {
_writer.write(_columnIndex(), v);
}
}
}
@Override
public void writeNumber(float v) throws IOException
{
_verifyValueWrite("write number");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(String.valueOf(v));
} else {
_writer.write(_columnIndex(), v);
}
}
}
@Override
public void writeNumber(BigDecimal v) throws IOException
{
if (v == null) {
writeNull();
return;
}
_verifyValueWrite("write number");
if (!_skipValue) {
String str = isEnabled(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN)
? v.toPlainString() : v.toString();
if (!_arraySeparator.isEmpty()) {
_addToArray(String.valueOf(v));
} else {
_writer.write(_columnIndex(), str);
}
}
}
@Override
public void writeNumber(String encodedValue) throws IOException
{
if (encodedValue == null) {
writeNull();
return;
}
_verifyValueWrite("write number");
if (!_skipValue) {
if (!_arraySeparator.isEmpty()) {
_addToArray(encodedValue);
} else {
_writer.write(_columnIndex(), encodedValue);
}
}
}
/*
/**********************************************************
/* Overrides for field methods
/**********************************************************
*/
@Override
public void writeOmittedField(String fieldName) throws IOException
{
// Hmmh. Should we require a match? Actually, let's use logic: if field found,
// assumption is we must add a placeholder; if not, we can merely ignore
CsvSchema.Column col = _schema.column(fieldName);
if (col == null) {
// assumed to have been removed from schema too
} else {
// basically combination of "writeFieldName()" and "writeNull()"
if (_writeContext.writeFieldName(fieldName) == JsonWriteContext.STATUS_EXPECT_VALUE) {
_reportError("Can not skip a field, expecting a value");
}
// and all we do is just note index to use for following value write
_nextColumnByName = col.getIndex();
// We can basically copy what 'writeNull()' does...
_verifyValueWrite("skip positional value due to filtering");
_writer.write(_columnIndex(), "");
}
}
/*
/**********************************************************
/* Implementations for methods from base class
/**********************************************************
*/
@Override
protected final void _verifyValueWrite(String typeMsg) throws IOException
{
int status = _writeContext.writeValue();
if (status == JsonWriteContext.STATUS_EXPECT_NAME) {
_reportError("Can not "+typeMsg+", expecting field name");
}
if (_handleFirstLine) {
_handleFirstLine();
}
}
@Override
protected void _releaseBuffers() {
_writer._releaseBuffers();
}
/*
/**********************************************************
/* Internal methods, error reporting
/**********************************************************
*/
/**
* Method called when there is a problem related to mapping data
* (compared to a low-level generation); if so, should be surfaced
* as
*
* @since 2.7
*/
protected void _reportMappingError(String msg) throws JsonProcessingException {
throw CsvMappingException.from(this, msg, _schema);
// throw new JsonGenerationException(msg, this);
}
/*
/**********************************************************
/* Internal methods, other
/**********************************************************
*/
protected final int _columnIndex()
{
int ix = _nextColumnByName;
if (ix < 0) { // if we had one, remove now
ix = _writer.nextColumnIndex();
}
return ix;
}
/**
* Method called when the current row is complete; typically
* will flush possibly buffered column values, append linefeed
* and reset state appropriately.
*/
protected void finishRow() throws IOException
{
_writer.endRow();
_nextColumnByName = -1;
}
protected void _handleFirstLine() throws IOException
{
_handleFirstLine = false;
if (_schema.usesHeader()) {
int count = _schema.size();
if (count == 0) {
_reportMappingError("Schema specified that header line is to be written; but contains no column names");
}
for (CsvSchema.Column column : _schema) {
_writer.writeColumnName(column.getName());
}
_writer.endRow();
}
}
protected void _addToArray(String value) {
if (_arrayElements > 0) {
_arrayContents.append(_arraySeparator);
}
++_arrayElements;
_arrayContents.append(value);
}
protected void _addToArray(char[] value) {
if (_arrayElements > 0) {
_arrayContents.append(_arraySeparator);
}
++_arrayElements;
_arrayContents.append(value);
}
}