/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     11/13/2009-2.0  mobrien - 294765: MapKey keyType DirectToField processing
//       should return attributeClassification class in getMapKeyTargetType when
//       accessor.attributeField is null in the absence of a MapKey annotation
//     11/10/2011-2.4 Guy Pelletier
//       - 357474: Address primaryKey option from tenant discriminator column
//     30/05/2012-2.4 Guy Pelletier
//       - 354678: Temp classloader is still being used during metadata processing
//     06/03/2013-2.5.1 Guy Pelletier
//       - 402380: 3 jpa21/advanced tests failed on server with
//         "java.lang.NoClassDefFoundError: org/eclipse/persistence/testing/models/jpa21/advanced/enums/Gender"
package org.eclipse.persistence.mappings.foundation;

import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.*;

import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.descriptors.DescriptorIterator;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedClassForName;
import org.eclipse.persistence.internal.security.PrivilegedNewInstanceFromClass;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.converters.Converter;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.sessions.remote.DistributedSession;

/**
 * <b>Purpose</b>: Maps an attribute or some other property to the corresponding
 * database field type. The list of field types that are supported by
 * EclipseLink's direct to field mapping is dependent on the relational database
 * being used.
 *
 * @see org.eclipse.persistence.mappings.foundation.AbstractDirectMapping
 * @see org.eclipse.persistence.mappings.MultitenantPrimaryKeyMapping
 *
 * @author Guy Pelletier
 * @since TopLink/Java 1.0
 */
public abstract class AbstractColumnMapping extends DatabaseMapping {

    /** DatabaseField which this mapping represents. */
    protected DatabaseField field;

    /** Allows user defined conversion between the object attribute value and the database value. */
    protected Converter converter;
    protected String converterClassName;

    /** Flag to support insertable JPA setting */
    protected boolean isInsertable = true;

    /** Flag to support updatable JPA setting */
    protected boolean isUpdatable = true;

    /**
     * Default constructor.
     */
    public AbstractColumnMapping() {
        super();
        this.setWeight(WEIGHT_DIRECT);
    }

    /**
     * INTERNAL:
     * Cascade perform delete through mappings that require the cascade.
     */
    public void cascadePerformRemoveIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
        // objects referenced by this mapping are not registered as they have
        // no identity, this is a no-op.
    }

    /**
     * INTERNAL:
     * Cascade registerNew for Create through mappings that require the cascade.
     */
    public void cascadeRegisterNewIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
        // objects referenced by this mapping are not registered as they have
        // no identity, this is a no-op.
    }

    /**
     * INTERNAL:
     * The mapping clones itself to create deep copy.
     */
    @Override
    public Object clone() {
        AbstractColumnMapping clone = (AbstractColumnMapping)super.clone();

        // Field must be cloned so aggregates do not share fields.
        clone.setField(getField().clone());

        return clone;
    }

    /**
     * Returns the field this mapping represents.
     */
    @Override
    protected Vector<DatabaseField> collectFields() {
        Vector databaseField = new Vector(1);

        databaseField.addElement(field);
        return databaseField;
    }

    /**
     * INTERNAL:
     * Convert all the class-name-based settings in this mapping to actual class-based settings
     * This method is implemented by subclasses as necessary.
     */
    @Override
    public void convertClassNamesToClasses(ClassLoader classLoader){
        super.convertClassNamesToClasses(classLoader);

        // Field may have a type name that needs to be initialize.
        if (field != null) {
            field.convertClassNamesToClasses(classLoader);
        }

        // Convert and any Converter class names.
        convertConverterClassNamesToClasses(converter, classLoader);

        // Instantiate any custom converter class
        if (converterClassName != null) {
            Class converterClass;
            Converter converter;

            try {
                if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                    try {
                        converterClass = AccessController.doPrivileged(new PrivilegedClassForName(converterClassName, true, classLoader));
                    } catch (PrivilegedActionException exception) {
                        throw ValidationException.classNotFoundWhileConvertingClassNames(converterClassName, exception.getException());
                    }

                    try {
                        converter = (Converter)AccessController.doPrivileged(new PrivilegedNewInstanceFromClass(converterClass));
                    } catch (PrivilegedActionException exception) {
                        throw ValidationException.classNotFoundWhileConvertingClassNames(converterClassName, exception.getException());
                    }
                } else {
                    converterClass = PrivilegedAccessHelper.getClassForName(converterClassName, true, classLoader);
                    converter = (Converter)PrivilegedAccessHelper.newInstanceFromClass(converterClass);
                }
            } catch (ClassNotFoundException exc) {
                throw ValidationException.classNotFoundWhileConvertingClassNames(converterClassName, exc);
            } catch (Exception e) {
                // Catches IllegalAccessException and InstantiationException
                throw ValidationException.classNotFoundWhileConvertingClassNames(converterClassName, e);
            }

            setConverter(converter);
        }
    }

    /**
     * INTERNAL:
     * An object has been serialized from the server to the client.
     * Replace the transient attributes of the remote value holders
     * with client-side objects.
     */
    @Override
    public void fixObjectReferences(Object object, Map objectDescriptors, Map processedObjects, ObjectLevelReadQuery query, DistributedSession session) {
    }

    /**
     * PUBLIC:
     * Return the converter on the mapping.
     * A converter can be used to convert between the object's value and database value of the attribute.
     */
    public Converter getConverter() {
        return converter;
    }

    /**
     * INTERNAL:
     * Returns the field which this mapping represents.
     */
    public DatabaseField getField() {
        return field;
    }

    /**
     * INTERNAL:
     * Convert the object (attribute or property) value to a field value.
     */
    public abstract Object getFieldValue(Object objectValue, AbstractSession session);

    /**
     * INTERNAL:
     * Allows for subclasses to convert the the attribute or property value.
     */
    public abstract Object getObjectValue(Object fieldValue, Session session);

    /**
     * Indicates if the mapping has a converter set on it.
     *
     * @return true if there is a converter set on the mapping,
     * false otherwise.
     */
    public boolean hasConverter() {
        return converter != null;
    }

    /**
     * INTERNAL:
     */
    @Override
    public boolean isAbstractColumnMapping() {
        return true;
    }

    /**
     * INTERNAL:
     * Return true if this mapping is insertable.
     */
    protected boolean isInsertable() {
        return isInsertable;
    }

    /**
     * INTERNAL:
     * Return true if this mapping is updatable.
     */
    protected boolean isUpdatable() {
        return isUpdatable;
    }

    /**
     * INTERNAL:
     * Iterate on the appropriate attribute.
     */
    @Override
    public void iterate(DescriptorIterator iterator) {
        // PERF: Only iterate when required.
        if (iterator.shouldIterateOnPrimitives()) {
            iterator.iteratePrimitiveForMapping(getAttributeValueFromObject(iterator.getVisitedParent()), this);
        }
    }

    /**
     * PUBLIC:
     * Set the converter on the mapping.
     * A converter can be used to convert between the object's value and database value of the attribute.
     */
    public void setConverter(Converter converter) {
        this.converter = converter;
    }

    /**
     * PUBLIC:
     * Set the converter class name on the mapping. It will be instantiated
     * during the convertClassNamesToClasses.
     * A converter can be used to convert between the object's value and
     * database value of the attribute.
     */
    public void setConverterClassName(String converterClassName) {
        this.converterClassName = converterClassName;
    }

    /**
     * ADVANCED:
     * Set the field in the mapping.
     * This can be used for advanced field types, such as XML nodes, or to set the field type.
     */
    public void setField(DatabaseField theField) {
        field = theField;
    }

    /**
     * INTERNAL:
     */
    @Override
    public String toString() {
        return getClass().getName() + "[" + getAttributeName() + "-->" + getField() + "]";
    }

    /**
     * INTERNAL:
     */
    protected abstract void writeValueIntoRow(AbstractRecord row, DatabaseField field, Object value);
}
