package com.fasterxml.jackson.dataformat.toml;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.base.GeneratorBase;
import com.fasterxml.jackson.core.io.IOContext;
import com.fasterxml.jackson.core.util.VersionUtil;

import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.Arrays;

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
    /**********************************************************************
     */

    protected final IOContext _ioContext;

    
Underlying Writer used for output.
/** * Underlying {@link Writer} used for output. */
protected final Writer _out; private final int _tomlFeatures; /* /********************************************************************** /* Output state /********************************************************************** */
Current context, in form we can use it (GeneratorBase has untyped reference; left as null)
/** * 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 _out.
/** * Intermediate buffer in which contents are buffered before * being written using {@code _out}. */
protected char[] _outputBuffer;
Pointer to the next available location in _outputBuffer
/** * Pointer to the next available location in {@code _outputBuffer} */
protected int _outputTail = 0;
Offset to index after the last valid index in _outputBuffer. Typically same as length of the buffer.
/** * 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); /* /********************************************************************** /* Life-cycle /********************************************************************** */ public TomlGenerator(IOContext ioCtxt, int stdFeatures, int tomlFeatures, ObjectCodec codec, Writer out) { super(stdFeatures, codec); _ioContext = ioCtxt; _tomlFeatures = tomlFeatures; _streamWriteContext = TomlWriteContext.createRootContext(); _out = out; _outputBuffer = ioCtxt.allocConcatBuffer(); _outputEnd = _outputBuffer.length; } /* /********************************************************************** /* Versioned /********************************************************************** */ @Override public Version version() { return PackageVersion.VERSION; } /* /********************************************************************** /* Overridden methods: low-level I/O /********************************************************************** */ @Override public void close() throws IOException { super.close(); _flushBuffer(); _outputTail = 0; // just to ensure we don't think there's anything buffered if (_out != null) { if (_ioContext.isResourceManaged() || isEnabled(StreamWriteFeature.AUTO_CLOSE_TARGET)) { _out.close(); } else if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) { // 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(); } @Override public void flush() throws IOException { _flushBuffer(); if (_out != null) { if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) { _out.flush(); } } } /* /********************************************************************** /* 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 IOException { if (_outputTail > 0) { _out.write(_outputBuffer, 0, _outputTail); _outputTail = 0; } } /* /********************************************************************** /* Internal methods; raw writes /********************************************************************** */ protected void _writeRaw(char c) throws IOException { if (_outputTail >= _outputEnd) { _flushBuffer(); } _outputBuffer[_outputTail++] = c; } protected void _writeRaw(String text) throws IOException { // 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); } } protected void _writeRaw(StringBuilder text) throws IOException { // 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); } } protected void _writeRaw(char[] text, int offset, int len) throws IOException { // 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(); _out.write(text, offset, len); } protected void _writeRawLong(String text) throws IOException { 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; } protected void _writeRawLong(StringBuilder text) throws IOException { 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 Object getCurrentValue() { return _streamWriteContext.getCurrentValue(); } @Override public void setCurrentValue(Object v) { _streamWriteContext.setCurrentValue(v); } /* /********************************************************************** /* Overrides: capability introspection methods /********************************************************************** */ @Override public boolean canWriteObjectId() { return false; } @Override public boolean canWriteTypeId() { return false; } /* /********************************************************************** /* Overridden methods; writing property names /********************************************************************** */ @Override public void writeFieldName(String name) throws IOException { if (!_streamWriteContext.writeName(name)) { _reportError("Cannot write a property name, expecting a value"); } if (_streamWriteContext._inline) { if (_streamWriteContext.hasCurrentIndex()) { _writeRaw(", "); } _writeStringImpl(StringOutputUtil.MASK_SIMPLE_KEY, 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); } } /* /********************************************************************** /* Public API: structural output /********************************************************************** */ @Override public void writeStartArray() throws IOException { writeStartArray(null); } @Override public void writeStartArray(Object currValue) throws IOException { // 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", true); _streamWriteContext = _streamWriteContext.createChildArrayContext(currValue, _basePath.length()); if (_streamWriteContext._inline) { _writeRaw('['); } } @Override public void writeEndArray() throws IOException { 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(); writeValueEnd(); } @Override public void writeStartObject() throws IOException { writeStartObject(null); } @Override public void writeStartObject(Object forValue) throws IOException { // objects aren't always materialized right now _verifyValueWrite("start an object", false); _streamWriteContext = _streamWriteContext.createChildObjectContext(forValue, _basePath.length()); if (_streamWriteContext._inline) { writeRaw('{'); } } @Override public void writeEndObject() throws IOException { if (!_streamWriteContext.inObject()) { _reportError("Current context not an Object but " + _streamWriteContext.typeDesc()); } if (_streamWriteContext._inline) { writeRaw('}'); _streamWriteContext = _streamWriteContext.getParent(); writeValueEnd(); } else { if (!_streamWriteContext.hasCurrentIndex()) { // empty object writeCurrentPath(); _writeRaw("{}"); writeValueEnd(); } _streamWriteContext = _streamWriteContext.getParent(); } } /* /********************************************************************** /* Output method implementations, textual /********************************************************************** */ @Override public void writeString(String text) throws IOException { if (text == null) { writeNull(); return; } _verifyValueWrite("write String value"); _writeStringImpl(StringOutputUtil.MASK_STRING, text); writeValueEnd(); } @Override public void writeString(char[] text, int offset, int len) throws IOException { _verifyValueWrite("write String value"); _writeStringImpl(StringOutputUtil.MASK_STRING, text, offset, len); writeValueEnd(); } @Override public void writeRawUTF8String(byte[] text, int offset, int len) throws IOException { _reportUnsupportedOperation(); } @Override public void writeUTF8String(byte[] text, int offset, int len) throws IOException { writeString(new String(text, offset, len, StandardCharsets.UTF_8)); writeValueEnd(); } /* /********************************************************************** /* Output method implementations, unprocessed ("raw") /********************************************************************** */ @Override public void writeRaw(String text) throws IOException { _writeRaw(text); } @Override public void writeRaw(String text, int offset, int len) throws IOException { _writeRaw(text.substring(offset, offset + len)); } @Override public void writeRaw(char[] text, int offset, int len) throws IOException { _writeRaw(text, offset, len); } @Override public void writeRaw(char c) throws IOException { _writeRaw(c); } @Override public void writeRaw(SerializableString text) throws IOException { writeRaw(text.toString()); } /* /********************************************************************** /* Output method implementations, base64-encoded binary /********************************************************************** */ @Override public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws IOException { if (data == null) { writeNull(); return; } _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('\''); writeValueEnd(); } /* /********************************************************************** /* Output method implementations, scalars /********************************************************************** */ @Override public void writeBoolean(boolean state) throws IOException { _verifyValueWrite("write boolean value"); _writeRaw(state ? "true" : "false"); writeValueEnd(); } @Override public void writeNumber(short v) throws IOException { writeNumber((int) v); writeValueEnd(); } @Override public void writeNumber(int i) throws IOException { _verifyValueWrite("write number"); _writeRaw(String.valueOf(i)); writeValueEnd(); } @Override public void writeNumber(long l) throws IOException { _verifyValueWrite("write number"); _writeRaw(String.valueOf(l)); writeValueEnd(); } @Override public void writeNumber(BigInteger v) throws IOException { if (v == null) { writeNull(); return; } _verifyValueWrite("write number"); _writeRaw(String.valueOf(v)); writeValueEnd(); } @Override public void writeNumber(double d) throws IOException { _verifyValueWrite("write number"); _writeRaw(String.valueOf(d)); writeValueEnd(); } @Override public void writeNumber(float f) throws IOException { _verifyValueWrite("write number"); _writeRaw(String.valueOf(f)); writeValueEnd(); } @Override public void writeNumber(BigDecimal dec) throws IOException { if (dec == null) { writeNull(); return; } _verifyValueWrite("write number"); String str = isEnabled(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) ? dec.toPlainString() : dec.toString(); _writeRaw(str); writeValueEnd(); } @Override public void writeNumber(String encodedValue) throws IOException { if (encodedValue == null) { writeNull(); return; } _verifyValueWrite("write number"); _writeRaw(encodedValue); writeValueEnd(); } @Override public void writeNull() throws IOException { if (TomlWriteFeature.FAIL_ON_NULL_WRITE.enabledIn(_tomlFeatures)) { throw new TomlStreamWriteException("TOML null writing disabled (TomlWriteFeature.FAIL_ON_NULL_WRITE)", this); } _verifyValueWrite("write null value"); _writeStringImpl(StringOutputUtil.MASK_STRING, ""); writeValueEnd(); } /* /********************************************************************** /* Implementations for methods from base class /********************************************************************** */ @Override protected void _verifyValueWrite(String typeMsg) throws IOException { _verifyValueWrite(typeMsg, true); } protected void _verifyValueWrite(String typeMsg, boolean forceMaterializeKey) throws IOException { // 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 (forceMaterializeKey) { writeCurrentPath(); } } } private void writeCurrentPath() throws IOException { _writeRaw(_basePath); _writeRaw(" = "); } private void writeValueEnd() throws IOException { if (!_streamWriteContext._inline) { writeRaw('\n'); } } /* /********************************************************************** /* String support /********************************************************************** */ private void _appendPropertyName(StringBuilder path, String name) throws IOException { int cat = StringOutputUtil.categorize(name) & StringOutputUtil.MASK_SIMPLE_KEY; if ((cat & StringOutputUtil.UNQUOTED_KEY) != 0) { path.append(name); } else if ((cat & StringOutputUtil.LITERAL_STRING) != 0) { path.append('\'').append(name).append('\''); } else if ((cat & StringOutputUtil.BASIC_STRING_NO_ESCAPE) != 0) { path.append('"').append(name).append('"'); } else if ((cat & StringOutputUtil.BASIC_STRING) != 0) { path.append('"'); for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); String escape = StringOutputUtil.getBasicStringEscape(c); if (escape == null) { path.append(c); } else { path.append(escape); } } path.append('"'); } else { throw new TomlStreamWriteException("Key contains unsupported characters", this); } // NOTE: we do NOT yet write the key; wait until we have value; just append to path } private void _writeStringImpl(int categoryMask, String name) throws IOException { int cat = StringOutputUtil.categorize(name) & categoryMask; if ((cat & StringOutputUtil.UNQUOTED_KEY) != 0) { _writeRaw(name); } else if ((cat & StringOutputUtil.LITERAL_STRING) != 0) { _writeRaw('\''); _writeRaw(name); _writeRaw('\''); } else if ((cat & StringOutputUtil.BASIC_STRING_NO_ESCAPE) != 0) { _writeRaw('"'); _writeRaw(name); _writeRaw('"'); } else if ((cat & StringOutputUtil.BASIC_STRING) != 0) { _writeRaw('"'); for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); String escape = StringOutputUtil.getBasicStringEscape(c); if (escape == null) { _writeRaw(c); } else { _writeRaw(escape); } } _writeRaw('"'); } else { throw new TomlStreamWriteException("Key contains unsupported characters", this); } } private void _writeStringImpl(int categoryMask, char[] text, int offset, int len) throws IOException { int cat = StringOutputUtil.categorize(text, offset, len) & categoryMask; if ((cat & StringOutputUtil.UNQUOTED_KEY) != 0) { _writeRaw(text, offset, len); } else if ((cat & StringOutputUtil.LITERAL_STRING) != 0) { _writeRaw('\''); _writeRaw(text, offset, len); _writeRaw('\''); } else if ((cat & StringOutputUtil.BASIC_STRING_NO_ESCAPE) != 0) { _writeRaw('"'); _writeRaw(text, offset, len); _writeRaw('"'); } else if ((cat & StringOutputUtil.BASIC_STRING) != 0) { _writeRaw('"'); for (int i = 0; i < len; i++) { char c = text[offset + len]; String escape = StringOutputUtil.getBasicStringEscape(c); if (escape == null) { _writeRaw(c); } else { _writeRaw(escape); } } _writeRaw('"'); } else { throw new TomlStreamWriteException("Key contains unsupported characters", this); } } /* /********************************************************************** /* Time types /********************************************************************** */ @Override public void writeObject(Object value) throws IOException { 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 { _objectCodec.writeValue(this, value); } } }