/*
 * 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
package org.eclipse.persistence.oxm.mappings;

import java.util.*;

import javax.xml.namespace.QName;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.Reference;
import org.eclipse.persistence.internal.oxm.ReferenceResolver;
import org.eclipse.persistence.internal.oxm.XMLConversionManager;
import org.eclipse.persistence.internal.oxm.mappings.ObjectReferenceMapping;
import org.eclipse.persistence.internal.oxm.record.UnmarshalRecord;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
import org.eclipse.persistence.internal.queries.JoinedAttributeManager;
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.AggregateMapping;
import org.eclipse.persistence.mappings.AttributeAccessor;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.oxm.XMLUnionField;
import org.eclipse.persistence.oxm.record.DOMRecord;
import org.eclipse.persistence.oxm.record.XMLRecord;
import org.eclipse.persistence.queries.ObjectBuildingQuery;

/**
 * TopLink OXM version of a 1-1 mapping.  A list of source-target key field
 * associations is used to link the source xpaths to their related target xpaths,
 * and hence their primary key (unique identifier) values used when (un)marshalling.
 * This mapping has a Vector of XMLFields as opposed to a single XMLField.
 *
 * It is important to note that each target xpath is assumed to be set as a primary
 * key field on the target (reference) class descriptor - this is necessary in order
 * to locate the correct target object instance in the session cache when resolving
 * mapping references.
 */
public class XMLObjectReferenceMapping extends AggregateMapping implements ObjectReferenceMapping<AbstractSession, AttributeAccessor, ContainerPolicy, ClassDescriptor, DatabaseField, UnmarshalRecord, XMLField, XMLRecord>, XMLMapping {
    protected HashMap sourceToTargetKeyFieldAssociations;
    protected Vector sourceToTargetKeys; // maintain the order of the keys
    private boolean isWriteOnly;
    private XMLInverseReferenceMapping inverseReferenceMapping;

    /**
     * PUBLIC:
     * The default constructor initializes the sourceToTargetKeyFieldAssociations
     * and sourceToTargetKeys data structures.
     */
    public XMLObjectReferenceMapping() {
        sourceToTargetKeyFieldAssociations = new HashMap();
        sourceToTargetKeys = new Vector();
    }

    /**
     * PUBLIC:
     * Add a source-target xpath pair to the map.
     *
     * @param srcXPath
     * @param tgtXPath
     */
    public void addSourceToTargetKeyFieldAssociation(String srcXPath, String tgtXPath) {
        XMLField srcFld = new XMLField(srcXPath);
        sourceToTargetKeys.add(srcFld);
        if(null == tgtXPath) {
            sourceToTargetKeyFieldAssociations.put(srcFld, null);
        } else {
            sourceToTargetKeyFieldAssociations.put(srcFld, new XMLField(tgtXPath));
        }
    }

    public void addSourceToTargetKeyFieldAssociation(XMLField srcField, XMLField tgtField) {
        sourceToTargetKeys.add(srcField);
        sourceToTargetKeyFieldAssociations.put(srcField, tgtField);
    }

    /**
     * INTERNAL:
     * Retrieve the target object's primary key value that is mapped to a given
     * source xpath (in the source-target key field association list).
     *
     * @param targetObject
     * @param xmlFld
     * @param session
     * @return null if the target object is null, the reference class is null, or
     * a primary key field name does not exist on the reference descriptor that
     * matches the target field name - otherwise, return the associated primary
     * key value
     */
    public Object buildFieldValue(Object targetObject, XMLField xmlFld, AbstractSession session) {
        if (targetObject == null || getReferenceClass() == null) {
            return null;
        }
        ClassDescriptor descriptor = referenceDescriptor;
        if(null == descriptor) {
            descriptor = session.getClassDescriptor(targetObject);
        }
        ObjectBuilder objectBuilder = descriptor.getObjectBuilder();
        Object primaryKey = objectBuilder.extractPrimaryKeyFromObject(targetObject, session);
        int idx = 0;
        if(!(null == referenceClass || ClassConstants.OBJECT == getReferenceClass())) {
            idx = descriptor.getPrimaryKeyFields().indexOf(getSourceToTargetKeyFieldAssociations().get(xmlFld));
            if (idx == -1) {
                return null;
            }
        }
        if (primaryKey instanceof CacheId) {
            return ((CacheId)primaryKey).getPrimaryKey()[idx];
        } else {
            return primaryKey;
        }
    }

    /**
     * INTERNAL:
     * Create (if necessary) and populate a reference object that will be used
     * during the mapping reference resolution phase after unmarshalling is
     * complete.
     *
     * @param record
     * @param xmlField
     * @param object
     * @param session
     */
    public void buildReference(UnmarshalRecord record, XMLField xmlField, Object object, AbstractSession session) {
        ReferenceResolver resolver = record.getReferenceResolver();
        if (resolver == null) {
            return;
        }

        Object srcObject = record.getCurrentObject();
        // the order in which the primary keys are added to the vector is
        // relevant for cache lookup - it must match the ordering of the
        // reference descriptor's primary key entries

        Reference reference = resolver.getReference(this, srcObject);
        CacheId primaryKeys;
        if(null == referenceClass || ClassConstants.OBJECT == referenceClass) {
            if (reference == null) {
                // if reference is null, create a new instance and set it on the resolver
                primaryKeys = new CacheId(new Object[1]);
                reference = new Reference(this, srcObject, referenceClass, primaryKeys);
                resolver.addReference(reference);
                record.reference(reference);
            }  else {
                primaryKeys = (CacheId) reference.getPrimaryKey();
            }
            primaryKeys.set(0, object);
        } else {
            Vector pkFieldNames = referenceDescriptor.getPrimaryKeyFieldNames();
            // if reference is null, create a new instance and set it on the resolver
            if (reference == null) {
                primaryKeys = new CacheId(new Object[pkFieldNames.size()]);
                reference = new Reference(this, srcObject, referenceClass, primaryKeys);
                resolver.addReference(reference);
                record.reference(reference);
            } else {
                primaryKeys = (CacheId) reference.getPrimaryKey();
            }
            XMLField tgtFld = (XMLField) getSourceToTargetKeyFieldAssociations().get(xmlField);
            int idx = pkFieldNames.indexOf(tgtFld.getQualifiedName());
            // fix for bug# 5687430
            // need to get the actual type of the target (i.e. int, String, etc.)
            // and use the converted value when checking the cache.
            Object value = session.getDatasourcePlatform().getConversionManager().convertObject(object, referenceDescriptor.getTypedField(tgtFld).getType());
            if (value != null) {
                primaryKeys.set(idx, value);
            }
        }
    }

    /**
     * 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, however mappings from the referenced object may need cascading.
        Object objectReferenced = getRealAttributeValueFromObject(object, uow);
        if (objectReferenced == null) {
            return;
        }
        if (!visitedObjects.containsKey(objectReferenced)) {
            visitedObjects.put(objectReferenced, objectReferenced);
            ObjectBuilder builder = getReferenceDescriptor(objectReferenced.getClass(), uow).getObjectBuilder();
            builder.cascadePerformRemove(objectReferenced, uow, visitedObjects);
        }
    }

    /**
     * INTERNAL:
     * Cascade registerNew for Create through mappings that require the cascade
     */
    public void cascadeRegisterNewIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
        // Aggregate objects are not registered but their mappings should be.
        Object objectReferenced = getRealAttributeValueFromObject(object, uow);
        if (objectReferenced == null) {
            return;
        }
        if (!visitedObjects.containsKey(objectReferenced)) {
            visitedObjects.put(objectReferenced, objectReferenced);
            ObjectBuilder builder = getReferenceDescriptor(objectReferenced.getClass(), uow).getObjectBuilder();
            builder.cascadeRegisterNewForCreate(objectReferenced, uow, visitedObjects);
        }
    }

    /**
     * INTERNAL:
     * Return a list of XMLFields based on the source XPath values
     * in the source-target key field associations list.
     */
    public Vector getFields() {
        return sourceToTargetKeys;
    }

    /**
     * Return a QName representation the schema type for a given XMLField, if
     * applicable.
     *
     * Note:  This method performs the same functionality as 'getSchemaType' in
     * org.eclipse.persistence.internal.oxm.XMLSimpleMappingNodeValue.
     *
     * @param xmlField
     * @param value
     * @return
     */
    protected QName getSchemaType(XMLField xmlField, Object value, AbstractSession session) {
        QName schemaType = null;
        if (xmlField.isTypedTextField()) {
            ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
            schemaType = xmlField.getXMLType(value.getClass(), conversionManager);
        } else if (xmlField.isUnionField()) {
            return getSingleValueToWriteForUnion((XMLUnionField) xmlField, value, session);
        } else if (xmlField.getSchemaType() != null) {
            schemaType = xmlField.getSchemaType();
        }
        return schemaType;
    }

    /**
     * Return a single QName representation for a given XMLUnionField, if applicable.
     *
     * Note:  This method performs the same functionality as 'getSingleValueToWriteForUnion'
     * in org.eclipse.persistence.internal.oxm.XMLSimpleMappingNodeValue.
     *
     * @param xmlField
     * @param value
     * @return
     */
    protected QName getSingleValueToWriteForUnion(XMLUnionField xmlField, Object value, AbstractSession session) {
        ArrayList schemaTypes = xmlField.getSchemaTypes();
        QName schemaType = null;
        QName nextQName;
        Class javaClass;
        for (int i = 0; i < schemaTypes.size(); i++) {
            nextQName = (QName) (xmlField).getSchemaTypes().get(i);
            try {
                if (nextQName != null) {
                    ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
                    javaClass = xmlField.getJavaClass(nextQName, conversionManager);
                    conversionManager.convertObject(value, javaClass, nextQName);
                    schemaType = nextQName;
                    break;
                }
            } catch (ConversionException ce) {
                if (i == (schemaTypes.size() - 1)) {
                    schemaType = nextQName;
                }
            }
        }
        return schemaType;
    }

    /**
     * INTERNAL:
     * Return a list of source-target xmlfield pairs.
     *
     * @return
     */
    public HashMap getSourceToTargetKeyFieldAssociations() {
        return sourceToTargetKeyFieldAssociations;
    }

    /**
     * Return a string representation of a given value, based on a given schema type.
     *
     * Note:  This method performs the same functionality as 'getValueToWrite'
     * in org.eclipse.persistence.internal.oxm.XMLSimpleMappingNodeValue.
     *
     * @param schemaType
     * @param value
     * @return
     */
    protected String getValueToWrite(QName schemaType, Object value, AbstractSession session) {
        return (String) ((XMLConversionManager) session.getDatasourcePlatform().getConversionManager()).convertObject(value, ClassConstants.STRING, schemaType);
    }

    /**
     * INTERNAL:
     * Register a ReferenceResolver as an event listener on the session,
     * if one doesn't already exist.  Each source/target field will have
     * a namespace resolver set as well.
     *
     * @see org.eclipse.persistence.internal.oxm.ReferenceResolver
     * @see org.eclipse.persistence.oxm.NamespaceResolver
     */
    public void initialize(AbstractSession session) throws DescriptorException {
        if (null == referenceClass) {
            if(referenceClassName != null){
                setReferenceClass(session.getDatasourcePlatform().getConversionManager().convertClassNameToClass(referenceClassName));
            }
        }
        if(!(null == referenceClass || referenceClass == ClassConstants.OBJECT)) {
            super.initialize(session);
        }

        // iterate over each source & target XMLField and set the
        // appropriate namespace resolver
        XMLDescriptor descriptor = (XMLDescriptor) this.getDescriptor();
        XMLDescriptor targetDescriptor = (XMLDescriptor) getReferenceDescriptor();
        for (int index = 0; index < sourceToTargetKeys.size(); index++) {
            XMLField sourceField = (XMLField) sourceToTargetKeys.get(index);
            XMLField targetField = (XMLField) sourceToTargetKeyFieldAssociations.remove(sourceField);
            sourceField = (XMLField) descriptor.buildField(sourceField);
            sourceToTargetKeys.set(index, sourceField);
            if(null != targetField) {
                if(null == targetDescriptor) {
                    throw DescriptorException.referenceClassNotSpecified(this);
                }
                //primary key field from ref desc
                List<DatabaseField > pkFields  = targetDescriptor.getPrimaryKeyFields();
                for(int i=0; i<pkFields.size(); i++){
                    XMLField nextPKField = (XMLField)pkFields.get(i);
                    if(targetField.equals(nextPKField)){
                        targetField = (XMLField) targetDescriptor.buildField(nextPKField);
                        sourceField.setSchemaType(targetField.getSchemaType());
                        break;
                    }
                }

            }
            sourceToTargetKeyFieldAssociations.put(sourceField, targetField);
        }

        if (this.inverseReferenceMapping != null) {
            if (null != this.inverseReferenceMapping.getAttributeAccessor()) {
                this.inverseReferenceMapping.getAttributeAccessor().initializeAttributes(this.referenceClass);
            }
        }
    }

    public void preInitialize(AbstractSession session) throws DescriptorException {
        getAttributeAccessor().setIsWriteOnly(this.isWriteOnly());
        getAttributeAccessor().setIsReadOnly(this.isReadOnly());
        super.preInitialize(session);
    }


    /**
     * INTERNAL:
     * Indicates that this is an XML mapping.
     */
    public boolean isXMLMapping() {
        return true;
    }

    /**
     * INTERNAL:
     * Extract the primary key values from the row, then create an
     * org.eclipse.persistence.internal.oxm.Reference instance and store it
     * on the session's org.eclipse.persistence.internal.oxm.ReferenceResolver.
     */
    public Object readFromRowIntoObject(AbstractRecord databaseRow, JoinedAttributeManager joinManager, Object targetObject, CacheKey parentCacheKey, ObjectBuildingQuery sourceQuery, AbstractSession executionSession, boolean isTargetProtected) throws DatabaseException {
        // the order in which the primary keys are added to the vector is
        // relevant for cache lookup - it must match the ordering of the
        // reference descriptor's primary key entries
        CacheId primaryKeys;
        ClassDescriptor descriptor = sourceQuery.getSession().getClassDescriptor(referenceClass);
        Vector pkFieldNames = null;
        if(null == descriptor) {
            primaryKeys = new CacheId(new Object[1]);
        } else {
            pkFieldNames = descriptor.getPrimaryKeyFieldNames();
            primaryKeys = new CacheId(new Object[pkFieldNames.size()]);
        }
        Iterator keyIt = sourceToTargetKeys.iterator();
        while (keyIt.hasNext()) {
            XMLField keyFld = (XMLField) keyIt.next();
            XMLField tgtFld = (XMLField) getSourceToTargetKeyFieldAssociations().get(keyFld);
            Object value;
            int idx = 0;
            if(null == tgtFld) {
               value = databaseRow.get(keyFld);
            } else {
                idx = pkFieldNames.indexOf(tgtFld.getXPath());
                if (idx == -1) {
                    continue;
                }
                // fix for bug# 5687430
                // need to get the actual type of the target (i.e. int, String, etc.)
                // and use the converted value when checking the cache.
                value = executionSession.getDatasourcePlatform().getConversionManager().convertObject(databaseRow.get(keyFld), descriptor.getTypedField(tgtFld).getType());
            }
            if (value != null) {
                primaryKeys.set(idx, value);
            }
        }
        // store the Reference instance on the resolver for use during mapping
        // resolution phase
        ReferenceResolver resolver = ((DOMRecord) databaseRow).getReferenceResolver();
        if (resolver != null) {
            resolver.addReference(new Reference(this, targetObject, referenceClass, primaryKeys));
        }
        return null;
    }

    /**
     * @param field
     */
    public void setField(DatabaseField field) {
        // do nothing.
    }

    /**
     * INTERNAL:
     * Set the list of source-target xmlfield pairs.
     */
    public void setSourceToTargetKeyFieldAssociations(HashMap sourceToTargetKeyFieldAssociations) {
        this.sourceToTargetKeyFieldAssociations = sourceToTargetKeyFieldAssociations;
    }

    /**
     * INTERNAL:
     * Write the attribute value from the object to the row.
     */
    @Override
    public void writeFromObjectIntoRow(Object object, AbstractRecord row, AbstractSession session, WriteType writeType) {
        // for each xmlField on this mapping
        Object targetObject = getAttributeValueFromObject(object);
        writeSingleValue(targetObject, object, (XMLRecord) row, session);
    }

    public void writeSingleValue(Object value, Object parent, XMLRecord row, AbstractSession session) {
        for (Iterator fieldIt = getFields().iterator(); fieldIt.hasNext();) {
            XMLField xmlField = (XMLField) fieldIt.next();
            Object fieldValue = buildFieldValue(value, xmlField, session);
            if (fieldValue != null) {
                QName schemaType = getSchemaType(xmlField, fieldValue, session);
                String stringValue = getValueToWrite(schemaType, fieldValue, session);
                row.put(xmlField, stringValue);
            }
        }

    }

    public void setIsWriteOnly(boolean b) {
        this.isWriteOnly = b;
    }

    public boolean isWriteOnly() {
        return this.isWriteOnly;
    }

    public void setAttributeValueInObject(Object object, Object value) throws DescriptorException {
        if(isWriteOnly()) {
            return;
        }
        super.setAttributeValueInObject(object, value);
    }

    public XMLInverseReferenceMapping getInverseReferenceMapping() {
        return inverseReferenceMapping;
    }

    void setInverseReferenceMapping(XMLInverseReferenceMapping inverseReferenceMapping) {
        this.inverseReferenceMapping = inverseReferenceMapping;
    }
    @Override
    public boolean isObjectReferenceMapping() {
        return true;
    }

}
