WriterBackedGenerator.java

package tools.jackson.dataformat.javaprop.impl;

import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;

import tools.jackson.core.*;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.NumberOutput;
import tools.jackson.dataformat.javaprop.JavaPropsGenerator;
import tools.jackson.dataformat.javaprop.JavaPropsSchema;
import tools.jackson.dataformat.javaprop.io.JPropEscapes;

public class WriterBackedGenerator extends JavaPropsGenerator
{
    /*
    /**********************************************************************
    /* Configuration
    /**********************************************************************
     */

    /**
     * Underlying {@link Writer} used for output.
     */
    protected final Writer _out;

    /**
     * Caller-provided {@link OutputStream} that {@link #_out} was constructed (by
     * Jackson) to wrap, if any; {@code null} if {@link #_out} was handed to us by the
     * caller.
     *<p>
     * A {@link Writer} we construct is an internal buffer, so it must always be flushed
     * on {@link #flush()} and closed on {@link #close()}. 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;

    /*
    /**********************************************************************
    /* Output buffering
    /**********************************************************************
     */

    /**
     * Intermediate buffer in which contents are buffered before
     * being written using {@link #_out}.
     */
    protected char[] _outputBuffer;

    /**
     * Pointer to the next available location 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;

    /*
    /**********************************************************************
    /* Life-cycle
    /**********************************************************************
     */

    public WriterBackedGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
            int stdFeatures, JavaPropsSchema schema,
            Writer out)
    {
        this(writeCtxt, ioCtxt, stdFeatures, schema, 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 WriterBackedGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
            int stdFeatures, JavaPropsSchema schema,
            Writer out, OutputStream target)
    {
        super(writeCtxt, ioCtxt, stdFeatures, schema);
        _out = out;
        _target = target;
        _outputBuffer = ioCtxt.allocConcatBuffer();
        _outputEnd = _outputBuffer.length;
    }

    /*
    /**********************************************************************
    /* Overridden methods, configuration
    /**********************************************************************
     */

    @Override
    public Object streamWriteOutputTarget() {
        // [dataformats-text#751]: 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 {
                _flushBuffer();
            } catch (Throwable t) {
                // [dataformats-text#751]: 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#734]: a Writer we constructed ourselves must be closed
        //   regardless: without that its buffered content never reaches the caller's
        //   OutputStream. Caller's stream is shielded from that, and closed (or
        //   flushed) as per features enabled now -- [dataformats-text#751] 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#751]: nothing to flush once closed -- and target may
        //   have been closed along with us
        if (isClosed() || (_out == null)) {
            return;
        }
        _flushBuffer();
        try {
            if (_target != null) {
                // [dataformats-text#734]: 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) {
            try {
                _out.write(_outputBuffer, 0, _outputTail);
            } catch (IOException e) {
                throw _wrapIOFailure(e);
            }
            _outputTail = 0;
        }
    }

    @Override
    protected void _appendPropertyName(StringBuilder path, String name) {
        // Note that escaping needs to be applied now...
        JPropEscapes.appendKey(_basePath, name);
        // NOTE: we do NOT yet write the key; wait until we have value; just append to path
    }

    /*
    /**********************************************************************
    /* Internal methods; escaping writes
    /**********************************************************************
     */

    @Override
    protected void _writeEscapedEntry(String value) throws JacksonException
    {
        // note: key has been already escaped so:
        _writeRaw(_basePath);
        _writeRaw(_schema.keyValueSeparator());

        _writeEscaped(value);
        _writeLinefeed();
    }

    @Override
    protected void _writeEscapedEntry(char[] text, int offset, int len) throws JacksonException
    {
        // note: key has been already escaped so:
        _writeRaw(_basePath);
        _writeRaw(_schema.keyValueSeparator());

        _writeEscaped(text, offset, len);
        _writeLinefeed();
    }

    @Override
    protected void _writeUnescapedEntry(String value) throws JacksonException
    {
        // note: key has been already escaped so:
        _writeRaw(_basePath);
        _writeRaw(_schema.keyValueSeparator());

        _writeRaw(value);
        _writeLinefeed();
    }

    @Override
    protected void _writeUnescapedEntry(int value) throws JacksonException
    {
        _writeRaw(_basePath);
        _writeRaw(_schema.keyValueSeparator());
        // up to 10 digits and possible minus sign
        if ((_outputTail + 11) > _outputEnd) {
            _flushBuffer();
        }
        _outputTail = NumberOutput.outputInt(value, _outputBuffer, _outputTail);
        _writeLinefeed();
    }

    @Override
    protected void _writeUnescapedEntry(long value) throws JacksonException
    {
        _writeRaw(_basePath);
        _writeRaw(_schema.keyValueSeparator());
        // up to 19 digits and possible minus sign
        if ((_outputTail + 20) > _outputEnd) {
            _flushBuffer();
        }
        _outputTail = NumberOutput.outputLong(value, _outputBuffer, _outputTail);
        _writeLinefeed();
    }

    @Override
    protected void _writeUnescapedEntry(double value) throws JacksonException
    {
        if (!isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)) {
            _writeUnescapedEntry(NumberOutput.toString(value, false));
            return;
        }
        _writeRaw(_basePath);
        _writeRaw(_schema.keyValueSeparator());
        if ((_outputTail + NumberOutput.MAX_DOUBLE_BYTES) > _outputEnd) {
            _flushBuffer();
        }
        _outputTail = NumberOutput.outputDouble(value, _outputBuffer, _outputTail);
        _writeLinefeed();
    }

    @Override
    protected void _writeUnescapedEntry(float value) throws JacksonException
    {
        if (!isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)) {
            _writeUnescapedEntry(NumberOutput.toString(value, false));
            return;
        }
        _writeRaw(_basePath);
        _writeRaw(_schema.keyValueSeparator());
        if ((_outputTail + NumberOutput.MAX_FLOAT_BYTES) > _outputEnd) {
            _flushBuffer();
        }
        _outputTail = NumberOutput.outputFloat(value, _outputBuffer, _outputTail);
        _writeLinefeed();
    }

    protected void _writeEscaped(String value) throws JacksonException
    {
        StringBuilder sb = JPropEscapes.appendValue(value);
        if (sb == null) {
            _writeRaw(value);
        } else {
            _writeRaw(sb);
        }
    }

    protected void _writeEscaped(char[] text, int offset, int len) throws JacksonException
    {
        _writeEscaped(new String(text, offset, len));
    }

    protected void _writeLinefeed() throws JacksonException
    {
        _writeRaw(_schema.lineEnding());
    }

    /*
    /**********************************************************************
    /* Internal methods; raw writes
    /**********************************************************************
     */

    @Override
    protected void _writeRaw(char c) throws JacksonException
    {
        if (_outputTail >= _outputEnd) {
            _flushBuffer();
        }
        _outputBuffer[_outputTail++] = c;
    }

    @Override
    protected 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);
        }
    }

    @Override
    protected void _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);
        }
    }

    @Override
    protected 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);
        }
    }

    protected void _writeRawLong(String 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;
    }

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