IonWriterSystem.java

/*
 * Copyright 2007-2019 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://www.apache.org/licenses/LICENSE-2.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 com.amazon.ion.impl;

import static com.amazon.ion.SymbolTable.UNKNOWN_SYMBOL_ID;
import static com.amazon.ion.impl._Private_Utils.newSymbolToken;
import static com.amazon.ion.impl._Private_Utils.newSymbolTokens;

import com.amazon.ion.IonException;
import com.amazon.ion.IonType;
import com.amazon.ion.SymbolTable;
import com.amazon.ion.SymbolToken;
import com.amazon.ion.SystemSymbols;
import com.amazon.ion.UnknownSymbolException;
import com.amazon.ion.system.IonWriterBuilder.InitialIvmHandling;
import com.amazon.ion.system.IonWriterBuilder.IvmMinimizing;
import java.io.IOException;



abstract class IonWriterSystem
    extends _Private_IonWriterBase
{
    /**
     * The system symtab used when resetting the stream.
     * Must not be null.
     */
    final SymbolTable _default_system_symbol_table;

    /**
     * What to do about IVMs at the start of the stream.
     * Becomes null as soon as we write anything.
     * After a {@link #finish()} this becomes {@link InitialIVMHandling#ENSURE}
     * because we must force another IVM to be emitted.
     */
    private InitialIvmHandling _initial_ivm_handling;

    /**
     * What to do about non-initial IVMs.
     */
    private final IvmMinimizing _ivm_minimizing;

    /**
     * Indicates whether the (immediately previous emitted value was an IVM.
     * This is cleared by {@link #endValue()}.
     */
    private boolean _previous_value_was_ivm;

    /**
     * Indicates whether we've written anything at all.
     */
    private boolean _anything_written;

    /**
     * Must be either local or system table, and never null.
     * May only be changed between top-level values.
     */
    private SymbolTable _symbol_table;

    /** really ion type is only used for int, string or null (unknown) */
    private IonType     _field_name_type;
    private String      _field_name;
    private int         _field_name_sid = UNKNOWN_SYMBOL_ID;

    private static final int DEFAULT_ANNOTATION_COUNT = 4;

    private int         _annotation_count;
    private SymbolToken[] _annotations =
        new SymbolToken[DEFAULT_ANNOTATION_COUNT];


    //========================================================================

    /**
     * @param defaultSystemSymbolTable must not be null.
     */
    IonWriterSystem(SymbolTable defaultSystemSymbolTable,
                    InitialIvmHandling initialIvmHandling,
                    IvmMinimizing ivmMinimizing,
                    boolean requireSymbolValidation)
    {
        super(requireSymbolValidation);
        defaultSystemSymbolTable.getClass(); // Efficient null check
        _default_system_symbol_table = defaultSystemSymbolTable;
        _symbol_table = defaultSystemSymbolTable;
        _initial_ivm_handling = initialIvmHandling;
        _ivm_minimizing = ivmMinimizing;
    }


    //========================================================================
    // Context management

    final SymbolTable getDefaultSystemSymtab()
    {
        return _default_system_symbol_table;
    }

    public final SymbolTable getSymbolTable()
    {
        return _symbol_table;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation simply validates that the argument is not a
     * shared symbol table, and assigns it to {@link #_symbol_table}.
     */
    @Override
    public final void setSymbolTable(SymbolTable symbols)
        throws IOException
    {
        if (symbols == null || _Private_Utils.symtabIsSharedNotSystem(symbols)) {
            throw new IllegalArgumentException("symbol table must be local or system to be set, or reset");
        }
        if (getDepth() > 0) {
            throw new IllegalStateException("the symbol table cannot be set, or reset, while a container is open");
        }
        _symbol_table = symbols;
    }

    boolean shouldWriteIvm()
    {
        if (_initial_ivm_handling == InitialIvmHandling.ENSURE)
        {
            return true;
        }
        if (_initial_ivm_handling == InitialIvmHandling.SUPPRESS)
        {
            // TODO amazon-ion/ion-java/issues/24 Must write IVM if given system != 1.0
            return false;
        }
        // TODO amazon-ion/ion-java/issues/24 Add SUPPRESS_ALL to suppress non 1.0 IVMs

        if (_ivm_minimizing == IvmMinimizing.ADJACENT)
        {
            // TODO amazon-ion/ion-java/issues/24 Write IVM if current system version != given system
            // For now we assume that it's the same since we only support 1.0
            return ! _previous_value_was_ivm;
        }
        if (_ivm_minimizing == IvmMinimizing.DISTANT)
        {
            // TODO amazon-ion/ion-java/issues/24 Write IVM if current system version != given system
            // For now we assume that it's the same since we only support 1.0
            return ! _anything_written;
        }

        return true;
    }

    /**
     * Sets {@link #_symbol_table} and clears {@link #_initial_ivm_handling}.
     * Subclasses should override to generate output.
     */
    final void writeIonVersionMarker(SymbolTable systemSymtab)
        throws IOException
    {
        if (getDepth() != 0)
        {
            String message =
                "Ion Version Markers are only valid at the top level of a " +
                "data stream";
            throw new IllegalStateException(message);
        }
        assert systemSymtab.isSystemTable();

        if (! SystemSymbols.ION_1_0.equals(systemSymtab.getIonVersionId()))
        {
            String message = "This library only supports Ion 1.0";
            throw new UnsupportedOperationException(message);
        }

        if (shouldWriteIvm())
        {
            _initial_ivm_handling = null;

            writeIonVersionMarkerAsIs(systemSymtab);

            _previous_value_was_ivm = true;
        }

        _symbol_table = systemSymtab;
    }

    /**
     * Writes an IVM without checking preconditions or
     * {@link InitialIVMHandling}.
     */
    abstract void writeIonVersionMarkerAsIs(SymbolTable systemSymtab)
        throws IOException;


    @Override
    public final void writeIonVersionMarker()
        throws IOException
    {
        writeIonVersionMarker(_default_system_symbol_table);
    }


    void writeLocalSymtab(SymbolTable symtab)
        throws IOException
    {
        assert symtab.isLocalTable();
        _symbol_table = symtab;
    }


    /**
     * Builds a new local symbol table from the current contextual symtab
     * (a system symtab).
     * @return not null.
     */
    SymbolTable inject_local_symbol_table() throws IOException
    {
        assert _symbol_table.isSystemTable();
        // no catalog since it doesn't matter as this is a
        // pure local table, with no imports
        return LocalSymbolTable.DEFAULT_LST_FACTORY.newLocalSymtab(_symbol_table);
    }

    @Override
    final String assumeKnownSymbol(int sid)
    {
        String text = _symbol_table.findKnownSymbol(sid);
        if (text == null)
        {
            throw new UnknownSymbolException(sid);
        }
        return text;
    }

    final int add_symbol(String name) throws IOException
    {
        int sid;
        if (_symbol_table.isSystemTable()) {
            sid = _symbol_table.findSymbol(name);
            if (sid != UNKNOWN_SYMBOL_ID) {
                return sid;
            }
            // @name is not a system symbol, so we inject a local symtab
            _symbol_table = inject_local_symbol_table();
        }
        assert _symbol_table.isLocalTable();
        sid = _symbol_table.intern(name).getSid();
        return sid;
    }

    void startValue() throws IOException
    {
        if (_initial_ivm_handling == InitialIvmHandling.ENSURE)
        {
            writeIonVersionMarker(_default_system_symbol_table);
        }
    }

    void endValue()
    {
        _initial_ivm_handling = null;
        _previous_value_was_ivm = false;
        _anything_written = true;
    }


    /** Writes a symbol without checking for system ID. */
    abstract void writeSymbolAsIs(int symbolId) throws IOException;

    /** Writes a symbol without checking for system ID. */
    abstract void writeSymbolAsIs(String value) throws IOException;

    @Override
    final void writeSymbol(int symbolId) throws IOException
    {
        if (symbolId < 0) {
            throw new IllegalArgumentException("symbol IDs are >= 0.");
        }

        if (symbolId == SystemSymbols.ION_1_0_SID
            && getDepth() == 0
            && _annotation_count == 0) {
            // $ion_1_0 is written as an IVM only if it is not annotated
            // TODO amazon-ion/ion-java/issues/24 Make sure to get the right symtab, default may differ.
            writeIonVersionMarker();
        }
        else
        {
            writeSymbolAsIs(symbolId);
        }
    }

    public final void writeSymbol(String value) throws IOException
    {
        if (SystemSymbols.ION_1_0.equals(value)
            && getDepth() == 0
            && _annotation_count == 0) {
            // $ion_1_0 is written as an IVM only if it is not annotated
            // TODO amazon-ion/ion-java/issues/24 Make sure to get the right symtab, default may differ.
            writeIonVersionMarker();
        }
        else {
            writeSymbolAsIs(value);
        }
    }


    public void finish() throws IOException
    {
        if (getDepth() != 0) {
            throw new IllegalStateException(ERROR_FINISH_NOT_AT_TOP_LEVEL);
        }

        flush();

        _previous_value_was_ivm = false;
        _initial_ivm_handling = InitialIvmHandling.ENSURE;
        _symbol_table = _default_system_symbol_table;
    }


    //========================================================================
    // Field names

    // This handles converting string to int (or the reverse) using the current
    // symbol table, if that is needed.  These routines are not generally
    // overridden except to throw UnsupportedOperationException when they are
    // not supported.


    @Override
    public final boolean isFieldNameSet()
    {
        if (_field_name_type != null) {
            switch (_field_name_type) {
            case STRING:
                return _field_name != null;
            case INT:
                return _field_name_sid >= 0;
            default:
                break;
            }
        }
        return false;
    }

    final void clearFieldName()
    {
        _field_name_type = null;
        _field_name = null;
        _field_name_sid = UNKNOWN_SYMBOL_ID;
    }


    public final void setFieldName(String name)
    {
        if (!this.isInStruct()) {
            throw new IllegalStateException();
        }

        name.getClass(); // fast null check

        _field_name_type = IonType.STRING;
        _field_name = name;
        _field_name_sid = UNKNOWN_SYMBOL_ID;
    }

    public final void setFieldNameSymbol(SymbolToken name)
    {
        if (!this.isInStruct()) {
            throw new IllegalStateException();
        }

        String text = name.getText();
        if (text != null)
        {
            _field_name_type = IonType.STRING;
            _field_name = text;
            _field_name_sid = UNKNOWN_SYMBOL_ID;
        }
        else
        {
            int sid = name.getSid();
            if (sid < 0) {
                throw new IllegalArgumentException();
            }
            validateSymbolId(sid);

            _field_name_type = IonType.INT;
            _field_name_sid = sid;
            _field_name = null;
        }
    }

    /**
     * Returns the symbol id of the current field name, if the field name
     * has been set.  If the name has not been set, either as either a String
     * or a symbol id value, this returns -1 (undefined symbol).
     * @return symbol id of the name of the field about to be written or -1 if
     * it is not set
     */
    final int getFieldId()
    {
        int id;

        if (_field_name_type == null) {
            throw new IllegalStateException("the field has not be set");
        }
        switch (_field_name_type) {
        case STRING:
                try {
                    id = add_symbol(_field_name);
                }
                catch (IOException e) {
                    throw new IonException(e);
                }
                // TODO cache the sid?
            break;
        case INT:
            id = _field_name_sid;
            break;
        default:
            throw new IllegalStateException("the field has not be set");
        }

        return id;
    }

    final SymbolToken assumeFieldNameSymbol()
    {
        if (_field_name_type == null)  {
            throw new IllegalStateException(ERROR_MISSING_FIELD_NAME);
        }
        // Exactly one of our fields is set.
        assert _field_name != null ^ _field_name_sid >= 0;
        return new SymbolTokenImpl(_field_name, _field_name_sid);
    }

    //========================================================================
    // Annotations

    /**
     * Ensures that our {@link #_annotations} and {@link #_annotation_sids}
     * arrays have enough capacity to hold the given number of annotations.
     * Does not increase {@link #_annotation_count}.
     */
    final void ensureAnnotationCapacity(int length) {
        int oldlen = (_annotations == null) ? 0 : _annotations.length;
        if (length < oldlen) return;

        int newlen = (_annotations == null) ? 10 : (_annotations.length * 2);
        if (length > newlen) {
            newlen = length;
        }

        SymbolToken[] temp1 = new SymbolToken[newlen];

        if (oldlen > 0) {
            System.arraycopy(_annotations, 0, temp1, 0, oldlen);
        }
        _annotations = temp1;
    }


    final int[] internAnnotationsAndGetSids() throws IOException
    {
        int count = _annotation_count;
        if (count == 0) return _Private_Utils.EMPTY_INT_ARRAY;

        int[] sids = new int[count];
        for (int i = 0; i < count; i++)
        {
            SymbolToken sym = _annotations[i];
            int sid = sym.getSid();
            if (sid == UNKNOWN_SYMBOL_ID)
            {
                String text = sym.getText();
                sid = add_symbol(text);
                _annotations[i] = new SymbolTokenImpl(text, sid);
            }
            sids[i] = sid;
        }
        return sids;
    }


    final boolean hasAnnotations()
    {
        return _annotation_count != 0;
    }

    final int annotationCount()
    {
        return _annotation_count;
    }

    final void clearAnnotations()
    {
        _annotation_count = 0;
    }

    @Override
    final int findAnnotation(String name) {
        if (_annotation_count > 0) {
            for (int ii=0; ii<_annotation_count; ii++) {
                if (name.equals(_annotations[ii].getText())) {
                    return ii;
                }
            }
        }
        return -1;
    }

    final SymbolToken[] getTypeAnnotationSymbols()
    {
        int count = _annotation_count;
        if (count == 0) return SymbolToken.EMPTY_ARRAY;

        SymbolToken[] syms = new SymbolToken[count];
        System.arraycopy(_annotations, 0, syms, 0, count);
        return syms;
    }

    public final void setTypeAnnotationSymbols(SymbolToken... annotations)
    {
        if (annotations == null || annotations.length == 0)
        {
            _annotation_count = 0;
        }
        else
        {
            int annotationsArrayLength = annotations.length;
            // TODO the following makes two copy passes
            // TODO validate the input
            ensureAnnotationCapacity(annotationsArrayLength);

            SymbolTable symtab = getSymbolTable();
            int count = 0;
            while (count < annotationsArrayLength)
            {
                SymbolToken sym = annotations[count];
                if (sym == null) {
                    break;
                }
                if (sym.getText() == null) {
                    validateSymbolId(sym.getSid());
                }
                sym = _Private_Utils.localize(symtab, sym);
                _annotations[count] = sym;

                count++;
            }
            _annotation_count = count;
        }
    }

    @Override
    final String[] getTypeAnnotations()
    {
        return _Private_Utils.toStrings(_annotations, _annotation_count);
    }

    public final void setTypeAnnotations(String... annotations)
    {
        if (annotations == null || annotations.length == 0)
        {
            _annotation_count = 0;
        }
        else
        {
            SymbolToken[] syms =
                newSymbolTokens(getSymbolTable(), annotations);
            int count = syms.length;
            // TODO the following makes two copy passes
            ensureAnnotationCapacity(count);
            System.arraycopy(syms, 0, _annotations, 0, count);
            _annotation_count = count;
        }
    }

    public final void addTypeAnnotation(String annotation)
    {
        SymbolToken is = newSymbolToken(getSymbolTable(), annotation);
        ensureAnnotationCapacity(_annotation_count + 1);
        _annotations[_annotation_count++] = is;
    }


    @Override
    final int[] getTypeAnnotationIds()
    {
        return _Private_Utils.toSids(_annotations, _annotation_count);
    }

    public <T> T asFacet(Class<T> facetType)
    {
        // This implementation has no facets.
        return null;
    }
}