LocalSymbolTableImports.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 com.amazon.ion.SymbolTable;
import com.amazon.ion.SymbolToken;
import java.util.Arrays;
import java.util.List;

/**
 * This class manages the system symbol table and any shared symbol table(s)
 * imported by a local symbol table. It provides "find" methods to find
 * either symbol Ids or names in the imported tables.
 * <p>
 * This class is <b>immutable</b>, and hence safe for use by multiple threads.
 */
// TODO amazon-ion/ion-java/issues/37 Create specialized class to handle the common case where
//      there are zero or one imported non-system shared symtab(s).
final class LocalSymbolTableImports
{
    /**
     * The symtabs imported by a local symtab, never null or empty. The first
     * symtab must be a system symtab, the rest must be non-system shared
     * symtabs.
     */
    private final SymbolTable[] myImports;

    /**
     * The maxId of all imported tables, i.e., the sum of all maxIds declared
     * by symtabs in {@link #myImports}.
     */
    private final int           myMaxId;

    /**
     * The base Sid of each symtab in {@link #myImports} in parallel, i.e.,
     * {@link #myBaseSids}[0] references {@link #myImports}[0]. Must be
     * the same length as {@link #myImports}.
     */
    private final int[]         myBaseSids;

    //==========================================================================
    // Constructor(s) and static factory methods
    //==========================================================================

    /**
     * Constructor, takes the passed-in {@code importTables} containing the
     * imported symtabs.
     *
     * @param importTables
     *          the imported symtabs, must contain at least one element; the
     *          first element must be a system symtab, the rest must be
     *          non-system shared symtabs
     *
     * @throws IllegalArgumentException
     *          if any import is a local table, or if any but the first is a
     *          system table
     * @throws NullPointerException
     *          if any import is null
     */
    LocalSymbolTableImports(List<SymbolTable> importTables)
    {
        int importTablesSize = importTables.size();

        myImports = importTables.toArray(new SymbolTable[importTablesSize]);

        myBaseSids = new int[importTablesSize];
        myMaxId = prepBaseSids(myBaseSids, myImports);
    }

    /**
     * @param defaultSystemSymtab
     *          the default system symtab, which will be used if the first
     *          import in {@code imports} isn't a system symtab, never null
     * @param imports
     *          the set of shared symbol tables to import; the first (and only
     *          the first) may be a system table, in which case the
     *          {@code defaultSystemSymtab is ignored}
     *
     * @throws IllegalArgumentException
     *          if any import is a local table, or if any but the first is a
     *          system table
     * @throws NullPointerException
     *          if any import is null
     */
    LocalSymbolTableImports(SymbolTable defaultSystemSymtab,
                            SymbolTable... imports)
    {
        assert defaultSystemSymtab.isSystemTable()
            : "defaultSystemSymtab isn't a system symtab";

        if (imports != null && imports.length > 0)
        {
            if (imports[0].isSystemTable())
            {
                // copy imports as-is
                myImports = imports.clone();
            }
            else
            {
                // use defaultSystemSymtab and append imports
                myImports = new SymbolTable[imports.length + 1];
                myImports[0] = defaultSystemSymtab;
                System.arraycopy(imports, 0, myImports, 1, imports.length);
            }
        }
        else
        {
            // use defaultSystemSymtab only
            myImports = new SymbolTable[] { defaultSystemSymtab };
        }

        myBaseSids = new int[myImports.length];
        myMaxId = prepBaseSids(myBaseSids, myImports);
    }

    /**
     * Collects the necessary maxId info. from the passed-in {@code imports}
     * and populates the {@code baseSids} array.
     *
     * @return the sum of all imports' maxIds
     *
     * @throws IllegalArgumentException
     *          if any symtab beyond the first is a local or system symtab
     */
    private static int prepBaseSids(int[] baseSids, SymbolTable[] imports)
    {
        SymbolTable firstImport = imports[0];

        assert firstImport.isSystemTable()
            : "first symtab must be a system symtab";

        baseSids[0] = 0;
        int total = firstImport.getMaxId();

        for (int i = 1; i < imports.length; i++)
        {
            SymbolTable importedTable = imports[i];

            if (importedTable.isLocalTable() || importedTable.isSystemTable())
            {
                String message = "only non-system shared tables can be imported";
                throw new IllegalArgumentException(message);
            }

            baseSids[i] = total;
            total += imports[i].getMaxId();
        }

        return total;
    }

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

    String findKnownSymbol(int sid)
    {
        String name = null;

        if (sid <= myMaxId)
        {
            int i, previousBaseSid = 0;
            for (i = 1; i < myImports.length; i++)
            {
                int baseSid = myBaseSids[i];
                if (sid <= baseSid)
                {
                    break;
                }
                previousBaseSid = baseSid;
            }

            // if we run over myImports.length, the sid is in the last symtab
            int importScopedSid = sid - previousBaseSid;
            name = myImports[i-1].findKnownSymbol(importScopedSid);
        }

        return name;
    }

    int findSymbol(String name)
    {
        SymbolToken tok = find(name);
        return (tok == null ? UNKNOWN_SYMBOL_ID : tok.getSid());
    }

    /**
     * Finds a symbol already interned by an import, returning the lowest
     * known SID.
     * <p>
     * This method will not necessarily return the same instance given the
     * same input.
     *
     * @param text the symbol text to find
     *
     * @return
     *          the interned symbol (with both text and SID), or {@code null}
     *          if it's not defined by an imported table
     */
    SymbolToken find(String text)
    {
        for (int i = 0; i < myImports.length; i++)
        {
            SymbolTable importedTable = myImports[i];
            SymbolToken tok = importedTable.find(text);

            if (tok != null)
            {
                int sid = tok.getSid() + myBaseSids[i];
                text = tok.getText(); // Use interned instance

                assert text != null;

                return new SymbolTokenImpl(text, sid);
            }
        }
        return null;
    }

    int getMaxId()
    {
        return myMaxId;
    }

    /**
     * Gets the sole system symtab.
     */
    SymbolTable getSystemSymbolTable()
    {
        assert myImports[0].isSystemTable();
        return myImports[0];
    }

    /**
     * Gets all non-system shared symtabs (if any).
     *
     * @return a newly allocated copy of the imported symtabs
     */
    SymbolTable[] getImportedTables()
    {
        int count = myImports.length - 1; // we don't include system symtab
        SymbolTable[] imports = new SymbolTable[count];
        if (count > 0)
        {
            // defensive copy
            System.arraycopy(myImports, 1, imports, 0, count);
        }
        return imports;
    }

    /**
     * Returns the {@link #myImports} member field without making a copy.
     * <p>
     * <b>Note:</b> Callers must not modify the resulting SymbolTable array!
     * This will violate the immutability property of this class.
     *
     * @return
     *          the backing array of imported symtabs, as-is; the first element
     *          is a system symtab, the rest are non-system shared symtabs
     *
     * @see #getImportedTables()
     */
    SymbolTable[] getImportedTablesNoCopy()
    {
        return myImports;
    }

    @Override
    public String toString()
    {
        return Arrays.toString(myImports);
    }

    /**
     * Determines whether the passed-in instance has the same sequence of
     * symbol table imports as this instance. Note that equality of these
     * imports are checked using their reference, instead of their semantic
     * state.
     */
    boolean equalImports(LocalSymbolTableImports other)
    {
        return Arrays.equals(myImports, other.myImports);
    }
}