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;
    }
}