TomlGenerator.java
package tools.jackson.dataformat.toml;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.util.Arrays;
import tools.jackson.core.*;
import tools.jackson.core.base.GeneratorBase;
import tools.jackson.core.exc.StreamWriteException;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.NumberOutput;
import tools.jackson.core.util.JacksonFeatureSet;
import tools.jackson.core.util.VersionUtil;
final class TomlGenerator extends GeneratorBase
{
// As an optimization we try coalescing short writes into
// buffer; but pass longer directly.
protected final static int SHORT_WRITE = 100;
/*
/**********************************************************************
/* Configuration
/**********************************************************************
*/
/**
* Underlying {@link Writer} used for output.
*/
protected final Writer _out;
/**
* Caller-provided {@link OutputStream} that {@link #_out} was constructed (by us)
* to wrap, if any; {@code null} if {@link #_out} was handed to us by the caller.
*<p>
* A {@link Writer} we construct must always be flushed and closed: that is what
* hands its pending content over and returns its buffers to the recycler. It is
* shielded from this stream (see {@link tools.jackson.core.io.GuardedOutputStream}), which is instead
* flushed and closed directly, as per {@link StreamWriteFeature#FLUSH_PASSED_TO_STREAM}
* and {@link StreamWriteFeature#AUTO_CLOSE_TARGET}.
*
* @since 3.3
*/
protected final OutputStream _target;
private final int _tomlFeatures;
/*
/**********************************************************************
/* Output state
/**********************************************************************
*/
/**
* Current context, in form we can use it (GeneratorBase has
* untyped reference; left as null)
*/
protected TomlWriteContext _streamWriteContext;
/*
/**********************************************************************
/* Output buffering
/**********************************************************************
*/
/**
* Intermediate buffer in which contents are buffered before
* being written using {@code _out}.
*/
protected char[] _outputBuffer;
/**
* Pointer to the next available location in {@code _outputBuffer}
*/
protected int _outputTail = 0;
/**
* Offset to index after the last valid index in {@code _outputBuffer}.
* Typically same as length of the buffer.
*/
protected final int _outputEnd;
protected final StringBuilder _basePath = new StringBuilder(50);
/**
* Whether a key/value line (outside inline arrays and tables) has been started
* but not yet terminated: lets {@link #writeValueEnd()} write the line feed
* just once, regardless of how many times it is called. Lines are terminated
* right after the value; as a safety net, also before the next key/value line,
* and on {@link #flush()} and {@link #close()}.
*/
private boolean _valueLineOpen;
/*
/**********************************************************************
/* Life-cycle
/**********************************************************************
*/
public TomlGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
int stdFeatures, int tomlFeatures, Writer out) {
this(writeCtxt, ioCtxt, stdFeatures, tomlFeatures, out, null);
}
/**
* @param out Writer to write to: either provided by the caller, or constructed
* by Jackson to wrap {@code target}
* @param target Caller-provided stream that {@code out} wraps, if {@code out} was
* constructed by Jackson (and hence must always be flushed and closed);
* {@code null} if {@code out} was provided by the caller
*
* @since 3.3
*/
public TomlGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
int stdFeatures, int tomlFeatures, Writer out, OutputStream target) {
super(writeCtxt, ioCtxt, stdFeatures);
_target = target;
_tomlFeatures = tomlFeatures;
_streamWriteContext = TomlWriteContext.createRootContext();
_out = out;
_outputBuffer = ioCtxt.allocConcatBuffer();
_outputEnd = _outputBuffer.length;
}
/*
/**********************************************************************
/* Versioned
/**********************************************************************
*/
@Override
public Version version() {
return PackageVersion.VERSION;
}
/*
/**********************************************************************
/* Overridden methods, configuration
/**********************************************************************
*/
@Override
public Object streamWriteOutputTarget() {
// [dataformats-text#750]: Writer we constructed ourselves is an implementation
// detail, shielded from caller's stream: so expose the stream instead
return (_target != null) ? _target : _out;
}
@Override
public int streamWriteOutputBuffered() {
return _outputTail;
}
/*
/**********************************************************************
/* Overridden methods: low-level I/O
/**********************************************************************
*/
@Override
public void close() {
if (!isClosed()) {
try {
writeValueEnd();
_flushBuffer();
} catch (Throwable t) {
// [dataformats-text#750]: must close regardless, but without masking
// the original failure
_outputTail = 0; // just to ensure we don't think there's anything buffered
try {
super.close();
} catch (Throwable t2) {
t.addSuppressed(t2);
}
throw t;
}
super.close();
}
}
@Override
protected void _closeInput() throws IOException
{
if (_out == null) {
return;
}
final boolean closeTarget = _ioContext.isResourceManaged()
|| isEnabled(StreamWriteFeature.AUTO_CLOSE_TARGET);
if (_target == null) { // Writer is caller's: close or flush as per features
if (closeTarget) {
_out.close();
} else if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
// If we can't close it, we should at least flush
_out.flush();
}
return;
}
// [dataformats-text#735]: a Writer we constructed ourselves must be closed
// regardless: without that its buffered content never reaches the caller's
// OutputStream, and the buffer it took from the recycler is lost. Caller's
// stream is shielded from that, and closed (or flushed) as per features
// enabled now -- [dataformats-text#750] even if closing our Writer fails:
// try-with-resources ensures that, and reports the first failure (with
// any later one added as suppressed)
final Closeable target = closeTarget ? _target
: (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM) ? _target::flush : null);
try (target) {
_out.close();
}
}
@Override
public void flush() {
// [dataformats-text#750]: nothing to flush once closed -- and target may
// have been closed along with us
if (isClosed() || (_out == null)) {
return;
}
writeValueEnd();
_flushBuffer();
try {
if (_target != null) {
// [dataformats-text#735]: a Writer we constructed ourselves is just a
// buffer, so must be flushed regardless (caller's stream is shielded)
_out.flush();
}
if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
final Flushable target = (_target != null) ? _target : _out;
target.flush();
}
} catch (IOException e) {
throw _wrapIOFailure(e);
}
}
/*
/**********************************************************************
/* Implementations for methods from base class
/**********************************************************************
*/
@Override
protected void _releaseBuffers() {
char[] buf = _outputBuffer;
if (buf != null) {
_outputBuffer = null;
_ioContext.releaseConcatBuffer(buf);
}
}
protected void _flushBuffer() throws JacksonException {
if (_outputTail > 0 && _out != null) {
try {
_out.write(_outputBuffer, 0, _outputTail);
} catch (IOException e) {
throw _wrapIOFailure(e);
}
_outputTail = 0;
}
}
/*
/**********************************************************************
/* Internal methods; raw writes
/**********************************************************************
*/
protected JsonGenerator _writeRaw(char c) throws JacksonException {
if (_outputTail >= _outputEnd) {
_flushBuffer();
}
_outputBuffer[_outputTail++] = c;
return this;
}
protected JsonGenerator _writeRaw(String text) throws JacksonException {
return _writeRaw(text, 0, text.length());
}
protected JsonGenerator _writeRaw(String text, int offset, int len) throws JacksonException {
// Nothing to check, can just output as is
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(offset, offset + len, _outputBuffer, _outputTail);
_outputTail += len;
} else {
_writeRawLong(text, offset, len);
}
return this;
}
protected JsonGenerator _writeRaw(StringBuilder 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);
}
return this;
}
protected JsonGenerator _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 this;
}
// Otherwise, better just pass through:
_flushBuffer();
try {
_out.write(text, offset, len);
} catch (IOException e) {
throw _wrapIOFailure(e);
}
return this;
}
protected void _writeRawLong(String text, int offset, int len) throws JacksonException {
int room = _outputEnd - _outputTail;
text.getChars(offset, offset + room, _outputBuffer, _outputTail);
_outputTail += room;
_flushBuffer();
offset += room;
len -= 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;
}
protected void _writeRawLong(StringBuilder text) throws JacksonException {
int room = _outputEnd - _outputTail;
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;
}
/*
/**********************************************************************
/* Overridden output state handling methods
/**********************************************************************
*/
@Override
public TokenStreamContext streamWriteContext() {
return _streamWriteContext;
}
@Override
public Object currentValue() {
return _streamWriteContext.currentValue();
}
@Override
public void assignCurrentValue(Object v) {
_streamWriteContext.assignCurrentValue(v);
}
/*
/**********************************************************************
/* Overrides: capability introspection methods
/**********************************************************************
*/
@Override
public boolean canWriteObjectId() {
return false;
}
@Override
public boolean canWriteTypeId() {
return false;
}
@Override
public boolean canOmitProperties() {
return true;
}
@Override
public JacksonFeatureSet<StreamWriteCapability> streamWriteCapabilities() {
return DEFAULT_WRITE_CAPABILITIES;
}
/*
/**********************************************************************
/* Overrides: config access
/**********************************************************************
*/
@Override
public PrettyPrinter getPrettyPrinter() {
return null;
}
/*
/**********************************************************************
/* Overridden methods; writing property names
/**********************************************************************
*/
@Override
public JsonGenerator writeName(String name) throws JacksonException {
// [dataformats-text#781]: validate before updating context or writing anything
// (separator), so that a rejected key leaves generator usable; context checks first
// [dataformats-text#758]: names only allowed within an Object
if (!_streamWriteContext.inObject()) {
_reportError("Cannot write a property name outside an Object (current context: "
+ _streamWriteContext.typeDesc() + ")");
}
if (_streamWriteContext.hasPendingName()) {
_reportError("Cannot write a property name, expecting a value");
}
if (name == null) {
_reportError("Cannot write a `null` property name");
}
final char quote = _quoting(StringOutputUtil.categorize(name) & StringOutputUtil.MASK_SIMPLE_KEY);
// validity checked above (throws IllegalStateException if not)
_streamWriteContext.writeName(name);
if (_streamWriteContext._inline) {
if (_streamWriteContext.hasCurrentIndex()) {
_writeRaw(", ");
}
_writeQuoted(quote, name);
} else {
// Ok; append to base path at this point.
// First: ensure possibly preceding property name is removed:
_streamWriteContext.truncatePath(_basePath);
if (_basePath.length() > 0) {
_basePath.append('.');
}
_appendPropertyName(_basePath, name, quote);
}
return this;
}
@Override
public JsonGenerator writePropertyId(long id) throws JacksonException {
// 15-Aug-2019, tatu: should be improved to avoid String generation
return writeName(Long.toString(id));
}
/*
/**********************************************************************
/* Public API: structural output
/**********************************************************************
*/
@Override
public JsonGenerator writeStartArray() throws JacksonException {
return writeStartArray(null);
}
@Override
public JsonGenerator writeStartArray(Object currValue) throws JacksonException {
// arrays are always inline, force writing the current key
// NOTE: if this ever changes, we need to add empty array handling in writeEndArray
_verifyValueWrite("start an array", false);
_streamWriteContext = _streamWriteContext.createChildArrayContext(currValue,
_basePath.length());
streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
if (_streamWriteContext._inline) {
_writeRaw('[');
}
return this;
}
@Override
public JsonGenerator writeEndArray() throws JacksonException {
if (!_streamWriteContext.inArray()) {
_reportError("Current context not an Array but " + _streamWriteContext.typeDesc());
}
if (_streamWriteContext._inline) {
_writeRaw(']');
} else if (!_streamWriteContext.hasCurrentIndex()) {
// empty array
VersionUtil.throwInternal();
}
_streamWriteContext = _streamWriteContext.getParent();
return writeValueEnd();
}
@Override
public JsonGenerator writeStartObject() throws JacksonException {
return writeStartObject(null);
}
@Override
public JsonGenerator writeStartObject(Object forValue) throws JacksonException {
// objects aren't always materialized right now
_verifyValueWrite("start an object", true);
_streamWriteContext = _streamWriteContext.createChildObjectContext(forValue, _basePath.length());
streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
if (_streamWriteContext._inline) {
_writeRaw('{');
}
return this;
}
@Override
public JsonGenerator writeEndObject() throws JacksonException {
if (!_streamWriteContext.inObject()) {
_reportError("Current context not an Object but " + _streamWriteContext.typeDesc());
}
// [dataformats-text#758]: property name without value is an error
if (_streamWriteContext.hasPendingName()) {
_reportError("Cannot end an Object, expecting a value for property '"
+ _streamWriteContext.currentName() + "'");
}
if (_streamWriteContext._inline) {
_writeRaw('}');
} else if (!_streamWriteContext.hasCurrentIndex()
// [dataformats-text#758]: empty root-level Object is just an empty document
&& !_streamWriteContext.getParent().inRoot()) {
// empty object
writeCurrentPath();
_writeRaw("{}");
}
_streamWriteContext = _streamWriteContext.getParent();
return writeValueEnd();
}
/*
/**********************************************************************
/* Output method implementations, textual
/**********************************************************************
*/
@Override
public JsonGenerator writeString(String text) throws JacksonException {
if (text == null) {
return writeNull();
}
// validate before writing anything (key, separator); root check first
_verifyNotAtRoot("write String value");
final char quote = _quoting(_stringValueCategories(StringOutputUtil.categorize(text)));
_verifyValueWrite("write String value");
_writeQuoted(quote, text);
return writeValueEnd();
}
@Override
public JsonGenerator writeString(char[] text, int offset, int len) throws JacksonException {
// NOTE: also reports `null` array as error
_checkRangeBoundsForCharArray(text, offset, len);
// validate before writing anything (key, separator); root check first
_verifyNotAtRoot("write String value");
final char quote = _quoting(_stringValueCategories(StringOutputUtil.categorize(text, offset, len)));
_verifyValueWrite("write String value");
if (quote == QUOTE_ESCAPED) {
_writeQuoted(quote, new String(text, offset, len));
} else { // common case: no escaping needed, so no need to construct String
_writeRaw(quote);
_writeRaw(text, offset, len);
_writeRaw(quote);
}
return writeValueEnd();
}
@Override
public JsonGenerator writeRawUTF8String(byte[] text, int offset, int len) throws JacksonException {
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeUTF8String(byte[] text, int offset, int len) throws JacksonException {
// NOTE: also reports `null` array as error
_checkRangeBoundsForByteArray(text, offset, len);
return writeString(new String(text, offset, len, StandardCharsets.UTF_8));
}
/*
/**********************************************************************
/* Output method implementations, unprocessed ("raw")
/**********************************************************************
*/
@Override
public JsonGenerator writeRaw(String text) throws JacksonException {
return _writeRaw(text);
}
@Override
public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException {
_checkRangeBoundsForString(text, offset, len);
return _writeRaw(text, offset, len);
}
@Override
public JsonGenerator writeRaw(char[] text, int offset, int len) throws JacksonException {
_checkRangeBoundsForCharArray(text, offset, len);
return _writeRaw(text, offset, len);
}
@Override
public JsonGenerator writeRaw(char c) throws JacksonException {
return _writeRaw(c);
}
@Override
public JsonGenerator writeRaw(SerializableString text) throws JacksonException {
return _writeRaw(text);
}
private JsonGenerator _writeRaw(SerializableString text) throws JacksonException {
// Copy unquoted chars directly into buffer if they fit; otherwise
// fall back to (unquoted) String value
int len = text.appendUnquoted(_outputBuffer, _outputTail);
if (len < 0) {
return _writeRaw(text.getValue());
}
_outputTail += len;
return this;
}
// Raw values need value-end handling (newline in table context) just
// like regular values; `GeneratorBase` implementations do not do that
@Override
public JsonGenerator writeRawValue(String text) throws JacksonException {
_verifyValueWrite("write raw value");
_writeRaw(text);
return writeValueEnd();
}
@Override
public JsonGenerator writeRawValue(String text, int offset, int len) throws JacksonException {
_checkRangeBoundsForString(text, offset, len);
_verifyValueWrite("write raw value");
_writeRaw(text, offset, len);
return writeValueEnd();
}
@Override
public JsonGenerator writeRawValue(char[] text, int offset, int len) throws JacksonException {
_checkRangeBoundsForCharArray(text, offset, len);
_verifyValueWrite("write raw value");
_writeRaw(text, offset, len);
return writeValueEnd();
}
@Override
public JsonGenerator writeRawValue(SerializableString text) throws JacksonException {
_verifyValueWrite("write raw value");
_writeRaw(text);
return writeValueEnd();
}
/*
/**********************************************************************
/* Output method implementations, base64-encoded binary
/**********************************************************************
*/
@Override
public JsonGenerator writeBinary(Base64Variant b64variant, byte[] data, int offset, int len)
throws JacksonException {
if (data == null) {
return writeNull();
}
_checkRangeBoundsForByteArray(data, offset, len);
_verifyValueWrite("write Binary value");
// 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);
// base64 needs no escaping
_writeRaw('\'');
_writeRaw(encoded);
_writeRaw('\'');
return writeValueEnd();
}
/*
/**********************************************************************
/* Output method implementations, scalars
/**********************************************************************
*/
@Override
public JsonGenerator writeBoolean(boolean state) throws JacksonException {
_verifyValueWrite("write boolean value");
_writeRaw(state ? "true" : "false");
return writeValueEnd();
}
@Override
public JsonGenerator writeNumber(short v) throws JacksonException {
return writeNumber((int) v);
}
@Override
public JsonGenerator writeNumber(int i) throws JacksonException {
_verifyValueWrite("write number");
// up to 10 digits and possible minus sign
if ((_outputTail + 11) > _outputEnd) {
_flushBuffer();
}
_outputTail = NumberOutput.outputInt(i, _outputBuffer, _outputTail);
return writeValueEnd();
}
@Override
public JsonGenerator writeNumber(long l) throws JacksonException {
_verifyValueWrite("write number");
// up to 19 digits and possible minus sign
if ((_outputTail + 20) > _outputEnd) {
_flushBuffer();
}
_outputTail = NumberOutput.outputLong(l, _outputBuffer, _outputTail);
return writeValueEnd();
}
@Override
public JsonGenerator writeNumber(BigInteger v) throws JacksonException {
if (v == null) {
return writeNull();
}
_verifyValueWrite("write number");
_writeRaw(v.toString());
return writeValueEnd();
}
@Override
public JsonGenerator writeNumber(double d) throws JacksonException {
_verifyValueWrite("write number");
// Non-finite values need TOML tokens (`nan`/`inf`/`-inf`): Java text
// forms (`NaN`/`Infinity`) are not valid TOML and cannot be read back
if (NumberOutput.notFinite(d)) {
_writeRaw(_nonFiniteTomlToken(d));
} else if (isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)) {
if ((_outputTail + NumberOutput.MAX_DOUBLE_BYTES) > _outputEnd) {
_flushBuffer();
}
_outputTail = NumberOutput.outputDouble(d, _outputBuffer, _outputTail);
} else {
_writeRaw(NumberOutput.toString(d, false));
}
return writeValueEnd();
}
@Override
public JsonGenerator writeNumber(float f) throws JacksonException {
_verifyValueWrite("write number");
if (NumberOutput.notFinite(f)) {
_writeRaw(_nonFiniteTomlToken(f));
} else if (isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)) {
if ((_outputTail + NumberOutput.MAX_FLOAT_BYTES) > _outputEnd) {
_flushBuffer();
}
_outputTail = NumberOutput.outputFloat(f, _outputBuffer, _outputTail);
} else {
_writeRaw(NumberOutput.toString(f, false));
}
return writeValueEnd();
}
/**
* Maps a non-finite floating-point value to the TOML float token
* ({@code nan}, {@code inf} or {@code -inf}).
*/
private static String _nonFiniteTomlToken(double d) {
if (Double.isNaN(d)) {
return "nan";
}
return (d > 0) ? "inf" : "-inf";
}
@Override
public JsonGenerator writeNumber(BigDecimal dec) throws JacksonException {
if (dec == null) {
return writeNull();
}
_verifyValueWrite("write number");
String str = isEnabled(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) ? dec.toPlainString() : dec.toString();
_writeRaw(str);
return writeValueEnd();
}
@Override
public JsonGenerator writeNumber(String encodedValue) throws JacksonException {
if (encodedValue == null) {
return writeNull();
}
_verifyValueWrite("write number");
_writeRaw(encodedValue);
return writeValueEnd();
}
@Override
public JsonGenerator writeNull() throws JacksonException {
if (TomlWriteFeature.FAIL_ON_NULL_WRITE.enabledIn(_tomlFeatures)) {
throw new TomlStreamWriteException(this, "TOML null writing disabled (TomlWriteFeature.FAIL_ON_NULL_WRITE)");
}
// [dataformats-text#758]: root-level null written as empty Object (empty document)
if (_streamWriteContext.inRoot()) {
_verifySingleRootValue("write null value");
writeStartObject();
return writeEndObject();
}
// as empty String
final char quote = _quoting(_stringValueCategories(StringOutputUtil.categorize("")));
_verifyValueWrite("write null value");
_writeQuoted(quote, "");
return writeValueEnd();
}
/*
/**********************************************************************
/* Implementations for methods from base class
/**********************************************************************
*/
@Override
protected void _verifyValueWrite(String typeMsg) throws JacksonException {
_verifyValueWrite(typeMsg, false);
}
/**
* @param isObject Whether value to write is an Object: if not, value is not
* allowed at root level, and its key is written right away (Objects are
* not materialized until their contents are written)
*/
protected void _verifyValueWrite(String typeMsg, boolean isObject) throws JacksonException {
if (isObject) {
_verifySingleRootValue(typeMsg);
} else {
_verifyNotAtRoot(typeMsg);
}
// check that name/value cadence works
if (!_streamWriteContext.writeValue()) {
_reportError("Cannot " + typeMsg + ", expecting a property name");
}
if (_streamWriteContext._inline) {
// write separators, if we're inline the key is already there
if (_streamWriteContext.inArray()) {
if (_streamWriteContext.getCurrentIndex() != 0) {
_writeRaw(", ");
}
} else {
_writeRaw(" = ");
}
} else {
// write the key if necessary
if (!isObject) {
writeCurrentPath();
}
}
}
/**
* [dataformats-text#758]: TOML document is a table, so only an Object may be
* written at root level. Called before any other validation of the value.
*
* @since 3.3
*/
private void _verifyNotAtRoot(String typeMsg) throws JacksonException {
if (_streamWriteContext.inRoot()) {
_reportError("Cannot " + typeMsg + " at root level: TOML document must be an Object");
}
}
/**
* [dataformats-text#758]: TOML document is a single table, so only one value
* (Object) may be written at root level.
*
* @since 3.3
*/
private void _verifySingleRootValue(String typeMsg) throws JacksonException {
if (_streamWriteContext.inRoot() && _streamWriteContext.hasCurrentIndex()) {
_reportError("Cannot " + typeMsg
+ " at root level: TOML document must be a single Object, and one has already been written");
}
}
// All write failures reported via `_reportError()` should be TOML-specific
@Override
protected StreamWriteException _constructWriteException(String msg) {
return new TomlStreamWriteException(this, msg);
}
@Override
protected StreamWriteException _constructWriteException(String msg, Throwable t) {
return new TomlStreamWriteException(this, msg, t);
}
// Starts every key/value line: terminate preceding one, in case it was not yet
private void writeCurrentPath() {
writeValueEnd();
_writeRaw(_basePath);
_writeRaw(" = ");
_valueLineOpen = true;
}
/**
* Terminates the current key/value line, if one is open (and we are not within
* an inline array or table): the only place that writes value-terminating line feeds.
*/
private JsonGenerator writeValueEnd() {
if (_valueLineOpen && !_streamWriteContext._inline) {
_valueLineOpen = false;
_writeRaw('\n');
}
return this;
}
/*
/**********************************************************************
/* String support
/**********************************************************************
*/
// Quoting styles returned by `_quoting()`, in addition to quote characters
private final static char QUOTE_NONE = 0;
private final static char QUOTE_ESCAPED = 1; // basic String, with escapes
/**
* Determines how to quote a key or String value with given categories: the only
* place where TOML quoting rules are decided.
*
* @param cat Categories of text (see {@link StringOutputUtil}), already masked
* for the context (key or value)
*
* @return {@link #QUOTE_NONE}, {@link #QUOTE_ESCAPED}, or quote character to use
* (no escaping needed)
*/
private char _quoting(int cat) {
if ((cat & StringOutputUtil.UNQUOTED_KEY) != 0) {
return QUOTE_NONE;
}
if ((cat & StringOutputUtil.LITERAL_STRING) != 0) {
return '\'';
}
if ((cat & StringOutputUtil.BASIC_STRING_NO_ESCAPE) != 0) {
return '"';
}
if ((cat & StringOutputUtil.BASIC_STRING) != 0) {
return QUOTE_ESCAPED;
}
// String values are validated before writing (see _stringValueCategories()),
// so only keys can get here
throw _reportUnsupportedKeyCharacters();
}
/**
* @param quote Quoting to use, as returned by {@link #_quoting}
*/
private void _appendPropertyName(StringBuilder path, String name, char quote) {
// NOTE: we do NOT yet write the key; wait until we have value; just append to path
if (quote == QUOTE_ESCAPED) {
_appendEscaped(path, name);
} else if (quote == QUOTE_NONE) {
path.append(name);
} else {
path.append(quote).append(name).append(quote);
}
}
/**
* @param quote Quoting to use, as returned by {@link #_quoting}
*/
private void _writeQuoted(char quote, String text) {
if (quote == QUOTE_ESCAPED) {
// rare enough to not bother escaping directly into output buffer
_writeRaw(_appendEscaped(new StringBuilder(text.length() + 16), text));
} else if (quote == QUOTE_NONE) {
_writeRaw(text);
} else {
_writeRaw(quote);
_writeRaw(text);
_writeRaw(quote);
}
}
/**
* Appends given text as a basic String, with escapes: the only implementation
* of TOML String escaping.
*/
private static StringBuilder _appendEscaped(StringBuilder sb, String text) {
sb.append('"');
int start = 0; // start of not-yet-appended run of chars needing no escaping
for (int i = 0, len = text.length(); i < len; i++) {
String escape = StringOutputUtil.getBasicStringEscape(text.charAt(i));
if (escape != null) {
sb.append(text, start, i).append(escape);
start = i + 1;
}
}
return sb.append(text, start, text.length()).append('"');
}
private JacksonException _reportUnsupportedKeyCharacters() {
return new TomlStreamWriteException(this, "Key contains unsupported characters");
}
/**
* Masks categories of a String value and verifies it can be written; to be called
* before {@link #_verifyValueWrite} so that nothing is written for invalid values.
*/
private int _stringValueCategories(int cat) throws JacksonException {
cat &= StringOutputUtil.MASK_STRING;
if ((cat & (StringOutputUtil.LITERAL_STRING | StringOutputUtil.BASIC_STRING
| StringOutputUtil.BASIC_STRING_NO_ESCAPE)) == 0) {
throw new TomlStreamWriteException(this, "String value contains unsupported characters");
}
return cat;
}
/*
/**********************************************************************
/* Time types
/**********************************************************************
*/
@Override
public JsonGenerator writePOJO(Object value) throws JacksonException {
if (value == null) {
// important: call method that does check value write:
writeNull();
} else if (
value instanceof LocalDate ||
value instanceof LocalTime ||
value instanceof LocalDateTime ||
value instanceof OffsetDateTime) {
_verifyValueWrite("write local date");
_writeRaw(value.toString());
writeValueEnd();
} else {
_objectWriteContext.writeValue(this, value);
}
return this;
}
}