/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License 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 org.apache.openjpa.jdbc.meta;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.enhance.Reflection;
import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.strats.NoneClassStrategy;
import org.apache.openjpa.jdbc.meta.strats.VerticalClassStrategy;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ColumnIO;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Schemas;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.sql.Joins;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.RowManager;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.PCState;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;
import org.apache.openjpa.util.OpenJPAId;

/**
 * Specialization of metadata for relational databases.
 *
 * @author Abe White
 */
public class ClassMapping
    extends ClassMetaData
    implements ClassStrategy {

    
    private static final long serialVersionUID = 1L;

    public static final ClassMapping[] EMPTY_MAPPINGS = new ClassMapping[0];

    private static final Localizer _loc = Localizer.forPackage
        (ClassMapping.class);

    private final ClassMappingInfo _info;
    private final Discriminator _discrim;
    private final Version _version;
    private ClassStrategy _strategy = null;

    private Table _table = null;
    private ColumnIO _io = null;
    private Column[] _cols = Schemas.EMPTY_COLUMNS;
    private ForeignKey _fk = null;
    private int _subclassMode = Integer.MAX_VALUE;

    private ClassMapping[] _joinSubMaps = null;
    private ClassMapping[] _assignMaps = null;

    // maps columns to joinables
    private final Map _joinables = new ConcurrentHashMap();

    /**
     * Constructor. Supply described type and owning repository.
     */
    protected ClassMapping(Class type, MappingRepository repos) {
        super(type, repos);
        _discrim = repos.newDiscriminator(this);
        _version = repos.newVersion(this);
        _info = repos.newMappingInfo(this);
    }

    /**
     * Embedded constructor. Supply embedding value and owning repository.
     */
    protected ClassMapping(ValueMetaData vmd) {
        super(vmd);
        _discrim = getMappingRepository().newDiscriminator(this);
        _version = getMappingRepository().newVersion(this);
        _info = getMappingRepository().newMappingInfo(this);
    }

    /**
     * The class discriminator.
     */
    public Discriminator getDiscriminator() {
        return _discrim;
    }

    /**
     * The version indicator.
     */
    public Version getVersion() {
        return _version;
    }

    ///////////
    // Runtime
    ///////////

    /**
     * Return the oid value stored in the result. This implementation will
     * recurse until it finds an ancestor class who uses oid values for its
     * primary key.
     *
     * @param fk if non-null, use the local columns of the given foreign
     * key in place of this class' primary key columns
     * @see #isPrimaryKeyObjectId
     */
    public Object getObjectId(JDBCStore store, Result res, ForeignKey fk,
        boolean subs, Joins joins)
        throws SQLException {
        ValueMapping embed = getEmbeddingMapping();
        if (embed != null)
            return embed.getFieldMapping().getDefiningMapping().
                getObjectId(store, res, fk, subs, joins);

        return getObjectId(this, store, res, fk, subs, joins);
    }

    /**
     * Recursive helper for public <code>getObjectId</code> method.
     */
    private Object getObjectId(ClassMapping cls, JDBCStore store, Result res,
        ForeignKey fk, boolean subs, Joins joins)
        throws SQLException {
        if (!isPrimaryKeyObjectId(true))
            return getPCSuperclassMapping().getObjectId(cls, store, res, fk,
                subs, joins);
        if (getIdentityType() == ID_UNKNOWN)
            throw new InternalException();

        Column[] pks = getPrimaryKeyColumns();
        if (getIdentityType() == ID_DATASTORE) {
            Column col = (fk == null) ? pks[0] : fk.getColumn(pks[0]);
            long id = res.getLong(col, joins);
            return (id == 0 && res.wasNull()) ? null
                : store.newDataStoreId(id, cls, subs);
        }

        // application identity
        Object[] vals = new Object[getPrimaryKeyFields().length];
        FieldMapping fm;
        Joinable join;
        int pkIdx;
        boolean canReadDiscriminator = true;
        boolean isNullPK = true;
        for (Column pk : pks) {
            // we know that all pk column join mappings use primary key fields,
            // cause this mapping uses the oid as its primary key (we recursed
            // at the beginning of the method to ensure this)
            join = assertJoinable(pk);
            fm = getFieldMapping(join.getFieldIndex());
            pkIdx = fm.getPrimaryKeyIndex();
            canReadDiscriminator &= isSelfReference(fk, join.getColumns());
            // could have already set value with previous multi-column joinable
            if (vals[pkIdx] == null) {
                res.startDataRequest(fm);
                vals[pkIdx] = join.getPrimaryKeyValue(res, join.getColumns(),
                        fk, store, joins);
                res.endDataRequest();
                isNullPK = isNullPK && vals[pkIdx] == null;
            }
        }
        if (isNullPK) {
            return null;
        }

        // the oid data is loaded by the base type, but if discriminator data
        // is present, make sure to use it to construct the actual oid instance
        // so that we get the correct app id class, etc

        // Discriminator refers to the row but the vals[] may hold data that
        // refer to another row. Then there is little point reading the disc
        // value

        ClassMapping dcls = cls;
        if (subs && canReadDiscriminator) {
            res.startDataRequest(cls.getDiscriminator());
            try {
                Class dtype = cls.getDiscriminator().getClass(store, cls, res);
                if (dtype != cls.getDescribedType())
                  dcls = cls.getMappingRepository().getMapping(dtype,
                    store.getContext().getClassLoader(), true);
            } catch (Exception e) {
                // intentionally ignored
            }
            res.endDataRequest();
        }
        Object oid = ApplicationIds.fromPKValues(vals, dcls);
        if (oid instanceof OpenJPAId) {
            ((OpenJPAId) oid).setManagedInstanceType(dcls.getDescribedType(),
                subs);
        }
        return oid;
    }

    boolean isSelfReference(ForeignKey fk, Column[] cols) {
    	if (fk == null)
    		return true;
    	for (Column col : cols)
    		if (fk.getColumn(col) != col)
    			return false;
    	return true;
    }

    /**
     * Return the given column value(s) for the given object. The given
     * columns will be primary key columns of this mapping, but may be in
     * any order. If there is only one column, return its value. If there
     * are multiple columns, return an object array of their values, in the
     * same order the columns are given.
     */
    public Object toDataStoreValue(Object obj, Column[] cols, JDBCStore store) {
        Object ret = (cols.length == 1) ? null : new Object[cols.length];

        // in the past we've been lenient about being able to translate objects
        // from other persistence contexts, so try to get sm directly from
        // instance before asking our context
        OpenJPAStateManager sm;
        if (ImplHelper.isManageable(obj)) {
        	PersistenceCapable pc = ImplHelper.toPersistenceCapable(obj,
                    getRepository().getConfiguration());
            sm = (OpenJPAStateManager) pc.pcGetStateManager();
            if (sm == null) {
            	ret = getValueFromUnmanagedInstance(obj, cols, true);

                // OPENJPA-2631 start
                // Check to see if we are dealing with a Embeddable pk. If the PK is an Embeddable, AND IFF the
                // columns in the Embeddable are greater than 1, we are dealing with a composite primary
                // key, and as such 'ret' will be an instance of the embeddable, NOT the individual PK values.
                // Given this, we need to dig deeper and get the individual values of the embeddable key.
            	// On the other hand, if the embeddable only contains one column, 'ret' will be the value of
            	// that column and as such no further digging is necessary.
                FieldMapping[] fmsPK = this.getPrimaryKeyFieldMappings();
                List<FieldMapping> fms = getFieldMappings(cols, true);

                // Note that if we are dealing with an embeddable that is an EmbeddableId, the fms.size will
                // always be 1 (since an EmbeddableId is slightly opaque, we don't have an fms for each field).
                // If on the other hand we are dealing with an embeddable that is an @IdClass, fms.size will be the
                // number columns in the @IdClass.  Furthermore, when dealing with @IdClass, 'ret' will already
                // properly contain the column values, therefore no further processing is needed.
                if (fmsPK.length > 0 && fmsPK[0].isEmbedded() && cols.length > 1 && fms.size() == 1) {
                    // OK, we know this PK is an embeddable. So get the individual field values.
                    Object[] tmpRet = new Object[cols.length];
                    for (int i = 0; i < cols.length; i++) {
                        Joinable join = this.assertJoinable(cols[i]);
                        tmpRet[i] = join.getJoinValue(ret, cols[i], store);
                    }
                    ret = tmpRet;
                }
                // OPENJPA-2631 end
            } else if (sm.isDetached()) {
            	obj = store.getContext().find(sm.getObjectId(), false, null);
            	sm = store.getContext().getStateManager(obj);
            }
        } else {
            sm = store.getContext().getStateManager(obj);
        }
        if (sm == null)
            return ret;

        Object val;
        for (int i = 0; i < cols.length; i++) {
            val = assertJoinable(cols[i]).getJoinValue(sm, cols[i], store);
            if (cols.length == 1)
                ret = val;
            else
                ((Object[]) ret)[i] = val;
        }
        return ret;
    }

    /**
     * Return the joinable for the given column, or throw an exception if
     * none is available.
     */
    public Joinable assertJoinable(Column col) {
        Joinable join = getJoinable(col);
        if (join == null)
            throw new MetaDataException(_loc.get("no-joinable",
                col.getQualifiedPath().toString()));
        return join;
    }

    /**
     * Return the {@link Joinable} for the given column. Any column that
     * another mapping joins to must be controlled by a joinable.
     */
    public Joinable getJoinable(Column col) {
        Joinable join;
        if (getEmbeddingMetaData() != null) {
            join = getEmbeddingMapping().getFieldMapping().
                getDefiningMapping().getJoinable(col);
            if (join != null)
                return join;
        }
        ClassMapping sup = getJoinablePCSuperclassMapping();
        if (sup != null) {
            join = sup.getJoinable(col);
            if (join != null)
                return join;
        }
        return (Joinable) _joinables.get(col);
    }

    /**
     * Add the given column-to-joinable mapping.
     */
    public void setJoinable(Column col, Joinable joinable) {
        // don't let non-pk override pk
        Joinable join = (Joinable) _joinables.get(col);
        if (join == null || (join.getFieldIndex() != -1
            && getField(join.getFieldIndex()).getPrimaryKeyIndex() == -1))
            _joinables.put(col, joinable);
    }

    /**
     * Return whether the columns of the given foreign key to this mapping
     * can be used to construct an object id for this type. This is a
     * relatively expensive operation; its results should be cached.
     *
     * @return {@link Boolean#TRUE} if the foreign key contains all oid
     * columns, <code>null</code> if it contains only some columns,
     * or {@link Boolean#FALSE} if it contains non-oid columns
     */
    public Boolean isForeignKeyObjectId(ForeignKey fk) {
        // if this mapping's primary key can't construct an oid, then no way
        // foreign key can
        if (getIdentityType() == ID_UNKNOWN || !isPrimaryKeyObjectId(false))
            return Boolean.FALSE;

        // with datastore identity, it's all or nothing
        Column[] cols = fk.getPrimaryKeyColumns();
        if (getIdentityType() == ID_DATASTORE) {
            if (cols.length != 1 || cols[0] != getPrimaryKeyColumns()[0])
                return Boolean.FALSE;
            return Boolean.TRUE;
        }

        // check the join mapping for each pk column to see if it links up to
        // a primary key field
        Joinable join;
        for (Column col : cols) {
            join = assertJoinable(col);
            if (join.getFieldIndex() != -1
                    && getField(join.getFieldIndex()).getPrimaryKeyIndex() == -1)
                return Boolean.FALSE;
        }

        // if all primary key links, see whether we join to all pks
        if (isPrimaryKeyObjectId(true)
            && cols.length == getPrimaryKeyColumns().length)
            return Boolean.TRUE;
        return null;
    }

    ///////
    // ORM
    ///////

    /**
     * Raw mapping data.
     */
    public ClassMappingInfo getMappingInfo() {
        return _info;
    }

    /**
     * The strategy used to map this mapping.
     */
    public ClassStrategy getStrategy() {
        return _strategy;
    }

    /**
     * The strategy used to map this mapping. The <code>adapt</code>
     * parameter determines whether to adapt when mapping the strategy;
     * use null if the strategy should not be mapped.
     */
    public void setStrategy(ClassStrategy strategy, Boolean adapt) {
        // set strategy first so we can access it during mapping
        ClassStrategy orig = _strategy;
        _strategy = strategy;
        if (strategy != null) {
            try {
                strategy.setClassMapping(this);
                if (adapt != null)
                    strategy.map(adapt);
            } catch (RuntimeException re) {
                // reset strategy
                _strategy = orig;
                throw re;
            }
        }
    }

    /**
     * The mapping's primary table.
     */
    public Table getTable() {
        return _table;
    }

    /**
     * The mapping's primary table.
     */
    public void setTable(Table table) {
        _table = table;
    }

    /**
     * The columns this mapping uses to uniquely identify an object.
     * These will typically be the primary key columns or the columns this
     * class uses to link to its superclass table.
     */
    public Column[] getPrimaryKeyColumns() {
        if (getIdentityType() == ID_APPLICATION && isMapped()) {
            if (_cols.length == 0) {
                FieldMapping[] pks = getPrimaryKeyFieldMappings();
                Collection cols = new ArrayList(pks.length);
                Column[] fieldCols;
                for (FieldMapping pk : pks) {
                    fieldCols = pk.getColumns();
                    if (fieldCols.length == 0) {
                        _cols = new Column[0];
                        return _cols;
                    }
                    for (Column fieldCol : fieldCols) {
                        cols.add(fieldCol);
                    }
                }
                _cols = (Column[]) cols.toArray(new Column[cols.size()]);
            }
        }
        return _cols;
    }

    /**
     * The columns this mapping uses to uniquely identify an object.
     * These will typically be the primary key columns or the columns this
     * class uses to link to its superclass table.
     */
    public void setPrimaryKeyColumns(Column[] cols) {
        if (cols == null)
            cols = Schemas.EMPTY_COLUMNS;
        _cols = cols;
    }

    /**
     * I/O information on the key columns / join key.
     */
    public ColumnIO getColumnIO() {
        return (_io == null) ? ColumnIO.UNRESTRICTED : _io;
    }

    /**
     * I/O information on the key columns / join key.
     */
    public void setColumnIO(ColumnIO io) {
        _io = io;
    }

    /**
     * Foreign key linking the primary key columns to the superclass table,
     * or null if none.
     */
    public ForeignKey getJoinForeignKey() {
        return _fk;
    }

    /**
     * Foreign key linking the primary key columns to the superclass table,
     * or null if none.
     */
    public void setJoinForeignKey(ForeignKey fk) {
        _fk = fk;
    }

    public void refSchemaComponents() {
        if (getEmbeddingMetaData() == null) {
            if (_table != null && _table.getPrimaryKey() != null)
                _table.getPrimaryKey().ref();
            if (_fk != null)
                _fk.ref();
            Column[] pks = getPrimaryKeyColumns();
            for (Column pk : pks) {
                pk.ref();
            }
        } else {
            FieldMapping[] fields = getFieldMappings();
            for (FieldMapping field : fields) {
                field.refSchemaComponents();
            }
        }
    }

    /**
     * Clear mapping information, including strategy.
     */
    public void clearMapping() {
        _strategy = null;
        _cols = Schemas.EMPTY_COLUMNS;
        _fk = null;
        _table = null;
        _info.clear();
        setResolve(MODE_MAPPING | MODE_MAPPING_INIT, false);
    }

    /**
     * Update {@link MappingInfo} with our current mapping information.
     */
    public void syncMappingInfo() {
        if (getEmbeddingMetaData() == null)
            _info.syncWith(this);
        else {
            _info.clear();
            FieldMapping[] fields = getFieldMappings();
            for (FieldMapping field : fields) {
                field.syncMappingInfo();
            }
        }
    }

    //////////////////////
    // MetaData interface
    //////////////////////

    @Override
    protected void setDescribedType(Class type) {
        super.setDescribedType(type);
        // this method called from superclass constructor, so _info not yet
        // initialized
        if (_info != null)
            _info.setClassName(type.getName());
    }

    /**
     * The subclass fetch mode, as one of the eager constants in
     * {@link JDBCFetchConfiguration}.
     */
    public int getSubclassFetchMode() {
        if (_subclassMode == Integer.MAX_VALUE) {
            if (getPCSuperclass() != null)
                _subclassMode = getPCSuperclassMapping().
                    getSubclassFetchMode();
            else
                _subclassMode = FetchConfiguration.DEFAULT;
        }
        return _subclassMode;
    }

    /**
     * The subclass fetch mode, as one of the eager constants in
     * {@link JDBCFetchConfiguration}.
     */
    public void setSubclassFetchMode(int mode) {
        _subclassMode = mode;
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getRepository}.
     */
    public MappingRepository getMappingRepository() {
        return (MappingRepository) getRepository();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getEmbeddingMetaData}
     */
    public ValueMapping getEmbeddingMapping() {
        return (ValueMapping) getEmbeddingMetaData();
    }

    /**
     * Returns true if this class does not use the "none" strategy (including
     * if it has a null strategy, and therefore is probably in the process of
     * being mapped).
     */
    @Override
    public boolean isMapped() {
        if (!super.isMapped())
            return false;
        if (_strategy != null)
            return _strategy != NoneClassStrategy.getInstance();
        return !NoneClassStrategy.ALIAS.equals(_info.getStrategy());
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getPCSuperclassMetaData}.
     */
    public ClassMapping getPCSuperclassMapping() {
        return (ClassMapping) getPCSuperclassMetaData();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getMappedPCSuperclassMetaData}.
     */
    public ClassMapping getMappedPCSuperclassMapping() {
        return (ClassMapping) getMappedPCSuperclassMetaData();
    }

    /**
     * Return the nearest mapped superclass that can join to this class.
     */
    public ClassMapping getJoinablePCSuperclassMapping() {
        ClassMapping sup = getMappedPCSuperclassMapping();
        if (sup == null)
            return null;
        if (_fk != null || _table == null || _table.equals(sup.getTable()))
            return sup;
        return null;
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getPCSubclassMetaDatas}.
     */
    public ClassMapping[] getPCSubclassMappings() {
        return (ClassMapping[]) getPCSubclassMetaDatas();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getMappedPCSubclassMetaDatas}.
     */
    public ClassMapping[] getMappedPCSubclassMappings() {
        return (ClassMapping[]) getMappedPCSubclassMetaDatas();
    }

    /**
     * Return mapped subclasses that are reachable via joins.
     */
    public ClassMapping[] getJoinablePCSubclassMappings() {
        ClassMapping[] subs = getMappedPCSubclassMappings(); // checks for new
        if (_joinSubMaps == null) {
            if (subs.length == 0)
                _joinSubMaps = subs;
            else {
                List joinable = new ArrayList(subs.length);
                for (ClassMapping sub : subs)
                    if (isSubJoinable(sub))
                        joinable.add(sub);
                _joinSubMaps = (ClassMapping[]) joinable.toArray
                    (new ClassMapping[joinable.size()]);
            }
        }
        return _joinSubMaps;
    }

    /**
     * Return whether we can reach the given subclass via joins.
     */
    private boolean isSubJoinable(ClassMapping sub) {
        if (sub == null)
            return false;
        if (sub == this)
            return true;
        return isSubJoinable(sub.getJoinablePCSuperclassMapping());
    }

    /**
     * Returns the closest-derived list of non-inter-joinable mapped types
     * assignable to this type. May return this mapping.
     */
    public ClassMapping[] getIndependentAssignableMappings() {
        ClassMapping[] subs = getMappedPCSubclassMappings(); // checks for new
        if (_assignMaps == null) {
            // remove unmapped subs
            if (subs.length == 0) {
                if (isMapped())
                    _assignMaps = new ClassMapping[]{ this };
                else
                    _assignMaps = subs;
            } else {
                int size = (int) (subs.length * 1.33 + 2);
                Set independent = new LinkedHashSet(size);
                if (isMapped())
                    independent.add(this);
                independent.addAll(Arrays.asList(subs));

                // remove all mappings that have a superclass mapping in the set
                ClassMapping map, sup;
                List clear = null;
                for (Object o : independent) {
                    map = (ClassMapping) o;
                    sup = map.getJoinablePCSuperclassMapping();
                    if (sup != null && independent.contains(sup)) {
                        if (clear == null)
                            clear = new ArrayList(independent.size() - 1);
                        clear.add(map);
                    }
                }
                if (clear != null)
                    independent.removeAll(clear);

                _assignMaps = (ClassMapping[]) independent.toArray
                    (new ClassMapping[independent.size()]);
            }
        }
        return _assignMaps;
    }

    /**
     * Convenience method to perform cast from {@link ClassMetaData#getFields}.
     */
    public FieldMapping[] getFieldMappings() {
        return (FieldMapping[]) getFields();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getDeclaredFields}.
     */
    public FieldMapping[] getDeclaredFieldMappings() {
        return (FieldMapping[]) getDeclaredFields();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getPrimaryKeyFields}.
     */
    public FieldMapping[] getPrimaryKeyFieldMappings() {
        return (FieldMapping[]) getPrimaryKeyFields();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getVersionField}.
     */
    public FieldMapping getVersionFieldMapping() {
        return (FieldMapping) getVersionField();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getDefaultFetchGroupFields}.
     */
    public FieldMapping[] getDefaultFetchGroupFieldMappings() {
        return (FieldMapping[]) getDefaultFetchGroupFields();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getDefinedFields}.
     */
    public FieldMapping[] getDefinedFieldMappings() {
        return (FieldMapping[]) getDefinedFields();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getFieldsInListingOrder}.
     */
    public FieldMapping[] getFieldMappingsInListingOrder() {
        return (FieldMapping[]) getFieldsInListingOrder();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getDefinedFieldsInListingOrder}.
     */
    public FieldMapping[] getDefinedFieldMappingsInListingOrder() {
        return (FieldMapping[]) getDefinedFieldsInListingOrder();
    }

    /**
     * Convenience method to perform cast from {@link ClassMetaData#getField}.
     */
    public FieldMapping getFieldMapping(int index) {
        return (FieldMapping) getField(index);
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getDeclaredField}.
     */
    public FieldMapping getDeclaredFieldMapping(int index) {
        return (FieldMapping) getDeclaredField(index);
    }

    /**
     * Convenience method to perform cast from {@link ClassMetaData#getField}.
     */
    public FieldMapping getFieldMapping(String name) {
        return (FieldMapping) getField(name);
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getDeclaredField}.
     */
    public FieldMapping getDeclaredFieldMapping(String name) {
        return (FieldMapping) getDeclaredField(name);
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#getDeclaredUnmanagedFields}.
     */
    public FieldMapping[] getDeclaredUnmanagedFieldMappings() {
        return (FieldMapping[]) getDeclaredUnmanagedFields();
    }

    /**
     * Convenience method to perform cast from
     * {@link ClassMetaData#addDeclaredField}.
     */
    public FieldMapping addDeclaredFieldMapping(String name, Class type) {
        return (FieldMapping) addDeclaredField(name, type);
    }

    @Override
    protected void resolveMapping(boolean runtime) {
        super.resolveMapping(runtime);

        // map class strategy; it may already be mapped by the repository before
        // the resolve process begins
        MappingRepository repos = getMappingRepository();
        if (_strategy == null)
            repos.getStrategyInstaller().installStrategy(this);
        Log log = getRepository().getLog();
        if (log.isTraceEnabled())
            log.trace(_loc.get("strategy", this, _strategy.getAlias()));

        // make sure unmapped superclass fields are defined if we're mapped;
        // also may have been done by repository already
        defineSuperclassFields(getJoinablePCSuperclassMapping() == null);

        // resolve everything that doesn't rely on any relations to avoid
        // recursion, then resolve all fields
        resolveNonRelationMappings();
        FieldMapping[] fms = getFieldMappings();
        for (FieldMapping fieldMapping : fms) {
            if (fieldMapping.getDefiningMetaData() == this) {
                boolean fill = getMappingRepository().getMappingDefaults().
                        defaultMissingInfo();
                ForeignKey fk = fieldMapping.getForeignKey();
                if (fill && fk != null &&
                        fk.getPrimaryKeyColumns().length == 0) {
                    // set resolve mode to force this field mapping to be
                    // resolved again. The need to resolve again occurs when
                    // a primary key is a relation field with the foreign key
                    // annotation. In this situation, this primary key field
                    // mapping is resolved during the call to
                    // resolveNonRelationMapping. Since it is a relation
                    // field, the foreign key will be constructed. However,
                    // the primary key of the parent entity may not have been
                    // resolved yet, resulting in missing information in the fk
                    fieldMapping.setResolve(MODE_META);
                    if (fieldMapping.getStrategy() != null)
                        fieldMapping.getStrategy().map(false);
                }
                fieldMapping.resolve(MODE_MAPPING);
            }
        }
        fms = getDeclaredUnmanagedFieldMappings();
        for (FieldMapping fm : fms) {
            fm.resolve(MODE_MAPPING);
        }

        // mark mapped columns
        if (_cols != null) {
            ColumnIO io = getColumnIO();
            for (int i = 0; i < _cols.length; i++) {
                if (io.isInsertable(i, false))
                    _cols[i].setFlag(Column.FLAG_DIRECT_INSERT, true);
                if (io.isUpdatable(i, false))
                    _cols[i].setFlag(Column.FLAG_DIRECT_UPDATE, true);
            }
        }
        // once columns are resolved, resolve unique constraints as they need
        // the columns be resolved
        _info.getUniques(this, true);
        _info.getIndices(this, true);
    }

    /**
     * Resolve non-relation field mappings so that when we do relation
     * mappings they can rely on them for joins.
     */
    void resolveNonRelationMappings() {
        // make sure primary key fields are resolved first because other
        // fields might rely on them
        FieldMapping[] fms = getPrimaryKeyFieldMappings();
        for (FieldMapping fieldMapping : fms) {
            fieldMapping.resolve(MODE_MAPPING);
        }

        // resolve defined fields that are safe; that don't rely on other types
        // also being resolved.  don't use getDefinedFields b/c it relies on
        // whether fields are mapped, which isn't known yet
        fms = getFieldMappings();
        for (FieldMapping fm : fms) {
            if (fm.getDefiningMetaData() == this
                    && !fm.isTypePC() && !fm.getKey().isTypePC()
                    && !fm.getElement().isTypePC()) {
                fm.resolve(MODE_MAPPING);
            }
        }
        _discrim.resolve(MODE_MAPPING);
        _version.resolve(MODE_MAPPING);
    }

    @Override
    protected void initializeMapping() {
        super.initializeMapping();

        FieldMapping[] fields = getDefinedFieldMappings();
        for (FieldMapping field : fields) {
            field.resolve(MODE_MAPPING_INIT);
        }
        _discrim.resolve(MODE_MAPPING_INIT);
        _version.resolve(MODE_MAPPING_INIT);
        _strategy.initialize();
    }

    @Override
    protected void clearDefinedFieldCache() {
        // just make this method available to other classes in this package
        super.clearDefinedFieldCache();
    }

    @Override
    protected void clearSubclassCache() {
        super.clearSubclassCache();
        _joinSubMaps = null;
        _assignMaps = null;
    }

    @Override
    public void copy(ClassMetaData cls) {
        super.copy(cls);
        if (_subclassMode == Integer.MAX_VALUE)
            _subclassMode = ((ClassMapping) cls).getSubclassFetchMode();
    }

    @Override
    protected boolean validateDataStoreExtensionPrefix(String prefix) {
        return "jdbc-".equals(prefix);
    }

    ////////////////////////////////
    // ClassStrategy implementation
    ////////////////////////////////

    @Override
    public String getAlias() {
        return assertStrategy().getAlias();
    }

    @Override
    public void map(boolean adapt) {
        assertStrategy().map(adapt);
    }

    @Override
    public void initialize() {
        assertStrategy().initialize();
    }

    @Override
    public void insert(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        assertStrategy().insert(sm, store, rm);
    }

    @Override
    public void update(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        assertStrategy().update(sm, store, rm);
    }

    @Override
    public void delete(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        assertStrategy().delete(sm, store, rm);
    }

    @Override
    public Boolean isCustomInsert(OpenJPAStateManager sm, JDBCStore store) {
        return assertStrategy().isCustomInsert(sm, store);
    }

    @Override
    public Boolean isCustomUpdate(OpenJPAStateManager sm, JDBCStore store) {
        return assertStrategy().isCustomUpdate(sm, store);
    }

    @Override
    public Boolean isCustomDelete(OpenJPAStateManager sm, JDBCStore store) {
        return assertStrategy().isCustomDelete(sm, store);
    }

    @Override
    public void customInsert(OpenJPAStateManager sm, JDBCStore store)
        throws SQLException {
        assertStrategy().customInsert(sm, store);
    }

    @Override
    public void customUpdate(OpenJPAStateManager sm, JDBCStore store)
        throws SQLException {
        assertStrategy().customUpdate(sm, store);
    }

    @Override
    public void customDelete(OpenJPAStateManager sm, JDBCStore store)
        throws SQLException {
        assertStrategy().customDelete(sm, store);
    }

    @Override
    public void setClassMapping(ClassMapping owner) {
        assertStrategy().setClassMapping(owner);
    }

    @Override
    public boolean isPrimaryKeyObjectId(boolean hasAll) {
        return assertStrategy().isPrimaryKeyObjectId(hasAll);
    }

    @Override
    public Joins joinSuperclass(Joins joins, boolean toThis) {
        return assertStrategy().joinSuperclass(joins, toThis);
    }

    @Override
    public boolean supportsEagerSelect(Select sel, OpenJPAStateManager sm,
        JDBCStore store, ClassMapping base, JDBCFetchConfiguration fetch) {
        return assertStrategy().supportsEagerSelect(sel, sm, store, base,
            fetch);
    }

    @Override
    public ResultObjectProvider customLoad(JDBCStore store, boolean subclasses,
        JDBCFetchConfiguration fetch, long startIdx, long endIdx)
        throws SQLException {
        return assertStrategy().customLoad(store, subclasses, fetch,
            startIdx, endIdx);
    }

    @Override
    public boolean customLoad(OpenJPAStateManager sm, JDBCStore store,
        PCState state, JDBCFetchConfiguration fetch)
        throws SQLException, ClassNotFoundException {
        return assertStrategy().customLoad(sm, store, state, fetch);
    }

    @Override
    public boolean customLoad(OpenJPAStateManager sm, JDBCStore store,
        JDBCFetchConfiguration fetch, Result result)
        throws SQLException {
        return assertStrategy().customLoad(sm, store, fetch, result);
    }

    private ClassStrategy assertStrategy() {
        if (_strategy == null)
            throw new InternalException();
        return _strategy;
    }

    /**
     * Find the field mappings that correspond to the given columns.
     *
     * @return null if no columns are given or no field mapping uses the given
     * columns.
     */
    private List<FieldMapping> getFieldMappings(Column[] cols, boolean prime) {
    	if (cols == null || cols.length == 0)
    		return null;
    	List<FieldMapping> result = null;
    	for (Column c : cols) {
    		List<FieldMapping> fms = hasColumn(c, prime);
    		if (fms == null) continue;
			if (result == null)
				result = new ArrayList<>();
			for (FieldMapping fm : fms)
				if (!result.contains(fm))
					result.add(fm);
    	}
    	return result;
    }

    /**
     * Looks up in reverse to find the list of field mappings that include the
     * given column. Costly.
     *
     * @return null if no field mappings carry this column.
     */
    private List<FieldMapping> hasColumn(Column c, boolean prime) {
    	List<FieldMapping> result = null;
    	FieldMapping[] fms = (prime) ?
    		getPrimaryKeyFieldMappings() : getFieldMappings();
    	for (FieldMapping fm : fms) {
    		Column[] cols = fm.getColumns();
    		if (contains(cols, c)) {
    			if (result == null)
    				result = new ArrayList<>();
    			result.add(fm);
    		}
    	}
    	return result;
    }

    boolean contains(Column[] cols, Column c) {
    	for (Column col : cols)
    		if (col == c)
    			return true;
    	return false;
    }

    /**
     * Gets the field values of the given instance for the given columns.
     * The given columns are used to identify the fields by a reverse lookup.
     *
     * @return a single object or an array of objects based on number of
     * fields the given columns represent.
     */
    private Object getValueFromUnmanagedInstance(Object obj, Column[] cols,
    		boolean prime) {
    	List<FieldMapping> fms = getFieldMappings(cols, prime);
    	if (fms == null)
    		return null;
    	if (fms.size() == 1)
    		return Reflection.getValue(obj, fms.get(0).getName(), true);
    	Object[] result = new Object[fms.size()];
    	int i = 0;
    	for (FieldMapping fm : fms) {
    		result[i++] = Reflection.getValue(obj, fm.getName(), true);
    	}
    	return result;
    }

    public boolean isVerticalStrategy() {
        String strat = getMappingInfo().getHierarchyStrategy();
        if (strat != null && strat.equals(VerticalClassStrategy.ALIAS))
            return true;
        return false;
    }
}
