IonGenerator.java

/*
 * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at:
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

package tools.jackson.dataformat.ion;

import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;

import tools.jackson.core.*;
import tools.jackson.core.base.GeneratorBase;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.json.DupDetector;
import tools.jackson.core.util.JacksonFeatureSet;

import com.amazon.ion.IonType;
import com.amazon.ion.IonValue;
import com.amazon.ion.IonWriter;
import com.amazon.ion.Timestamp;

/**
 * Implementation of {@link JsonGenerator} that will use an underlying
 * {@link IonWriter} for actual writing of content.
 */
public class IonGenerator
    extends GeneratorBase
{
    /*
    /**********************************************************************
    /* Basic configuration
    /**********************************************************************
     */

    /* This is the textual or binary writer */
    protected final IonWriter _writer;
    /* Indicates whether the IonGenerator is responsible for closing the underlying IonWriter. */
    protected final boolean _ionWriterIsManaged;

    /**
     * Bit flag composed of bits that indicate which
     * {@link IonWriteFeature}s
     * are enabled.
     */
    protected int _formatFeatures;

    /**
     * Highest-level output abstraction we can use; either
     * OutputStream or Writer.
     */
    protected final Closeable _destination;

    /*
    /**********************************************************************
    /* State
    /**********************************************************************
     */

    /**
     * Object that keeps track of the current contextual state of the generator.
     */
    protected IonWriteContext _streamWriteContext;

    /*
    /**********************************************************************
    /* Instantiation
    /**********************************************************************
     */

    public IonGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
            int streamWriteFeatures, int formatWriteFeatures,
            IonWriter ion, boolean ionWriterIsManaged, Closeable dst)
    {
        super(writeCtxt, ioCtxt, streamWriteFeatures);
        _formatFeatures = formatWriteFeatures;
        _writer = ion;
        _ionWriterIsManaged = ionWriterIsManaged;
        _destination = dst;

        final DupDetector dups = StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures)
                ? DupDetector.rootDetector(this) : null;
                //  Overwrite the writecontext with our own implementation
        _streamWriteContext = IonWriteContext.createRootContext(dups);
    }

    @Override
    public Version version() {
        return PackageVersion.VERSION;
    }

    /*
    /**********************************************************************
    /* JsonGenerator implementation: state handling
    /**********************************************************************
     */

    @Override
    public TokenStreamContext streamWriteContext() { return _streamWriteContext; }

    @Override
    public Object currentValue() {
        return _streamWriteContext.currentValue();
    }

    @Override
    public void assignCurrentValue(Object v) {
        _streamWriteContext.assignCurrentValue(v);
    }

    /*
    /**********************************************************************
    /* JsonGenerator implementation: low-level I/O
    /**********************************************************************
     */

    @Override
    public int streamWriteOutputBuffered() { return -1; }

    @Override
    public Object streamWriteOutputTarget() {
        // 25-May-2020, tatu: Tricky one here; should we return `IonWriter` or
        //    actual underlying Writer/OutputStream?
        //    For now, return underlying Writer/OutputStream
        return _writer;
    }

    @Override
    protected void _closeInput() throws IOException
    {
        if (_ionWriterIsManaged) {
            _writer.close();
        }
        if (_ioContext.isResourceManaged()) {
            _destination.close();
        } else {
            if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
                if (_destination instanceof Flushable) {
                    ((Flushable) _destination).flush();
                }
            }
        }
    }

    @Override
    public void flush()
    {
        if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
            Object dst = _ioContext.contentReference().getRawContent();
            if (dst instanceof Flushable) {
                try {
                    ((Flushable) dst).flush();
                } catch (IOException e) {
                    throw _wrapIOFailure(e);
                }
            }
        }
    }

    /*
    /**********************************************************************
    /* Capability introspection
    /**********************************************************************
     */

    @Override
    public boolean canWriteTypeId() {
        // yes, Ion does support Native Type Ids!
        // 29-Nov-2020, jobarr: Except as per [dataformats-binary#225] might not want to...
        return IonWriteFeature.USE_NATIVE_TYPE_ID.enabledIn(_formatFeatures);
    }

    @Override
    public JacksonFeatureSet<StreamWriteCapability> streamWriteCapabilities() {
        return DEFAULT_BINARY_WRITE_CAPABILITIES;
    }

    /*
    /**********************************************************************
    /* Config access
    /**********************************************************************
     */

    @Override
    public PrettyPrinter getPrettyPrinter() {
        return null;
    }

    /*
    /**********************************************************************
    /* JsonGenerator implementation: write numeric values
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeNumber(short v) throws JacksonException {
        return writeNumber((int) v);
    }

    @Override
    public JsonGenerator writeNumber(int value) throws JacksonException {
        _verifyValueWrite("write numeric value");
        try {
            _writer.writeInt((long)value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeNumber(long value) throws JacksonException {
        _verifyValueWrite("write numeric value");
        try {
            _writer.writeInt(value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeNumber(BigInteger value) throws JacksonException {
        if (value == null) {
            return writeNull();
        }
        _verifyValueWrite("write numeric value");
        try {
            _writer.writeInt(value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeNumber(double value) throws JacksonException {
        _verifyValueWrite("write numeric value");
        try {
            _writer.writeFloat(value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeNumber(float value) throws JacksonException {
        _verifyValueWrite("write numeric value");
        try {
            _writer.writeFloat((double) value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeNumber(BigDecimal value) throws JacksonException {
        if (value == null) {
            return writeNull();
        }
        _verifyValueWrite("write numeric value");
        try {
            _writer.writeDecimal(value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeNumber(String value) throws JacksonException {
        // will lose its identity... fine
        return writeString(value);
    }

    /*
    /**********************************************************************
    /* Ion-specific additional write methods:
    /**********************************************************************
     */

    public JsonGenerator writeSymbol(String value) throws JacksonException {
        _verifyValueWrite("write symbol value");
        try {
            _writer.writeSymbol(value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    /**
     * Annotates the next structure or value written -- {@link IonWriter#stepIn(IonType) stepIn()} or one of the
     * {@link IonWriter}s {@code write*()} methods.
     *
     * @param annotation a type annotation
     *
     * @see tools.jackson.dataformat.ion.polymorphism.IonAnnotationTypeSerializer
     */
    public JsonGenerator annotateNextValue(String annotation) {
        // We're not calling _verifyValueWrite because this doesn't actually write anything -- writing happens upon
        // the next _writer.write*() or stepIn().
        _writer.addTypeAnnotation(annotation);
        return this;
    }

    // // // Ion Extensions

    public JsonGenerator writeDate(Calendar value) throws JacksonException {
        _verifyValueWrite("write date value");
        try {
            _writer.writeTimestamp(Timestamp.forCalendar(value));
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    /*
    /**********************************************************************
    /* JsonGenerator implementation: write textual values
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeString(String value) throws JacksonException {
        _verifyValueWrite("write text value");
        try {
            _writer.writeString(value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeString(char[] buffer, int offset, int length) throws JacksonException {
        // Ion doesn't have matching optimized method, so:
        return writeString(new String(buffer, offset, length));
    }

    @Override
    public JsonGenerator writeUTF8String(byte[] buffer, int offset, int length) throws JacksonException {
        // Ion doesn't have matching optimized method, so:
        return writeString(new String(buffer, offset, length, StandardCharsets.UTF_8));
    }

    /*
    /**********************************************************************
    /* JsonGenerator implementation: write raw JSON; N/A for Ion
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeRaw(String value) throws JacksonException {
        return _reportNoRaw();
    }

    @Override
    public JsonGenerator writeRaw(char value) throws JacksonException {
        return _reportNoRaw();
    }

    @Override
    public JsonGenerator writeRaw(String value, int arg1, int arg2) throws JacksonException {
        return _reportNoRaw();
    }

    @Override
    public JsonGenerator writeRaw(char[] value, int arg1, int arg2) throws JacksonException {
        return _reportNoRaw();
    }

    @Override
    public JsonGenerator writeRawValue(String value) throws JacksonException {
        return _reportNoRaw();
    }

    @Override
    public JsonGenerator writeRawValue(String value, int arg1, int arg2) throws JacksonException {
        return _reportNoRaw();
    }

    @Override
    public JsonGenerator writeRawValue(char[] value, int arg1, int arg2) throws JacksonException {
        return _reportNoRaw();
    }

    @Override
    public JsonGenerator writeRawUTF8String(byte[] text, int offset, int length) throws JacksonException {
        return _reportNoRaw();
    }

    /*
    /**********************************************************************
    /* JsonGenerator implementation: write other types of values
    /**********************************************************************
      */

    @Override
    public JsonGenerator writeBinary(Base64Variant b64v, byte[] data, int offset, int length) throws JacksonException {
        _verifyValueWrite("write binary data");
        // no distinct variants for Ion; should produce plain binary
        try {
            _writer.writeBlob(data, offset, length);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeBoolean(boolean value) throws JacksonException {
        _verifyValueWrite("write boolean");
        try {
            _writer.writeBool(value);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeNull() throws JacksonException {
        _verifyValueWrite("write null");
        try {
            _writer.writeNull();
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    public JsonGenerator writeNull(IonType ionType) throws JacksonException {
        _verifyValueWrite("write null");
        try {
            _writer.writeNull(ionType);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    // 06-Oct-2017, tatu: Base impl from `GeneratorBase` should be sufficient
    /*
    @Override
    public JsonGenerator writeObject(Object pojo) throws JacksonException
    {
        if (pojo == null) {
            // important: call method that does check value write:
            writeNull();
        } else {
            // note: should NOT verify value write is ok; will be done indirectly by codec
            if (_objectCodec == null) {
                throw new IllegalStateException("No ObjectCodec defined for the generator, can not serialize regular Java objects");
            }
            _objectCodec.writeValue(this, pojo);
        }
        return this;
    }
    */

    public JsonGenerator writeValue(IonValue value) throws JacksonException {
        _verifyValueWrite("write ion value");
        if (value == null) {
            try {
                _writer.writeNull();
            } catch (IOException e) {
                throw _wrapIOFailure(e);
            }
        } else {
            value.writeTo(_writer);
        }
        return this;
    }

    public JsonGenerator writeValue(Timestamp value) throws JacksonException {
        _verifyValueWrite("write timestamp");
        try {
            if (value == null) {
                _writer.writeNull();
            } else {
                _writer.writeTimestamp(value);
            }
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    /*
    /**********************************************************************
    /* Methods base impl needs
    /**********************************************************************
     */

    @Override
    protected void _releaseBuffers() {
        // nothing to do here...
    }

    @Override
    protected void _verifyValueWrite(String msg) throws JacksonException
    {
        if (!_streamWriteContext.writeValue()) {
            _reportError("Can not "+msg+", expecting a property name");
        }
        // 18-Feb-2021, tatu: as per [dataformats-binary#247], this does not work
        //   (Ion impl must do pretty-printing), so
        /*
        // Only additional work needed if we are pretty-printing
        if (_prettyPrinter != null) {
            // If we have a pretty printer, it knows what to do:
            switch (status) {
            case JsonWriteContext.STATUS_OK_AFTER_COMMA: // array
                _prettyPrinter.writeArrayValueSeparator(this);
                break;
            case JsonWriteContext.STATUS_OK_AFTER_COLON:
                _prettyPrinter.writeObjectFieldValueSeparator(this);
                break;
            case JsonWriteContext.STATUS_OK_AFTER_SPACE:
                _prettyPrinter.writeRootValueSeparator(this);
                break;
            case IonWriteContext.STATUS_OK_AFTER_SEXP_SEPARATOR:
                // Special handling of sexp value separators can be added later. Root value
                // separator will be whitespace which is sufficient to separate sexp values
                _prettyPrinter.writeRootValueSeparator(this);
                break;
            case JsonWriteContext.STATUS_OK_AS_IS:
                // First entry, but of which context?
                if (_outputContext.inArray()) {
                    _prettyPrinter.beforeArrayValues(this);
                } else if (_outputContext.inObject()) {
                    _prettyPrinter.beforeObjectEntries(this);
                } else if(((IonWriteContext) _writeContext).inSexp()) {
                    // Format sexps like arrays
                    _prettyPrinter.beforeArrayValues(this);
                }
                break;
            default:
                throw new IllegalStateException("Should never occur; status "+status);
            }
        }
        */
    }

    @Override
    public JsonGenerator writeEndArray() throws JacksonException {
        _streamWriteContext = _streamWriteContext.getParent();
        try {
            _writer.stepOut();
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeEndObject() throws JacksonException {
        _streamWriteContext = _streamWriteContext.getParent();
        try {
            _writer.stepOut();
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    /**
     * @since 2.12.2
     */
    public JsonGenerator writeEndSexp() throws JacksonException {
        _streamWriteContext = _streamWriteContext.getParent();
        try {
            _writer.stepOut();
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeName(String value) throws JacksonException {
        //This call to _outputContext is copied from Jackson's UTF8JsonGenerator.writeName(String)
        if (!_streamWriteContext.writeName(value)) {
            throw _constructWriteException("Can not write a property name, expecting a value");
        }
        _writeName(value);
        return this;
    }

    @Override
    public JsonGenerator writePropertyId(long id) throws JacksonException {
        // Should not force construction of a String here...
        String idStr = Long.valueOf(id).toString(); // since instances for small values cached
        return writeName(idStr);
    }

    protected void _writeName(String value) throws JacksonException {
        //Even though this is a one-liner, putting it into a function "_writeName"
        //to keep this code matching the factoring in Jackson's UTF8JsonGenerator.
        _writer.setFieldName(value);
    }

    @Override
    public JsonGenerator writeStartArray() throws JacksonException {
        _verifyValueWrite("start an array");
        _streamWriteContext = _streamWriteContext.createChildArrayContext(null);
        streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
        try {
            _writer.stepIn(IonType.LIST);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeStartArray(Object currValue) throws JacksonException {
        _verifyValueWrite("start an array");
        _streamWriteContext = _streamWriteContext.createChildArrayContext(currValue);
        streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
        try {
            _writer.stepIn(IonType.LIST);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeStartObject() throws JacksonException {
        _verifyValueWrite("start an object");
        _streamWriteContext = _streamWriteContext.createChildObjectContext(null);
        streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
        try {
            _writer.stepIn(IonType.STRUCT);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    @Override
    public JsonGenerator writeStartObject(Object currValue) throws JacksonException {
        _verifyValueWrite("start an object");
        _streamWriteContext = _streamWriteContext.createChildObjectContext(currValue);
        streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
        try {
            _writer.stepIn(IonType.STRUCT);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
    }

    /**
     * @since 2.12.2
     */
    public JsonGenerator writeStartSexp() throws JacksonException {
        _verifyValueWrite("start a sexp");
        _streamWriteContext = _streamWriteContext.createChildSexpContext(null);
        streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
        try {
            _writer.stepIn(IonType.SEXP);
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
        return this;
   }

    /*
    /**********************************************************************
    /* Support for type ids
    /**********************************************************************
     */

    @Override
    public JsonGenerator writeTypeId(Object rawId) throws JacksonException {
        if (rawId instanceof String[]) {
            String[] ids = (String[]) rawId;
            for (String id : ids) {
                annotateNextValue(id);
            }
        } else {
            annotateNextValue(String.valueOf(rawId));
        }
        return this;
    }

    // Default impl should work fine here:
    // public WritableTypeId writeTypePrefix(WritableTypeId typeIdDef) throws JacksonException

    // Default impl should work fine here:
    // public WritableTypeId writeTypeSuffix(WritableTypeId typeIdDef) throws JacksonException

    /*
    /**********************************************************************
    /* Standard methods
    /**********************************************************************
     */

    @Override
    public String toString() {
        return "["+getClass().getSimpleName()+", Ion writer: "+_writer+"]";
    }

    /*
    /**********************************************************************
    /* Internal helper methods
    /**********************************************************************
     */

    protected <T> T _reportNoRaw() throws JacksonException {
        throw _constructWriteException("writeRaw() functionality not available with Ion backend");
    }
}