All Downloads are FREE. Search and download functionalities are using the official Maven repository.

oracle.kv.impl.api.table.FieldMap Maven / Gradle / Ivy

/*-
 * Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle NoSQL
 * Database made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle NoSQL Database for a copy of the license and
 * additional information.
 */

package oracle.kv.impl.api.table;

import static oracle.kv.impl.api.table.TableJsonUtils.FIELDS;
import static oracle.kv.impl.api.table.TableJsonUtils.NAME;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import oracle.kv.impl.util.SerialVersion;
import oracle.kv.table.FieldDef;

import com.sleepycat.persist.model.Persistent;

import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;

/**
 * FieldMap represents a RECORD type definition. RecordDefImpl stores an
 * instance of FieldMap, which does the bulk of the implementation work.
 * However, FieldMap is also used in TableImpl as well as the builder
 * utilities.
 *
 * fieldProperties:
 * An ArrayList of FieldMapEntry instances, one for each field declared by the
 * RECORD type. The list preserves the order of field declaration with the
 * RECORD type. Each FieldMapEntry stores the properties of a field, like its
 * name, type, nullability, and default value.
 *
 * fieldNames:
 * An ArrayList storing the names of the fields defined by this FieldMap.
 * Although the field names are stored in the FieldMapEntries, storing them
 * separately in this list as well avoids the creation of a new list in the
 * getFieldNames() method. So, this field acts as a cache.
 *
 * fieldPositions:
 * A TreeMap<String, Integer>, mapping field names to the position of the
 * associated fields within the RECORD declaration. Since fieldProperties
 * maintains the declaration order, these positions are used as indexes
 * inside the fieldProperties array. As a result, fieldPositions is used
 * to provide fast access to the field properties by field name.
 *
 * Note: fieldPositions does case-insensitive comparisons of the stored field
 * names, in order to implement the semantics of case-insensitive field names.
 *
 * Note: FieldMap is @Persistent but the comparator is not saved with the
 * object. This is not a problem because in all cases a new FieldMap is
 * constructed from the raw Map when the deserialized FieldMap is used so
 * order in that case does not matter.
 *
 * Note: this.fields and this.fieldOrder represent the previous implementation
 * of FieldMap (pre 4.2). When a FieldMap undergoes java-based serialization
 * (for example when table metadata is sent from a server to client) we have
 * to serialize in this old format, because we cannot know if the receiver
 * is pre or post 4.2. As a result, we have to keep these old fields around
 * and re-define the writeObject and readObject methods to always write and
 * read the old format. Specifically: (a) All the new fields are transient so
 * that they don't participate in the default java serialization. (b) The
 * writeObject method populates the old fields from the new ones, calls
 * defaultWriteObject, and then clears out the old fields, and (c) The
 * readObject method calls defaultReadObject to populate the old fields,
 * then populates the new fields from the old ones, and finally clears out
 * the old fields.
 *
 * Note: this.fieldOrder and this.fieldNames are basically the same thing
 * (and they are made to point the same ArrayList object). "fieldNames" is
 * just a nicer name than "fieldOrder".
 */
@Persistent(version=2)
public class FieldMap implements Cloneable, Serializable {

    private static final long serialVersionUID = 1L;

    @Deprecated
    private Map fields;

    @Deprecated
    private List fieldOrder;

    private transient List fieldProperties;

    private transient List fieldNames;

    private transient Map fieldPositions;

    public FieldMap() {
        fieldNames = new ArrayList();
        fieldProperties = new ArrayList();
        fieldPositions = new TreeMap(FieldComparator.instance);

        fields = new TreeMap(FieldComparator.instance);
        fieldOrder = fieldNames;
    }

    private FieldMap(FieldMap other) {
        assert(other.fieldPositions != null);
        assert(other.fieldProperties != null);

        int numFields = other.fieldNames.size();

        /* Do a deep copy of the fieldProperties list */
        fieldProperties = new ArrayList(numFields);

        for (FieldMapEntry fme : other.fieldProperties) {
            fieldProperties.add(fme.clone());
        }

        /* Copy the fieldPositions map */
        fieldPositions = new TreeMap(FieldComparator.instance);
        fieldPositions.putAll(other.fieldPositions);

        /* Copy of the fieldNames list */
        fieldNames = new ArrayList(other.fieldNames);

        fields = new TreeMap(FieldComparator.instance);
        fieldOrder = fieldNames;
    }

    @Override
    public FieldMap clone() {
        return new FieldMap(this);
    }

    /*
     * Multiple threads may try to serialize the same FieldMap at the same
     * time (for example there is a shared MetaData object at a server, which
     * is requested by multiple clients at the same time). Because the method
     * modifies the FieldMap object, it has to be synchronized.
     */
    private synchronized void writeObject(java.io.ObjectOutputStream out)
        throws IOException {

        try {
            int numFields = fieldProperties.size();

            for (int i = 0; i < numFields; ++i) {
                FieldMapEntry fme = fieldProperties.get(i);
                fields.put(fme.getFieldName(), fme);
            }

            out.defaultWriteObject();

            fields.clear();
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw e;
        }
    }

    /*
     * Override readObject() to handle deserialization of a binary FieldMap
     * created by an older-version server that used the old FieldMap impl.
     */
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException {

        try {
            in.defaultReadObject();

            /* Convert old format to new format */
            convertToNewFormat(false);

        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw e;
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw e;
        }
    }

    /*
     * This method is called from readObject() above, as well as from
     * TableMetadata.convert() to convert an old-format FieldMap that
     * was serialized via DPL.
     */
    void convertToNewFormat(boolean forDPL) {

        fieldNames = fieldOrder;

        fieldProperties = new ArrayList(fieldOrder.size());

        fieldPositions = new TreeMap(FieldComparator.instance);

        for (String fname : fieldNames) {

            FieldMapEntry oldFME = fields.get(fname);

            FieldValueImpl defVal = oldFME.getDefaultValueInternal();
            if (forDPL && defVal != null && defVal.isRecord()) {
                ((RecordValueImpl)defVal).convertToNewFormat();
            }

            FieldMapEntry newFME = new FieldMapEntry(fname,
                                                     oldFME.getFieldDef(),
                                                     oldFME.isNullable(),
                                                     defVal);

            fieldProperties.add(newFME);
            fieldPositions.put(fname, fieldProperties.size() - 1);
        }

        fields.clear();
    }

    Map getFieldPositions() {
        return fieldPositions;
    }

    List getFieldProperties() {
        return fieldProperties;
    }

    List getFieldNames() {
        return fieldNames;
    }

    FieldMapEntry getFieldMapEntry(String name) {
        Integer pos = fieldPositions.get(name);
        return (pos != null ? fieldProperties.get(pos) : null);
    }

    FieldMapEntry getFieldMapEntry(int pos) {
        return fieldProperties.get(pos);
    }

    public String getFieldName(int pos) {
        return fieldNames.get(pos);
    }

    boolean exists(String name) {
        return fieldPositions.containsKey(name);
    }

    int getFieldPos(String name) {
        Integer pos = fieldPositions.get(name);
        if (pos == null) {
            throw new IllegalArgumentException(
                "There is no field with name " + name);
        }
        return pos.intValue();
    }

    public FieldDefImpl getFieldDef(int pos) {
        return fieldProperties.get(pos).getFieldDef();
    }

    FieldDefImpl getFieldDef(String name) {
        FieldMapEntry fme = getFieldMapEntry(name);
        return (fme != null ? fme.getFieldDef() :  null);
    }

    FieldValueImpl getDefaultValue(int pos) {
        return fieldProperties.get(pos).getDefaultValue();
    }

    boolean isNullable(int pos) {
        return fieldProperties.get(pos).isNullable();
    }

    /**
     * Utility method used by the query translator.
     */
    public void reverseFieldOrder() {
        Collections.reverse(fieldNames);
        Collections.reverse(fieldProperties);
        for (int i = 0; i < fieldProperties.size(); ++i) {
            fieldPositions.put(fieldProperties.get(i).getFieldName(), i);
        }
    }

    void put(FieldMapEntry fme) {

        if (fieldPositions.put(fme.getFieldName(),
                               fieldPositions.size()) != null) {
            throw new IllegalArgumentException(
              "Field " + fme.getFieldName() + " exists already");
        }

        fieldProperties.add(fme);
        fieldNames.add(fme.getFieldName());
    }

    public void put(
        String name,
        FieldDefImpl type,
        boolean nullable,
        FieldValueImpl defaultValue) {

        put(new FieldMapEntry(name, type, nullable, defaultValue));
    }

    boolean remove(String name) {

        Integer pos = fieldPositions.remove(name);
        if (pos == null) {
            return false;
        }

        FieldMapEntry fme = fieldProperties.remove(pos.intValue());
        assert(fme != null);

        fieldNames.remove(pos.intValue());

        /*
         * Adjust the positions stored in fieldPositions: subtract 1 from all
         * positions after "pos".
         */
        for (Map.Entry entry : fieldPositions.entrySet()) {
            String fname = entry.getKey();
            Integer fpos = entry.getValue();
            if (fpos > pos) {
                fieldPositions.put(fname, fpos - 1);
            }
        }

        return true;
    }

    public boolean isEmpty() {
        return fieldProperties.isEmpty();
    }

    int size() {
        return fieldProperties.size();
    }

    /**
     * Compare equality.  Field names are case-insensitive, so ignore case.
     * Order of declaration does not matter for equality.
     */
    @Override
    public boolean equals(Object obj) {

        if (!(obj instanceof FieldMap)) {
            return false;
        }

        FieldMap other = (FieldMap) obj;

        if (fieldProperties.size() != other.fieldProperties.size()) {
            return false;
        }

        for (int i = 0; i < fieldProperties.size(); ++i) {

            FieldMapEntry fme1 = fieldProperties.get(i);
            FieldMapEntry fme2 = other.fieldProperties.get(i);
            if (fme1.equals(fme2)) {
                continue;
            }
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("FieldMap[");
        for (FieldMapEntry fme : fieldProperties) {
            sb.append(fme).append(" ");
        }
        sb.append("]");
        return sb.toString();
    }

    public boolean isPrecise() {

        for (FieldMapEntry fme : fieldProperties) {
            if (!fme.isPrecise()) {
                return false;
            }
        }
        return true;
    }

    public boolean isSubtype(FieldMap superType) {

        if (fieldProperties.size() != superType.fieldProperties.size()) {
            return false;
        }

        for (int i = 0; i < fieldProperties.size(); ++i) {

            FieldMapEntry fme1 = fieldProperties.get(i);
            FieldMapEntry fme2 = superType.fieldProperties.get(i);
            if (fme1.isSubtype(fme2)) {
                continue;
            }
            return false;
        }

        return true;
    }


    @Override
    public int hashCode() {
        int code = fieldProperties.size();
        for (FieldMapEntry fme : fieldProperties) {
            code += fme.hashCode();
        }
        return code;
    }

    /**
     * Puts the fields of this map into an ObjectNode for display as JSON.
     * This is called indirectly from toJsonString() methods on tables and
     * records.  Output in declaration order.
     */
    void putFields(ObjectNode node) {

        ArrayNode array = node.putArray(FIELDS);

        for (FieldMapEntry fme : fieldProperties) {
            ObjectNode fnode = array.addObject();
            fnode.put(NAME, fme.getFieldName());
            fme.toJson(fnode);
        }
    }

    FieldMapEntry getFieldMapEntry(TablePath tablePath) {
        FieldMap containingMap = findContainingMap(tablePath);
        String lastStep = tablePath.getLastStep();
        return containingMap.getFieldMapEntry(lastStep);
    }

    /*
     * Find the FieldMap containing the entry for a record field declared in
     * a table or record schema. The field to lookup may be deeply nested
     * inside the type hierarchy, so it is specified as a path consisting of
     * field-name and [] steps.
     *
     * @throws IllegalArgumentException for param values that don't work in
     * the current state
     */
    public FieldMap findContainingMap(TablePath tablePath) {

        int numSteps = tablePath.numSteps();

        if (numSteps == 0) {
            throw new IllegalArgumentException(" Field path is empty.");
        }

        if (tablePath.getLastStep() == TableImpl.BRACKETS ||
            tablePath.getLastStep().equalsIgnoreCase(TableImpl.VALUES)) {
            throw new IllegalArgumentException(
                "Path " + tablePath.getPathName() +
                " does not lead to a record field");
        }

        String currentStep = tablePath.getStep(0);
        FieldDef def = getFieldDef(currentStep);
        if (def == null) {
            throw new IllegalArgumentException(
                "Field \"" + currentStep + "\" in path " +
                currentStep + " does not exist.");
        }

        if (numSteps == 1) {
            return this;
        }

        int stepIndex = 1;

        while (stepIndex < numSteps - 1) {

            currentStep = tablePath.getStep(stepIndex);
            ++stepIndex;

            if (currentStep == TableImpl.BRACKETS) {
                if (def.isArray()) {
                    def = def.asArray().getElement();
                    assert def != null;
                } else {
                    throw new IllegalArgumentException(
                        "Step \"[]\" in path " + tablePath.getPathName() +
                        " does not have an array as input");
                }
            } else if (currentStep.equalsIgnoreCase(TableImpl.VALUES)) {
                if (def.isMap()) {
                    def = def.asMap().getElement();
                    assert def != null;
                } else {
                    throw new IllegalArgumentException(
                        "Step \".values()\" in path " + tablePath.getPathName() +
                        " does not have a map as input");
                }
            } else {
                // def must be a record type
                if (def.isRecord()) {
                    def = def.asRecord().getFieldDef(currentStep);

                    if (def == null) {
                        throw new IllegalArgumentException(
                            "Field \"" + currentStep + "\" in path " +
                             tablePath.getPathName() + " does not exist.");
                    }
                    //} else if (def.isArray() || def.isMap()) {
                    // TODO: allow for the [] steps to be optional.
                } else if (def.isAnyRecord()) {
                    throw new IllegalStateException(
                        "AnyRecord cannot be used in table schema.");
                } else {
                    throw new IllegalArgumentException(
                        "Step \"" + currentStep + "\" in path " +
                        tablePath.getPathName() +
                        " does not have a record as input");
                }
            }
        }

        currentStep = tablePath.getStep(stepIndex);

        if (!def.isRecord()) {
            throw new IllegalArgumentException(
                "Step \"" + currentStep + "\" in path " +
                tablePath.getPathName() +
                " does not have a record as input");
        }

        if (def.asRecord().getFieldDef(currentStep) == null) {
            throw new IllegalArgumentException(
                "Field \"" + currentStep + "\" in path " +
                tablePath.getPathName() + " does not exist.");
        }

        return ((RecordDefImpl)def).getFieldMap();
    }

    /**
     * Remove a field from the schema of a table or record. The field to
     * remove may be deeply nested inside the type hierarchy, so it is
     * specified as a path consisting of field-name and [] steps.
     *
     *  @throws IllegalArgumentException for param values that don't work in
     *  the current state
     */
    public void removeField(List stepsList) {
        TablePath tablePath = new TablePath(this, stepsList);
        removeField(tablePath);
    }

    void removeField(TablePath tablePath) {
        FieldMap containingMap = findContainingMap(tablePath);
        containingMap.remove(tablePath.getLastStep());
    }

    /**
     * Returns the minimum version of the server that can execute this
     * fieldMap. This is the maximum serialVersion of the fields
     * contained in this FieldMap.
     */
    public short getRequiredSerialVersion() {
        short requiredSerialVersion = SerialVersion.TABLE_API_VERSION;
        for (int i = size() - 1; i >= 0; i--) {
            requiredSerialVersion = (short)Math.max(
                requiredSerialVersion,
                getFieldDef(i).getRequiredSerialVersion());
        }
        return requiredSerialVersion;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy