_Private_IonWriterBase.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 com.amazon.ion.IonReader;
import com.amazon.ion.IonType;
import com.amazon.ion.IonValue;
import com.amazon.ion.IonWriter;
import com.amazon.ion.SymbolTable;
import com.amazon.ion.SymbolToken;
import com.amazon.ion.Timestamp;
import com.amazon.ion.UnknownSymbolException;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;

/**
 * NOT FOR APPLICATION USE!
 * <p>
 * Base type for Ion writers.  This handles the writeIonEvents and provides default
 * handlers for the list forms of write.  This also resolves symbols if a symbol
 * table is available (which it will not be if the underlying writer is a system
 * writer).
 */
public abstract class _Private_IonWriterBase
    implements IonWriter, _Private_ReaderWriter
{
    protected static final String ERROR_MISSING_FIELD_NAME =
        "IonWriter.setFieldName() must be called before writing a value into a struct.";

    static final String ERROR_FINISH_NOT_AT_TOP_LEVEL =
        "IonWriter.finish() can only be called at top-level.";

    private final boolean requireSymbolValidation;

    /**
     * @param requireSymbolValidation true if SID validation should be performed; otherwise, false.
     *                                See {@link _Private_IonTextWriterBuilder#withInvalidSidsAllowed(boolean)}
     */
    public _Private_IonWriterBase(boolean requireSymbolValidation) {
        this.requireSymbolValidation = requireSymbolValidation;
    }

    /**
     * Returns the current depth of containers the writer is at.  This is
     * 0 if the writer is at top-level.
     * @return int depth of container nesting
     */
    protected abstract int getDepth();


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


    /**
     * Write an Ion version marker symbol to the output.  This
     * is the $ion_1_0 value currently (in later versions the
     * number may change).  In text output this appears as the
     * text symbol.  In binary this will be the symbol id if
     * the writer is in a list, sexp or struct.  If the writer
     * is currently at the top level this will write the
     * "magic cookie" value.
     *
     *  Writing a version marker will reset the symbol table
     *  to be the system symbol table.
     */
    abstract void writeIonVersionMarker() throws IOException;

    /**
     * Sets the symbol table to use for encoding to be the passed
     * in symbol table.  The can only be done between top-level values.
     * As symbols are written
     * this symbol table is used to resolve them.  If the symbols
     * are undefined this symbol table is updated to include them
     * as local symbols.  The updated symbol table will be
     * written before any of the local values are emitted.
     * <p>
     * If the symbol table is the system symbol table an Ion
     * version marker will be written to the output.  If symbols
     * not in the system symbol table are written a local
     * symbol table will be created and written before the
     * current top level value.
     *
     * @param symbols base symbol table for encoding. Must not be null.
     * @throws IllegalArgumentException if symbols is null or a shared symbol
     * table, or if this writer isn't at top level.
     */
    public abstract void setSymbolTable(SymbolTable symbols)
        throws IOException;


    /**
     * Translates a symbol using the current symtab.
     *
     * @return not null.
     *
     * @throws UnknownSymbolException if the text is unknown.
     *
     * @see SymbolTable#findKnownSymbol(int)
     */
    abstract String assumeKnownSymbol(int sid);


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


    /**
     * Returns true if the field name has been set either through setFieldName or
     * setFieldId.  This is generally more efficient than calling getFieldName or
     * getFieldId and checking the return type as it does not need to resolve the
     * name through a symbol table.  This returns false if the field name has not
     * been set.
     * @return true if a field name has been set false otherwise
     */
    public abstract boolean isFieldNameSet();


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

    /**
     * Returns the given annotation's index in the value's annotations list, or -1 if not present.
     * @param name the annotation to find.
     * @return the index or -1.
     */
    abstract int findAnnotation(String name);


    /**
     * Gets the current list of pending annotations.
     * This is the contents of the current {@code annotations} array
     * of this writer.
     * <p>
     * If the annotations were set as IDs they
     * will be converted if a symbol table is available.  In the event
     * a symbol table is not available a null array will be returned.
     * If no annotations are set a 0 length array will be returned.
     * <p>
     * @return pending type annotations as strings, null if the
     * annotations cannot be expressed as strings.
     */
    abstract String[] getTypeAnnotations();


    /**
     * Gets the current list of pending annotations.
     * This is the contents of the current {@code annotations} array
     * of this writer.
     * <p>
     * If the annotations were set as string they
     * will be converted to symbol IDs (ints) if a symbol table is
     * available.  In the event a symbol table is not available a
     * null array will be returned.
     * If no annotations are set a 0 length array will be returned.
     * <p>
     * @return pending type annotations as symbol ID ints, null if the
     * annotations cannot be expressed as IDs.
     */
    abstract int[] getTypeAnnotationIds();

    /**
     * Write symbolId out as an IonSymbol value.  The value must
     * be valid in the symbol table.
     *
     * @param symbolId symbol table id to write
     */
    abstract void writeSymbol(int symbolId) throws IOException;


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


    //
    // default overload implementations.  These generally
    // convert the users value to a value of the intrinsic
    // underlying type and then write that type using
    // the concrete writers method.
    //
    public void writeBlob(byte[] value) throws IOException
    {
        if (value == null) {
            this.writeNull(IonType.BLOB);
        }
        else {
            this.writeBlob(value, 0, value.length);
        }
        return;
    }
    public void writeClob(byte[] value) throws IOException
    {
        if (value == null) {
            this.writeNull(IonType.CLOB);
        }
        else {
            this.writeClob(value, 0, value.length);
        }
        return;
    }

    abstract public void writeDecimal(BigDecimal value) throws IOException;


    public void writeFloat(float value) throws IOException
    {
        writeFloat((double)value);
    }

    public void writeNull() throws IOException
    {
        writeNull(IonType.NULL);
    }

    final void validateSymbolId(int sid) {
        if (requireSymbolValidation && sid > getSymbolTable().getMaxId()) {
            // There is no slot for this symbol ID in the symbol table,
            // so an error would be raised on read. Fail early on write.
            throw new UnknownSymbolException(sid);
        }
    }

    public final void writeSymbolToken(SymbolToken tok)
        throws IOException
    {
        if (tok == null) {
            writeNull(IonType.SYMBOL);
            return;
        }

        String text = tok.getText();
        if (text != null)
        {
            writeSymbol(text);
        }
        else
        {
            int sid = tok.getSid();
            validateSymbolId(sid);
            writeSymbol(sid);
        }
    }


    public void writeTimestampUTC(Date value) throws IOException
    {
        Timestamp time = Timestamp.forDateZ(value);
        writeTimestamp(time);
    }



    //
    //  default value and reader implementations.
    //  note that these could be optimized, especially
    //  the reader versions, when the reader is of the
    //  same format as the writer.
    //
    @Deprecated
    public void writeValue(IonValue value) throws IOException
    {
        if (value != null)
        {
            value.writeTo(this);
        }
    }

    public void writeValues(IonReader reader) throws IOException
    {
        if (reader.getDepth() == 0) {
            clear_system_value_stack();
        }

        if (reader.getType() == null) reader.next();

        if (getDepth() == 0 && reader instanceof _Private_ReaderWriter) {
            // Optimize symbol table copying
            _Private_ReaderWriter private_reader =
                (_Private_ReaderWriter)reader;
            while (reader.getType() != null) {
                transfer_symbol_tables(private_reader);
                writeValue(reader);
                reader.next();
            }
        }
        else {
            while (reader.getType() != null) {
                writeValue(reader);
                reader.next();
            }
        }
    }

    private final void transfer_symbol_tables(_Private_ReaderWriter reader)
        throws IOException
    {
        SymbolTable reader_symbols = reader.pop_passed_symbol_table();
        if (reader_symbols != null) {
            clear_system_value_stack();
            setSymbolTable(reader_symbols);
            while (reader_symbols != null) {
                // TODO these symtabs are never popped!
                // Why bother pushing them?
                push_symbol_table(reader_symbols);
                reader_symbols = reader.pop_passed_symbol_table();
            }
        }
    }

    /**
     * @throws UnknownSymbolException if the text of the field name is
     *  unknown.
     */
    private final void write_value_field_name_helper(IonReader reader)
    {
        if (this.isInStruct() && !isFieldNameSet())
        {
            SymbolToken tok = reader.getFieldNameSymbol();
            if (tok == null)
            {
                throw new IllegalStateException("Field name not set");
            }

            setFieldNameSymbol(tok);
        }
    }

    private final void write_value_annotations_helper(IonReader reader)
    {
        SymbolToken[] a = reader.getTypeAnnotationSymbols();
        // At present, we must always call this, even when the list is empty,
        // because local symtab diversion leaves the $ion_symbol_table
        // dangling on the system writer!  TODO fix that, it's broken.
        setTypeAnnotationSymbols(a);
    }


    public boolean isStreamCopyOptimized()
    {
        return false;
    }

    /**
     * Overrides can optimize special cases.
     */
    public void writeValue(IonReader reader) throws IOException
    {
        // TODO this should do symtab optimization as per writeValues()
        writeValueRecursively(reader);
    }

    /**
     * Writes the provided IonReader's current value including any annotations. This function will not advance the
     * IonReader beyond the end of the current value; users wishing to continue using the IonReader at the current
     * depth will need to call {@link IonReader#next()} again.
     *
     * - If the IonReader is not positioned over a value (for example: because it is at the beginning or end of a
     *   stream), then this function does nothing.
     * - If the current value is a container, this function will visit all of its child values and write those too,
     *   advancing the IonReader to the end of the container in the process.
     * - If both this writer and the IonReader are in a struct, the writer will write the current value's field name.
     * - If the writer is not in a struct but the reader is, the writer will ignore the current value's field name.
     * - If the writer is in a struct but the IonReader is not, this function throws an IllegalStateException.
     *
     * @param reader       The IonReader that will provide a value to write.
     * @throws IOException if either the provided IonReader or this writer's underlying OutputStream throw an
     *                     IOException.
     * @throws IllegalStateException if this writer is inside a struct but the IonReader is not.
     */
    final void writeValueRecursively(IonReader reader) throws IOException
    {
        // The IonReader does not need to be at the top level (getDepth()==0) when the function is called.
        // We take note of its initial depth so we can avoid advancing the IonReader beyond the starting value.
        int startingDepth = getDepth();

        // The IonReader will be at `startingDepth` when the function is first called and then again when we
        // have finished traversing all of its children. This boolean tracks which of those two states we are
        // in when `getDepth() == startingDepth`.
        boolean alreadyProcessedTheStartingValue = false;

        // The IonType of the IonReader's current value.
        IonType type;

        while (true) {
            // Each time we reach the top of the loop we are in one of three states:
            // 1. We have not yet begun processing the starting value.
            // 2. We are currently traversing the starting value's children.
            // 3. We have finished processing the starting value.
            if (getDepth() == startingDepth) {
                // The IonReader is at the starting depth. We're either beginning our traversal or finishing it.
                if (alreadyProcessedTheStartingValue) {
                    // We're finishing our traversal.
                    break;
                }
                // We're beginning our traversal. Don't advance the cursor; instead, use the current
                // value's IonType.
                type = reader.getType();
                // We've begun processing the starting value.
                alreadyProcessedTheStartingValue = true;
            } else {
                // We're traversing the starting value's children (that is: values at greater depths). We need to
                // advance the cursor by calling next().
                type = reader.next();
            }

            if (type == null) {
                // There are no more values at this level. If we're at the starting level, we're done.
                if (getDepth() == startingDepth) {
                    break;
                }
                // Otherwise, step out once and then try to move forward again.
                reader.stepOut();
                stepOut();
                continue;
            }

            // We found a value. Write out its field name and annotations, if any.
            write_value_field_name_helper(reader);
            write_value_annotations_helper(reader);

            if (reader.isNullValue()) {
                this.writeNull(type);
                continue;
            }

            switch (type) {
                case NULL:
                    // The isNullValue() check above will handle this.
                    throw new IllegalStateException("isNullValue() was false but IonType was NULL.");
                case BOOL:
                    writeBool(reader.booleanValue());
                    break;
                case INT:
                    writeInt(reader.bigIntegerValue());
                    break;
                case FLOAT:
                    writeFloat(reader.doubleValue());
                    break;
                case DECIMAL:
                    writeDecimal(reader.decimalValue());
                    break;
                case TIMESTAMP:
                    writeTimestamp(reader.timestampValue());
                    break;
                case STRING:
                    writeString(reader.stringValue());
                    break;
                case SYMBOL:
                    writeSymbolToken(reader.symbolValue());
                    break;
                case BLOB:
                    writeBlob(reader.newBytes());
                    break;
                case CLOB:
                    writeClob(reader.newBytes());
                    break;
                case STRUCT: // Intentional fallthrough
                case LIST:   // Intentional fallthrough
                case SEXP:
                    reader.stepIn();
                    stepIn(type);
                    break;
                default:
                    throw new IllegalStateException("Unknown value type: " + type);
            }
        }
    }

    //
    //  This code handles the skipped symbol table
    //  support - it is cloned in IonReaderTextUserX,
    //  IonReaderBinaryUserX and _Private_IonWriterBase
    //
    //  SO ANY FIXES HERE WILL BE NEEDED IN THOSE
    //  THREE LOCATIONS AS WELL.
    //
    private int _symbol_table_top = 0;
    private SymbolTable[] _symbol_table_stack = new SymbolTable[3]; // 3 is rare, IVM followed by a local sym tab with open content
    private void clear_system_value_stack()
    {
        while (_symbol_table_top > 0) {
            _symbol_table_top--;
            _symbol_table_stack[_symbol_table_top] = null;
        }
    }
    private void push_symbol_table(SymbolTable symbols)
    {
        assert(symbols != null);
        if (_symbol_table_top >= _symbol_table_stack.length) {
            int new_len = _symbol_table_stack.length * 2;
            SymbolTable[] temp = new SymbolTable[new_len];
            System.arraycopy(_symbol_table_stack, 0, temp, 0, _symbol_table_stack.length);
            _symbol_table_stack = temp;
        }
        _symbol_table_stack[_symbol_table_top++] = symbols;
    }
    public final SymbolTable pop_passed_symbol_table()
    {
        if (_symbol_table_top <= 0) {
            return null;
        }
        _symbol_table_top--;
        SymbolTable symbols = _symbol_table_stack[_symbol_table_top];
        _symbol_table_stack[_symbol_table_top] = null;
        return symbols;
    }

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