CsvEncoder.java
package tools.jackson.dataformat.csv.impl;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import tools.jackson.core.JacksonException;
import tools.jackson.core.exc.JacksonIOException;
import tools.jackson.core.io.CharTypes;
import tools.jackson.core.io.CharacterEscapes;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.NumberOutput;
import tools.jackson.dataformat.csv.CsvSchema;
import tools.jackson.dataformat.csv.CsvWriteException;
import tools.jackson.dataformat.csv.CsvWriteFeature;
/**
* Helper class that handles actual low-level construction of
* CSV output, based only on indexes given without worrying about reordering,
* or binding from logical properties.
*/
public class CsvEncoder
{
// Default set of escaped characters (none)
private static final int [] sOutputEscapes = new int[0];
// Upper case hex chars:
protected final static char[] HEX_CHARS = CharTypes.copyHexChars(true);
/**
* As an optimization we try coalescing short writes into
* buffer; but pass longer directly.
*/
protected final static int SHORT_WRITE = 32;
/**
* Default maximum length of a column value that will be inspected
* character-by-character to determine whether quoting is needed
* (when not using {@link CsvWriteFeature#STRICT_CHECK_FOR_QUOTING}).
* Values longer than this are always quoted as a performance optimization.
*<p>
* Configurable via {@link tools.jackson.dataformat.csv.CsvFactoryBuilder#maxQuoteCheckChars(int)}.
*
* @since 3.2
*/
public final static int DEFAULT_MAX_QUOTE_CHECK = 24;
protected final BufferedValue[] NO_BUFFERED = new BufferedValue[0];
private final static char[] TRUE_CHARS = "true".toCharArray();
private final static char[] FALSE_CHARS = "false".toCharArray();
/**
* Currently active set of output escape code definitions (whether
* and how to escape or not).
*/
protected int[] _outputEscapes = sOutputEscapes;
/*
/**********************************************************************
/* Configuration
/**********************************************************************
*/
protected final IOContext _ioContext;
/**
* Underlying {@link Writer} used for output.
*/
protected final Writer _out;
/**
* Flag that indicates whether {@link #_out} was constructed by Jackson (wrapping a
* caller-provided {@link java.io.OutputStream}) rather than handed to us by the
* caller. A {@link Writer} we construct must always be closed: that is what flushes
* its pending content into the stream and returns its buffers to the recycler --
* whether the stream underneath goes with it is a separate decision, made in
* {@link #close}.
*
* @since 3.3
*/
protected final boolean _ownsWriter;
protected final char _cfgColumnSeparator;
protected final int _cfgQuoteCharacter;
protected final int _cfgEscapeCharacter;
protected final char[] _cfgLineSeparator;
protected final char[] _cfgNullValue;
protected final int _cfgLineSeparatorLength;
protected final int _cfgMaxQuoteCheckChars;
/**
* Lowest-valued character that is safe to output without using
* quotes around value, NOT including possible escape character.
*/
protected final int _cfgMinSafeChar;
protected final int _csvFeatures;
/**
* Marker flag used to determine if to do optimal (aka "strict") quoting
* checks or not (looser conservative check)
*/
protected final boolean _cfgOptimalQuoting;
protected final boolean _cfgAllowsComments;
protected final boolean _cfgIncludeMissingTail;
protected final boolean _cfgAlwaysQuoteStrings;
protected final boolean _cfgAlwaysQuoteEmptyStrings;
// @since 2.16
protected final boolean _cfgAlwaysQuoteNumbers;
// @since 3.2
protected final boolean _cfgQuoteLeadingTrailingWhitespace;
protected final boolean _cfgEscapeQuoteCharWithEscapeChar;
protected final boolean _cfgEscapeControlCharWithEscapeChar;
/**
* @since 2.14
*/
protected final boolean _cfgUseFastDoubleWriter;
protected final char _cfgQuoteCharEscapeChar;
protected final char _cfgControlCharEscapeChar;
/*
/**********************************************************************
/* Output state
/**********************************************************************
*/
protected int _columnCount;
/**
* Index of column we expect to write next
*/
protected int _nextColumnToWrite = 0;
/**
* And if output comes in shuffled order we will need to do
* bit of ordering.
*/
protected BufferedValue[] _buffered = NO_BUFFERED;
/**
* Index of the last buffered value
*/
protected int _lastBuffered = -1;
protected boolean _trailingLFRemoved = false;
/*
/**********************************************************************
/* Output buffering, low-level
/**********************************************************************
*/
/**
* Intermediate buffer in which contents are buffered before
* being written using {@link #_out}.
*/
protected char[] _outputBuffer;
/**
* Flag that indicates whether the <code>_outputBuffer</code> is recyclable (and
* needs to be returned to recycler once we are done) or not.
*/
protected boolean _bufferRecyclable;
/**
* Pointer to the next available char position in {@link #_outputBuffer}
*/
protected int _outputTail = 0;
/**
* Offset to index after the last valid index in {@link #_outputBuffer}.
* Typically same as length of the buffer.
*/
protected final int _outputEnd;
/**
* Let's keep track of how many bytes have been output, may prove useful
* when debugging. This does <b>not</b> include bytes buffered in
* the output buffer, just bytes that have been written using underlying
* stream writer.
*/
protected int _charsWritten;
/*
/**********************************************************************
/* Construction, (re)configuration
/**********************************************************************
*/
/**
* @since 3.2
*/
public CsvEncoder(IOContext ctxt, int csvFeatures, Writer out, CsvSchema schema,
CharacterEscapes esc, boolean useFastDoubleWriter,
int maxQuoteCheckChars)
{
this(ctxt, csvFeatures, out, schema, esc, useFastDoubleWriter,
maxQuoteCheckChars, false);
}
/**
* @param ownsWriter Whether {@code out} was constructed by Jackson (and hence must
* always be closed), or provided by the caller
*
* @since 3.3
*/
public CsvEncoder(IOContext ctxt, int csvFeatures, Writer out, CsvSchema schema,
CharacterEscapes esc, boolean useFastDoubleWriter,
int maxQuoteCheckChars, boolean ownsWriter)
{
_ioContext = ctxt;
_ownsWriter = ownsWriter;
_csvFeatures = csvFeatures;
_cfgUseFastDoubleWriter = useFastDoubleWriter;
_cfgOptimalQuoting = CsvWriteFeature.STRICT_CHECK_FOR_QUOTING.enabledIn(csvFeatures);
_cfgIncludeMissingTail = !CsvWriteFeature.OMIT_MISSING_TAIL_COLUMNS.enabledIn(_csvFeatures);
_cfgAlwaysQuoteStrings = CsvWriteFeature.ALWAYS_QUOTE_STRINGS.enabledIn(csvFeatures);
_cfgAlwaysQuoteEmptyStrings = CsvWriteFeature.ALWAYS_QUOTE_EMPTY_STRINGS.enabledIn(csvFeatures);
_cfgAlwaysQuoteNumbers = CsvWriteFeature.ALWAYS_QUOTE_NUMBERS.enabledIn(csvFeatures);
_cfgQuoteLeadingTrailingWhitespace = CsvWriteFeature.QUOTE_STRINGS_WITH_LEADING_TRAILING_WHITESPACE.enabledIn(csvFeatures);
_cfgEscapeQuoteCharWithEscapeChar = CsvWriteFeature.ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR.enabledIn(csvFeatures);
_cfgEscapeControlCharWithEscapeChar = CsvWriteFeature.ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR.enabledIn(csvFeatures);
_outputBuffer = ctxt.allocConcatBuffer();
_bufferRecyclable = true;
_outputEnd = _outputBuffer.length;
_out = out;
_cfgColumnSeparator = schema.getColumnSeparator();
_cfgQuoteCharacter = schema.getQuoteChar();
_cfgEscapeCharacter = schema.getEscapeChar();
_cfgLineSeparator = schema.getLineSeparator();
_cfgLineSeparatorLength = (_cfgLineSeparator == null) ? 0 : _cfgLineSeparator.length;
_cfgNullValue = schema.getNullValueOrEmpty();
_cfgAllowsComments = schema.allowsComments();
_columnCount = schema.size();
_outputEscapes = (esc == null) ? sOutputEscapes : esc.getEscapeCodesForAscii();
_cfgMinSafeChar = _calcSafeChar();
_cfgMaxQuoteCheckChars = maxQuoteCheckChars;
_cfgQuoteCharEscapeChar = _getQuoteCharEscapeChar(
_cfgEscapeQuoteCharWithEscapeChar,
_cfgQuoteCharacter,
_cfgEscapeCharacter
);
_cfgControlCharEscapeChar = _cfgEscapeCharacter > 0 ? (char) _cfgEscapeCharacter : '\\';
_verifyConfiguration(schema);
}
@Deprecated // @since 3.2
public CsvEncoder(IOContext ctxt, int csvFeatures, Writer out, CsvSchema schema,
CharacterEscapes esc, boolean useFastDoubleWriter)
{
this(ctxt, csvFeatures, out, schema, esc, useFastDoubleWriter,
DEFAULT_MAX_QUOTE_CHECK);
}
public CsvEncoder(CsvEncoder base, CsvSchema newSchema)
{
_ioContext = base._ioContext;
_ownsWriter = base._ownsWriter;
_csvFeatures = base._csvFeatures;
_cfgUseFastDoubleWriter = base._cfgUseFastDoubleWriter;
_cfgOptimalQuoting = base._cfgOptimalQuoting;
_cfgIncludeMissingTail = base._cfgIncludeMissingTail;
_cfgAlwaysQuoteStrings = base._cfgAlwaysQuoteStrings;
_cfgAlwaysQuoteEmptyStrings = base._cfgAlwaysQuoteEmptyStrings;
_cfgAlwaysQuoteNumbers = base._cfgAlwaysQuoteNumbers;
_cfgQuoteLeadingTrailingWhitespace = base._cfgQuoteLeadingTrailingWhitespace;
_cfgEscapeQuoteCharWithEscapeChar = base._cfgEscapeQuoteCharWithEscapeChar;
_cfgEscapeControlCharWithEscapeChar = base._cfgEscapeControlCharWithEscapeChar;
_outputBuffer = base._outputBuffer;
_bufferRecyclable = base._bufferRecyclable;
_outputEnd = base._outputEnd;
_out = base._out;
_cfgMaxQuoteCheckChars = base._cfgMaxQuoteCheckChars;
_outputEscapes = base._outputEscapes;
_cfgColumnSeparator = newSchema.getColumnSeparator();
_cfgQuoteCharacter = newSchema.getQuoteChar();
_cfgEscapeCharacter = newSchema.getEscapeChar();
_cfgLineSeparator = newSchema.getLineSeparator();
_cfgLineSeparatorLength = _cfgLineSeparator.length;
_cfgNullValue = newSchema.getNullValueOrEmpty();
_cfgAllowsComments = newSchema.allowsComments();
_cfgMinSafeChar = _calcSafeChar();
_columnCount = newSchema.size();
_cfgQuoteCharEscapeChar = _getQuoteCharEscapeChar(
base._cfgEscapeQuoteCharWithEscapeChar,
newSchema.getQuoteChar(),
newSchema.getEscapeChar()
);
_cfgControlCharEscapeChar = _cfgEscapeCharacter > 0 ? (char) _cfgEscapeCharacter : '\\';
_verifyConfiguration(newSchema);
}
private void _verifyConfiguration(CsvSchema schema)
{
// 21-Feb-2023, tatu: [dataformats-text#374]: Need to verify that Escape character
// is defined if need to use it
if (_cfgEscapeQuoteCharWithEscapeChar || _cfgEscapeControlCharWithEscapeChar) {
if (!schema.usesEscapeChar()) {
throw CsvWriteException.from(null,
"Cannot use `CsvGenerator.Feature.ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR` or `CsvGenerator.Feature.ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR`"
+" if no escape character defined in `CsvSchema`",
schema);
}
}
}
private final char _getQuoteCharEscapeChar(
final boolean escapeQuoteCharWithEscapeChar,
final int quoteCharacter,
final int escapeCharacter)
{
final char quoteEscapeChar;
if (escapeQuoteCharWithEscapeChar && escapeCharacter > 0) {
quoteEscapeChar = (char) escapeCharacter;
}
else if (quoteCharacter > 0) {
quoteEscapeChar = (char) quoteCharacter;
}
else {
quoteEscapeChar = '\\';
}
return quoteEscapeChar;
}
private final int _calcSafeChar()
{
// note: quote char may be -1 to signify "no quoting":
int min = Math.max(_cfgColumnSeparator, _cfgQuoteCharacter);
// 06-Nov-2015, tatu: We will NOT apply escape character, because it usually
// has higher ascii value (with backslash); better handle separately.
// 23-Sep-2020, tatu: Should not actually need to consider anything but the
// first character when checking... but leaving rest for now
for (int i = 0; i < _cfgLineSeparatorLength; ++i) {
min = Math.max(min, _cfgLineSeparator[i]);
}
return min+1;
}
public CsvEncoder withSchema(CsvSchema schema) {
return new CsvEncoder(this, schema);
}
public CsvEncoder setOutputEscapes(int[] esc) {
_outputEscapes = (esc != null) ? esc : sOutputEscapes;
return this;
}
/*
/**********************************************************************
/* Read-access to output state
/**********************************************************************
*/
public Object getOutputTarget() {
return _out;
}
/**
* NOTE: while value does indeed indicate amount that has been written in the buffer,
* there may be more intermediate data that is buffered as values but not yet in
* buffer.
*/
public int getOutputBuffered() {
return _outputTail;
}
public int nextColumnIndex() {
return _nextColumnToWrite;
}
/*
/**********************************************************************
/* Writer API, writes from generator
/**********************************************************************
*/
public final void write(int columnIndex, String value) throws JacksonException
{
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
appendValue(value);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.buffered(value));
}
public final void write(int columnIndex, char[] ch, int offset, int len) throws JacksonException
{
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
appendValue(ch, offset, len);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.buffered(new String(ch, offset, len)));
}
public final void write(int columnIndex, int value) throws JacksonException
{
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
// inlined 'appendValue(int)'
// up to 10 digits and possible minus sign, leading comma, possible quotes
if ((_outputTail + 14) > _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
_outputTail = NumberOutput.outputInt(value, _outputBuffer, _outputTail);
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.buffered(value));
}
public final void write(int columnIndex, long value) throws JacksonException
{
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
// inlined 'appendValue(int)'
// up to 20 digits, minus sign, leading comma, possible quotes
if ((_outputTail + 24) > _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
_outputTail = NumberOutput.outputLong(value, _outputBuffer, _outputTail);
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.buffered(value));
}
public final void write(int columnIndex, BigInteger value) throws JacksonException
{
// easy case: all in order
final String numStr = value.toString();
if (columnIndex == _nextColumnToWrite) {
appendNumberValue(numStr);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.bufferedNumber(numStr));
}
public final void write(int columnIndex, float value) throws JacksonException
{
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
appendValue(value);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.buffered(value));
}
public final void write(int columnIndex, double value) throws JacksonException
{
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
appendValue(value);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.buffered(value));
}
public final void write(int columnIndex, BigDecimal value, boolean plain) throws JacksonException
{
final String numStr = plain ? value.toPlainString() : value.toString();
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
appendNumberValue(numStr);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.bufferedNumber(numStr));
}
public final void write(int columnIndex, boolean value) throws JacksonException
{
// easy case: all in order
if (columnIndex == _nextColumnToWrite) {
appendValue(value);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.buffered(value));
}
public final void writeNonEscaped(int columnIndex, String rawValue) throws JacksonException
{
if (columnIndex == _nextColumnToWrite) {
appendRawValue(rawValue);
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.bufferedRaw(rawValue));
}
public final void writeNull(int columnIndex) throws JacksonException
{
if (columnIndex == _nextColumnToWrite) {
appendNull();
++_nextColumnToWrite;
return;
}
_buffer(columnIndex, BufferedValue.bufferedNull());
}
public final void writeColumnName(String name) throws JacksonException
{
appendValue(name);
++_nextColumnToWrite;
}
public void endRow() throws JacksonException
{
// First things first; any buffered?
if (_lastBuffered >= 0) {
final int last = _lastBuffered;
_lastBuffered = -1;
for (; _nextColumnToWrite <= last; ++_nextColumnToWrite) {
BufferedValue value = _buffered[_nextColumnToWrite];
if (value != null) {
_buffered[_nextColumnToWrite] = null;
value.write(this);
} else if (_nextColumnToWrite > 0) { // ) {
// note: write method triggers prepending of separator; but for missing
// values we need to do it explicitly.
appendColumnSeparator();
}
}
} else if (_nextColumnToWrite <= 0) { // empty line; do nothing
return;
}
// Any missing values?
if (_nextColumnToWrite < _columnCount) {
if (_cfgIncludeMissingTail) {
do {
appendColumnSeparator();
} while (++_nextColumnToWrite < _columnCount);
}
}
// write line separator
_nextColumnToWrite = 0;
if ((_outputTail + _cfgLineSeparatorLength) > _outputEnd) {
_flushBuffer();
}
System.arraycopy(_cfgLineSeparator, 0, _outputBuffer, _outputTail, _cfgLineSeparatorLength);
_outputTail += _cfgLineSeparatorLength;
}
/*
/**********************************************************************
/* Writer API, writes via buffered values
/**********************************************************************
*/
protected void appendValue(String value) throws JacksonException
{
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
appendColumnSeparator();
}
final int len = value.length();
// First: cases where no scanning of content is needed
if (_cfgAlwaysQuoteStrings) {
_writeQuotedValue(value);
return;
}
if (_cfgQuoteCharacter < 0) { // quoting disabled
writeRaw(value);
return;
}
if (_needsQuotesWithoutContentScan(value, len, _nextColumnToWrite)) {
_writeQuotedValue(value);
return;
}
// Common case is that of no quoting needed: so make a speculative copy
// into output buffer and scan that (single pass over content, instead of
// scanning String and then copying). If quoting turns out to be needed,
// quoted write simply overwrites the speculative copy.
if ((_outputTail + len) > _outputEnd) {
if (len > _outputEnd) { // too long to fit even in empty buffer
if (_needsQuoting(value)) {
_writeQuotedValue(value);
} else {
writeRaw(value);
}
return;
}
_flushBuffer();
}
value.getChars(0, len, _outputBuffer, _outputTail);
if (_needsQuoting(_outputBuffer, _outputTail, len)) {
_writeQuotedValue(value);
} else {
_outputTail += len;
}
}
/**
* Variant of {@link #appendValue(String)} for {@code char[]} input:
* common case of no quoting needed is handled without constructing
* a {@code String}.
*
* @since 3.3
*/
protected void appendValue(char[] ch, int offset, int len) throws JacksonException
{
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
appendColumnSeparator();
}
if (_cfgAlwaysQuoteStrings) {
_writeQuotedValue(new String(ch, offset, len));
return;
}
if (_cfgQuoteCharacter < 0) { // quoting disabled
writeRaw(ch, offset, len);
return;
}
if (_needsQuotesWithoutContentScan(ch, offset, len, _nextColumnToWrite)) {
_writeQuotedValue(new String(ch, offset, len));
return;
}
if ((_outputTail + len) > _outputEnd) {
if (len > _outputEnd) { // too long to fit even in empty buffer
if (_needsQuoting(ch, offset, len)) {
_writeQuotedValue(new String(ch, offset, len));
} else {
writeRaw(ch, offset, len);
}
return;
}
_flushBuffer();
}
System.arraycopy(ch, offset, _outputBuffer, _outputTail, len);
if (_needsQuoting(_outputBuffer, _outputTail, len)) {
_writeQuotedValue(new String(ch, offset, len));
} else {
_outputTail += len;
}
}
private void _writeQuotedValue(String value) throws JacksonException
{
if (_cfgEscapeCharacter > 0) {
_writeQuotedAndEscaped(value, (char) _cfgEscapeCharacter);
} else {
_writeQuoted(value);
}
}
protected void appendRawValue(String value) throws JacksonException
{
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
appendColumnSeparator();
}
writeRaw(value);
}
protected void appendValue(int value) throws JacksonException
{
// up to 10 digits and possible minus sign, leading comma, possible quotes
if ((_outputTail + 14) > _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
_outputTail = NumberOutput.outputInt(value, _outputBuffer, _outputTail);
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
}
protected void appendValue(long value) throws JacksonException
{
// up to 20 digits, minus sign, leading comma, possible quotes
if ((_outputTail + 24) > _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
_outputTail = NumberOutput.outputLong(value, _outputBuffer, _outputTail);
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
}
protected void appendValue(float value) throws JacksonException
{
if (_cfgUseFastDoubleWriter) {
// Fast path: Schubfach writes straight into the output buffer,
// no intermediate String. Quoting (if enabled) is applied inline,
// same as for int/long values.
// up to MAX_FLOAT_BYTES chars, leading comma, possible quotes
if ((_outputTail + NumberOutput.MAX_FLOAT_BYTES + 3) > _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
_outputTail = NumberOutput.outputFloat(value, _outputBuffer, _outputTail);
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
return;
}
String str = NumberOutput.toString(value, false);
final int len = str.length();
if ((_outputTail + len) >= _outputEnd) { // >= to include possible comma too
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
writeNumber(str);
}
protected void appendValue(double value) throws JacksonException
{
if (_cfgUseFastDoubleWriter) {
// Fast path: Schubfach writes straight into the output buffer,
// no intermediate String. Quoting (if enabled) is applied inline,
// same as for int/long values.
// up to MAX_DOUBLE_BYTES chars, leading comma, possible quotes
if ((_outputTail + NumberOutput.MAX_DOUBLE_BYTES + 3) > _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
_outputTail = NumberOutput.outputDouble(value, _outputBuffer, _outputTail);
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
}
return;
}
String str = NumberOutput.toString(value, false);
final int len = str.length();
if ((_outputTail + len) >= _outputEnd) { // >= to include possible comma too
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
writeNumber(str);
}
// @since 2.16: pre-encoded BigInteger/BigDecimal value
protected void appendNumberValue(String numStr) throws JacksonException
{
// Same as "appendRawValue()", except may want quoting
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
appendColumnSeparator();
}
writeNumber(numStr);
}
protected void appendValue(boolean value) throws JacksonException {
_append(value ? TRUE_CHARS : FALSE_CHARS);
}
protected void appendNull() throws JacksonException {
_append(_cfgNullValue);
}
protected void _append(char[] ch) throws JacksonException {
final int len = ch.length;
if ((_outputTail + len) >= _outputEnd) { // >= to include possible comma too
_flushBuffer();
}
if (_nextColumnToWrite > 0) {
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
if (len > 0) {
System.arraycopy(ch, 0, _outputBuffer, _outputTail, len);
}
_outputTail += len;
}
protected void appendColumnSeparator() throws JacksonException {
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
}
/*
/**********************************************************************
/* Output methods, unprocessed ("raw")
/**********************************************************************
*/
public void writeRaw(String text) throws JacksonException
{
// Nothing to check, can just output as is
int len = text.length();
int room = _outputEnd - _outputTail;
if (room == 0) {
_flushBuffer();
room = _outputEnd - _outputTail;
}
// But would it nicely fit in? If yes, it's easy
if (room >= len) {
text.getChars(0, len, _outputBuffer, _outputTail);
_outputTail += len;
} else {
writeRawLong(text);
}
}
public void writeRaw(String text, int start, int len) throws JacksonException
{
// Nothing to check, can just output as is
int room = _outputEnd - _outputTail;
if (room < len) {
_flushBuffer();
room = _outputEnd - _outputTail;
}
// But would it nicely fit in? If yes, it's easy
if (room >= len) {
text.getChars(start, start+len, _outputBuffer, _outputTail);
_outputTail += len;
} else {
writeRawLong(text.substring(start, start+len));
}
}
public void writeRaw(char[] text, int offset, int len) throws JacksonException
{
// Only worth buffering if it's a short write?
if (len < SHORT_WRITE) {
int room = _outputEnd - _outputTail;
if (len > room) {
_flushBuffer();
}
System.arraycopy(text, offset, _outputBuffer, _outputTail, len);
_outputTail += len;
return;
}
// Otherwise, better just pass through:
_flushBuffer();
try {
_out.write(text, offset, len);
} catch (IOException e) {
throw _wrapIOFailure(e);
}
}
public void writeRaw(char c) throws JacksonException
{
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = c;
}
private void writeRawLong(String text) throws JacksonException
{
int room = _outputEnd - _outputTail;
// If not, need to do it by looping
text.getChars(0, room, _outputBuffer, _outputTail);
_outputTail += room;
_flushBuffer();
int offset = room;
int len = text.length() - room;
while (len > _outputEnd) {
int amount = _outputEnd;
text.getChars(offset, offset+amount, _outputBuffer, 0);
_outputTail = amount;
_flushBuffer();
offset += amount;
len -= amount;
}
// And last piece (at most length of buffer)
text.getChars(offset, offset+len, _outputBuffer, 0);
_outputTail = len;
}
// @since 2.16
private void writeNumber(String text) throws JacksonException
{
final int len = text.length();
if ((_outputTail + len + 2) > _outputEnd) {
_flushBuffer();
}
if (_cfgAlwaysQuoteNumbers) {
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
text.getChars(0, len, _outputBuffer, _outputTail);
_outputTail += len;
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
} else {
text.getChars(0, len, _outputBuffer, _outputTail);
_outputTail += len;
}
}
/*
/**********************************************************************
/* Output methods, with quoting and escaping
/**********************************************************************
*/
public void _writeQuoted(String text) throws JacksonException
{
final int[] escCodes = _outputEscapes;
final int escLen = escCodes.length;
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
// NOTE: caller should guarantee quote char is valid (not -1) at this point:
final char q = (char) _cfgQuoteCharacter;
_outputBuffer[_outputTail++] = q;
// simple case: if we have enough room, no need for boundary checks
final int len = text.length();
if ((_outputTail + len + len) >= _outputEnd) {
_writeLongQuoted(text, q);
return;
}
// 22-Jan-2015, tatu: Common case is that of no quoting needed, so let's
// make a speculative copy, then scan
// 06-Nov-2015, tatu: Not sure if copy actually improves perf; it did with
// older JVMs (1.5 at least), but not sure about 1.8 and later
final char[] buf = _outputBuffer;
int ptr = _outputTail;
text.getChars(0, len, buf, ptr);
final int end = ptr+len;
for (; ptr < end; ++ptr) {
char c = buf[ptr];
// see if any of the characters need escaping.
// if yes, fall back to the more convoluted write method
if ((c == q) || (c < escLen && escCodes[c] != 0)) {
break; // for
}
}
if (ptr == end) { // all good, no quoting or escaping!
_outputBuffer[ptr] = q;
_outputTail = ptr+1;
} else { // doh. do need quoting
_writeQuoted(text, q, ptr - _outputTail);
}
}
protected void _writeQuoted(String text, char q, int i) throws JacksonException
{
final int[] escCodes = _outputEscapes;
final int escLen = escCodes.length;
final char[] buf = _outputBuffer;
_outputTail += i;
final int len = text.length();
for (; i < len; ++i) {
char c = text.charAt(i);
if (c < escLen) {
int escCode = escCodes[c];
if (escCode != 0) { // for escape control and double quotes, c will be 0
_appendCharacterEscape(c, escCode);
continue; // for
}
}
if (c == q) { // double up
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
buf[_outputTail++] = _cfgQuoteCharEscapeChar; // this will be the quote
}
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
buf[_outputTail++] = c;
}
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
buf[_outputTail++] = q;
}
private final void _writeLongQuoted(String text, char q) throws JacksonException
{
final int[] escCodes = _outputEscapes;
final int escLen = escCodes.length;
final int len = text.length();
for (int i = 0; i < len; ++i) {
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
char c = text.charAt(i);
if (c < escLen) {
int escCode = escCodes[c];
if (escCode != 0) { // for escape control and double quotes, c will be 0
_appendCharacterEscape(c, escCode);
continue; // for
}
}
if (c == q) { // double up
_outputBuffer[_outputTail++] = _cfgQuoteCharEscapeChar;
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
}
_outputBuffer[_outputTail++] = c;
}
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = q;
}
public void _writeQuotedAndEscaped(String text, char esc) throws JacksonException
{
final int[] escCodes = _outputEscapes;
final int escLen = escCodes.length;
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
// NOTE: caller should guarantee quote char is valid (not -1) at this point:
final char q = (char) _cfgQuoteCharacter;
_outputBuffer[_outputTail++] = q;
final int len = text.length();
if ((_outputTail + len + len) >= _outputEnd) {
_writeLongQuotedAndEscaped(text, esc);
return;
}
final char[] buf = _outputBuffer;
int ptr = _outputTail;
text.getChars(0, len, buf, ptr);
final int end = ptr+len;
for (; ptr < end; ++ptr) {
char c = buf[ptr];
if ((c == q) || (c == esc) || (c < escLen && escCodes[c] != 0)) {
break;
}
}
if (ptr == end) { // all good, no quoting or escaping!
_outputBuffer[ptr] = q;
_outputTail = ptr+1;
} else { // quoting AND escaping
_writeQuotedAndEscaped(text, q, esc, ptr - _outputTail);
}
}
protected void _writeQuotedAndEscaped(String text, char q, char esc, int i) throws JacksonException
{
final int[] escCodes = _outputEscapes;
final int escLen = escCodes.length;
final char[] buf = _outputBuffer;
_outputTail += i;
final int len = text.length();
for (; i < len; ++i) {
char c = text.charAt(i);
if (c < escLen) {
int escCode = escCodes[c];
if (escCode != 0) { // for escape control and double quotes, c will be 0
_appendCharacterEscape(c, escCode);
continue; // for
}
}
if (c == q) { // double up
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = _cfgQuoteCharEscapeChar;
} else if (c == esc) { // double up
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = _cfgControlCharEscapeChar;
}
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
buf[_outputTail++] = c;
}
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
buf[_outputTail++] = q;
}
private final void _writeLongQuotedAndEscaped(String text, char esc) throws JacksonException
{
final int[] escCodes = _outputEscapes;
final int escLen = escCodes.length;
final int len = text.length();
// NOTE: caller should guarantee quote char is valid (not -1) at this point:
final char q = (char) _cfgQuoteCharacter;
// 23-Sep-2020, tatu: Why was this defined but not used? Commented out in 2.11.3
// final char quoteEscape = _cfgEscapeQuoteCharWithEscapeChar ? esc : q;
for (int i = 0; i < len; ++i) {
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
char c = text.charAt(i);
if (c < escLen) {
int escCode = escCodes[c];
if (escCode != 0) { // for escape control and double quotes, c will be 0
_appendCharacterEscape(c, escCode);
continue; // for
}
}
if (c == q) { // double up
_outputBuffer[_outputTail++] = _cfgQuoteCharEscapeChar;
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
} else if (c == esc) { // double up
_outputBuffer[_outputTail++] = _cfgControlCharEscapeChar;
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
}
_outputBuffer[_outputTail++] = c;
}
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = q;
}
/*
/**********************************************************************
/* Writer API, state changes
/**********************************************************************
*/
public void flush(boolean flushStream) throws IOException
{
_flushBuffer();
// 18-Sep-2026, tatu: [dataformats-text#719] content buffered in a Writer of ours
// has to be handed over to the caller's OutputStream here too, and not just on
// `close()`: `FLUSH_PASSED_TO_STREAM` only decides whether the stream itself is
// flushed, not whether our own buffers reach it
if (_ownsWriter && (_out instanceof UTF8Writer utf8w)) {
utf8w.flush(flushStream);
} else if (flushStream) {
_out.flush();
}
}
public void close(boolean autoClose, boolean flushStream) throws IOException
{
// May need to remove the linefeed appended after the last row written
// (if not yet done)
if (!CsvWriteFeature.WRITE_LINEFEED_AFTER_LAST_ROW.enabledIn(_csvFeatures)) {
_removeTrailingLF();
}
try {
_flushBuffer();
} finally {
// 08-Sep-2026, pjfanning: [dataformats-text#719] a Writer we constructed
// ourselves must be closed regardless of `autoClose`: without that its
// buffered content never reaches the caller's OutputStream, and the buffer
// it took from the recycler is lost. `autoClose` only decides whether the
// caller's target is closed along with it -- and that has to be decided
// here and not when the Writer was constructed, since stream-write
// features may be changed on the generator after that point.
if (_ownsWriter) {
// NOTE: `finally` so that a failing flush() does not leave our Writer
// unclosed -- that would lose its encoding buffer from the recycler
try {
if (!autoClose && flushStream) {
// If we can't close the target, we should at least flush it
_out.flush();
}
} finally {
if (_out instanceof UTF8Writer utf8w) {
utf8w.close(autoClose);
} else { // should not happen, but let's not lose content if it does
_out.close();
}
}
} else if (autoClose) {
_out.close();
} else if (flushStream) {
// If we can't close it, we should at least flush
_out.flush();
}
// Internal buffer(s) generator has can now be released as well
_releaseBuffers();
}
}
private void _removeTrailingLF() throws IOException {
if (!_trailingLFRemoved) {
_trailingLFRemoved = true;
// Remove trailing LF if (but only if) it appears to be in output
// buffer (may not be possible if `flush()` has been called)
_outputTail = Math.max(0, _outputTail - _cfgLineSeparatorLength);
}
}
/*
/**********************************************************************
/* Internal methods
/**********************************************************************
*/
/**
* Checks that can determine need for quoting without scanning content
* (leading/trailing whitespace, comment marker, length, empty String).
* Returns {@code false} if content scan ({@link #_needsQuoting}) is
* still needed to decide.
*/
private boolean _needsQuotesWithoutContentScan(String value, int length, int columnIndex)
{
if (length == 0) {
return _needsQuotesWithoutContentScan((char) 0, (char) 0, length, columnIndex);
}
return _needsQuotesWithoutContentScan(value.charAt(0), value.charAt(length - 1),
length, columnIndex);
}
/**
* Variant of {@link #_needsQuotesWithoutContentScan(String,int,int)} for
* {@code char[]} input.
*/
private boolean _needsQuotesWithoutContentScan(char[] ch, int offset, int length, int columnIndex)
{
if (length == 0) {
return _needsQuotesWithoutContentScan((char) 0, (char) 0, length, columnIndex);
}
return _needsQuotesWithoutContentScan(ch[offset], ch[offset + length - 1],
length, columnIndex);
}
// NOTE: `first` and `last` only meaningful if `length > 0`
private boolean _needsQuotesWithoutContentScan(char first, char last, int length, int columnIndex)
{
// [dataformats-text#210]: check for leading/trailing whitespace
if (_cfgQuoteLeadingTrailingWhitespace && length > 0) {
if (first <= ' ' || last <= ' ') {
return true;
}
}
// may skip checks unless we want exact checking
if (_cfgOptimalQuoting) {
// 31-Dec-2014, tatu: Comment lines start with # so quote if starts with #
// 28-May-2021, tatu: As per [dataformats-text#270] only check if first column
return _cfgAllowsComments && (columnIndex == 0)
&& (length > 0) && (first == '#');
}
if (length > _cfgMaxQuoteCheckChars) {
return true;
}
if (_cfgEscapeCharacter > 0) {
return false;
}
return _cfgAlwaysQuoteEmptyStrings && length == 0;
}
/**
* Content scan to determine whether quoting is needed; strict or loose
* depending on configuration.
*<p>
* NOTE: only reached for values too long to be copied into the output
* buffer; shorter ones are scanned via {@link #_needsQuoting(char[],int,int)}.
*/
private boolean _needsQuoting(String value)
{
if (_cfgOptimalQuoting) {
if (_cfgEscapeCharacter > 0) {
return _needsQuotingStrict(value, _cfgEscapeCharacter);
}
return _needsQuotingStrict(value);
}
if (_cfgEscapeCharacter > 0) {
return _needsQuotingLoose(value, _cfgEscapeCharacter);
}
return _needsQuotingLoose(value);
}
/**
* Content scan over a {@code char[]} region; same logic as
* {@link #_needsQuoting(String)}.
*/
private boolean _needsQuoting(char[] ch, int offset, int len)
{
final int end = offset + len;
if (_cfgOptimalQuoting) {
if (_cfgEscapeCharacter > 0) {
return _needsQuotingStrict(ch, offset, end, _cfgEscapeCharacter);
}
return _needsQuotingStrict(ch, offset, end);
}
if (_cfgEscapeCharacter > 0) {
return _needsQuotingLoose(ch, offset, end, _cfgEscapeCharacter);
}
return _needsQuotingLoose(ch, offset, end);
}
/**
* NOTE: keep in sync with {@link #_needsQuotingLoose(char[],int,int)};
* verified by {@code QuotingScanParityTest}.
*
* @since 2.4
*/
protected final boolean _needsQuotingLoose(String value)
{
char esc1 = _cfgQuoteCharEscapeChar;
char esc2 = _cfgControlCharEscapeChar;
for (int i = 0, len = value.length(); i < len; ++i) {
char c = value.charAt(i);
if ((c < _cfgMinSafeChar)
|| (c == esc1)
|| (c == esc2)) {
return true;
}
}
return false;
}
// NOTE: keep in sync with `_needsQuotingLoose(char[],int,int,int)`;
// verified by `QuotingScanParityTest`
protected final boolean _needsQuotingLoose(String value, int esc)
{
for (int i = 0, len = value.length(); i < len; ++i) {
int ch = value.charAt(i);
if ((ch < _cfgMinSafeChar) || (ch == esc)) {
return true;
}
}
return false;
}
/**
* Single source of truth for the "does this character force quoting" decision
* under strict ({@code STRICT_CHECK_FOR_QUOTING}) checking; shared by all
* {@code _needsQuotingStrict()} variants so that the rules only ever need to
* be changed in one place.
*<p>
* NOTE: only to be called for characters below {@code _cfgMinSafeChar}.
*/
// 23-Sep-2020, tatu: [dataformats-text#217] Must also ensure line separator
// leads to quoting
// 17-Dec-2025, tatu: [dataformats-text#479] Must check for ALL line separators
// (LF, CR) per RFC 4180, not just the configured schema line separator.
// NOTE: escCodes is empty by default (unless custom CharacterEscapes provided),
// so we cannot rely on `escCodes[c] != 0` check to catch newlines - we must
// check for them explicitly.
private boolean _quotingTriggerStrict(int c, int[] escCodes, int lfFirst)
{
return (c == _cfgColumnSeparator) || (c == _cfgQuoteCharacter)
|| (c < escCodes.length && escCodes[c] != 0)
|| (c == lfFirst)
// Per RFC 4180: must quote if contains LF or CR
|| (c == '\n') || (c == '\r');
}
protected boolean _needsQuotingStrict(String value)
{
final int minSafe = _cfgMinSafeChar;
final int[] escCodes = _outputEscapes;
final int lfFirst = (_cfgLineSeparatorLength == 0) ? 0 : _cfgLineSeparator[0];
for (int i = 0, len = value.length(); i < len; ++i) {
int c = value.charAt(i);
if (c < minSafe) {
if (_quotingTriggerStrict(c, escCodes, lfFirst)) {
return true;
}
}
}
return false;
}
protected boolean _needsQuotingStrict(String value, int esc)
{
final int minSafe = _cfgMinSafeChar;
final int[] escCodes = _outputEscapes;
final int lfFirst = (_cfgLineSeparatorLength == 0) ? 0 : _cfgLineSeparator[0];
for (int i = 0, len = value.length(); i < len; ++i) {
int c = value.charAt(i);
if (c < minSafe) {
if (_quotingTriggerStrict(c, escCodes, lfFirst)) {
return true;
}
} else if (c == esc) {
return true;
}
}
return false;
}
// NOTE: keep in sync with `_needsQuotingLoose(String)`
private boolean _needsQuotingLoose(char[] ch, int i, int end)
{
final char esc1 = _cfgQuoteCharEscapeChar;
final char esc2 = _cfgControlCharEscapeChar;
final int minSafe = _cfgMinSafeChar;
for (; i < end; ++i) {
char c = ch[i];
if ((c < minSafe) || (c == esc1) || (c == esc2)) {
return true;
}
}
return false;
}
// NOTE: keep in sync with `_needsQuotingLoose(String,int)`
private boolean _needsQuotingLoose(char[] ch, int i, int end, int esc)
{
final int minSafe = _cfgMinSafeChar;
for (; i < end; ++i) {
int c = ch[i];
if ((c < minSafe) || (c == esc)) {
return true;
}
}
return false;
}
// NOTE: keep in sync with `_needsQuotingStrict(String)` (per-character
// rules themselves shared via `_quotingTriggerStrict()`)
private boolean _needsQuotingStrict(char[] ch, int i, int end)
{
final int minSafe = _cfgMinSafeChar;
final int[] escCodes = _outputEscapes;
final int lfFirst = (_cfgLineSeparatorLength == 0) ? 0 : _cfgLineSeparator[0];
for (; i < end; ++i) {
int c = ch[i];
if (c < minSafe) {
if (_quotingTriggerStrict(c, escCodes, lfFirst)) {
return true;
}
}
}
return false;
}
// NOTE: keep in sync with `_needsQuotingStrict(String,int)`
private boolean _needsQuotingStrict(char[] ch, int i, int end, int esc)
{
final int minSafe = _cfgMinSafeChar;
final int[] escCodes = _outputEscapes;
final int lfFirst = (_cfgLineSeparatorLength == 0) ? 0 : _cfgLineSeparator[0];
for (; i < end; ++i) {
int c = ch[i];
if (c < minSafe) {
if (_quotingTriggerStrict(c, escCodes, lfFirst)) {
return true;
}
} else if (c == esc) {
return true;
}
}
return false;
}
protected void _buffer(int index, BufferedValue v)
{
_lastBuffered = Math.max(_lastBuffered, index);
if (index >= _buffered.length) {
_buffered = Arrays.copyOf(_buffered, Math.max(index+1, _columnCount));
}
_buffered[index] = v;
}
protected void _flushBuffer() throws JacksonException
{
if (_outputTail > 0) {
_charsWritten += _outputTail;
try {
_out.write(_outputBuffer, 0, _outputTail);
} catch (IOException e) {
throw _wrapIOFailure(e);
}
_outputTail = 0;
}
}
public void _releaseBuffers()
{
char[] buf = _outputBuffer;
if (buf != null && _bufferRecyclable) {
_outputBuffer = null;
_ioContext.releaseConcatBuffer(buf);
}
}
/**
* Method called to append escape sequence for given character, at the
* end of standard output buffer; or if not possible, write out directly.
*/
private void _appendCharacterEscape(char ch, int escCode)
throws JacksonException
{
if (escCode >= 0) { // \\N (2 char)
if ((_outputTail + 2) > _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = _cfgControlCharEscapeChar;
_outputBuffer[_outputTail++] = (char) escCode;
return;
}
if ((_outputTail + 5) >= _outputEnd) {
_flushBuffer();
}
int ptr = _outputTail;
char[] buf = _outputBuffer;
buf[ptr++] = '\\';
buf[ptr++] = 'u';
// We know it's a control char, so only the last 2 chars are non-0
if (ch > 0xFF) { // beyond 8 bytes
int hi = (ch >> 8) & 0xFF;
buf[ptr++] = HEX_CHARS[hi >> 4];
buf[ptr++] = HEX_CHARS[hi & 0xF];
ch &= 0xFF;
} else {
buf[ptr++] = '0';
buf[ptr++] = '0';
}
buf[ptr++] = HEX_CHARS[ch >> 4];
buf[ptr++] = HEX_CHARS[ch & 0xF];
_outputTail = ptr;
return;
}
// @since 3.0: defined by basic JsonParser/JsonGenerator but since we are
// not extending need to copy here
protected JacksonException _wrapIOFailure(IOException e) {
return JacksonIOException.construct(e);
}
}