/*
 * 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.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.xml.namespace.QName;

import org.eclipse.persistence.descriptors.ClassDescriptor;
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.helper.NonSynchronizedVector;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
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.CollectionReferenceMapping;
import org.eclipse.persistence.internal.oxm.mappings.XMLContainerMapping;
import org.eclipse.persistence.internal.oxm.record.UnmarshalRecord;
import org.eclipse.persistence.internal.queries.CollectionContainerPolicy;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
import org.eclipse.persistence.internal.queries.JoinedAttributeManager;
import org.eclipse.persistence.internal.queries.ListContainerPolicy;
import org.eclipse.persistence.internal.queries.MapContainerPolicy;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.AttributeAccessor;
import org.eclipse.persistence.mappings.ContainerMapping;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.oxm.mappings.nullpolicy.AbstractNullPolicy;
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-M 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.
 *
 * 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.
 *
 * The usesSingleNode flag should be set to true if the keys are to be written out in space-separated
 * lists.
 *
 * @see XMLObjectReferenceMapping
 * @see ContainerMapping
 */
public class XMLCollectionReferenceMapping extends XMLObjectReferenceMapping implements CollectionReferenceMapping<AbstractSession, AttributeAccessor, ContainerPolicy, ClassDescriptor, DatabaseField, UnmarshalRecord, XMLField, XMLRecord>, ContainerMapping {
    protected ContainerPolicy containerPolicy; // type of container used to hold the aggregate objects
    private static final String SPACE = " ";
    private DatabaseField field;
    private boolean defaultEmptyContainer = XMLContainerMapping.EMPTY_CONTAINER_DEFAULT;
    private boolean usesSingleNode;
    private boolean reuseContainer;
    private AbstractNullPolicy wrapperNullPolicy;

    /**
     * PUBLIC:
     * The default constructor initializes the sourceToTargetKeyFieldAssociations
     * and sourceToTargetKeys data structures.
     */
    public XMLCollectionReferenceMapping() {
        sourceToTargetKeyFieldAssociations = new HashMap();
        sourceToTargetKeys = new NonSynchronizedVector();
        this.containerPolicy = ContainerPolicy.buildDefaultPolicy();
        this.usesSingleNode = false;
    }

    public DatabaseField getField() {
        return field;
    }

    public void setField(DatabaseField field) {
        this.field = field;
    }


    /**
     * Get the XPath String
     * @return String the XPath String associated with this Mapping
     */
    public String getXPath() {
        return getField().getName();
    }

    /**
     * Set the Mapping field name attribute to the given XPath String
     * @param xpathString String
     */
    public void setXPath(String xpathString) {
        this.setField(new XMLField(xpathString));
    }

    /**
     * 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 - the reference class instance that holds the required pk value
     * @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) {
            return null;
        }
        ClassDescriptor descriptor = referenceDescriptor;
        if(null == descriptor) {
            descriptor = session.getClassDescriptor(targetObject);
        }
        ObjectBuilder objectBuilder = descriptor.getObjectBuilder();
        Object primaryKey = objectBuilder.extractPrimaryKeyFromObject(targetObject, session);
        XMLField tgtXMLField = (XMLField) getSourceToTargetKeyFieldAssociations().get(xmlFld);
        int idx = 0;
        if(!(null == referenceClass || ClassConstants.OBJECT == referenceClass)) {
            idx = descriptor.getPrimaryKeyFields().indexOf(tgtXMLField);
            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.
     */
    public void buildReference(UnmarshalRecord record, XMLField xmlField, Object object, AbstractSession session, Object container) {
         buildReference(record.getCurrentObject(), xmlField, object, session, container, record.getReferenceResolver());
    }

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

        Reference reference = resolver.getReference(this, srcObject, xmlField);
        if (reference == null) {
            // if reference is null, create a new instance and set it on the resolver
            reference = new Reference(this, srcObject, referenceClass, new HashMap(), container);
            resolver.addReference(reference);
        }
        CacheId primaryKeys;
        if(null == referenceClass || ClassConstants.OBJECT == referenceClass) {
            HashMap primaryKeyMap = reference.getPrimaryKeyMap();
            CacheId pks = (CacheId) primaryKeyMap.get(null);
            if (pks == null){
                Object[] pkValues = new Object[1];
                pks = new CacheId(pkValues);
                primaryKeyMap.put(null, pks);
            }
            if(usesSingleNode) {
                for (StringTokenizer stok = new StringTokenizer((String) object); stok.hasMoreTokens();) {
                    pks.add(stok.nextToken());
                    reference = resolver.getReference(this, srcObject, xmlField);
                    if (reference == null) {
                        // if reference is null, create a new instance and set it on the resolver
                        reference = new Reference(this, srcObject, referenceClass, new HashMap(), container);
                        resolver.addReference(reference);
                    }
                    primaryKeyMap = reference.getPrimaryKeyMap();
                    pks = (CacheId) primaryKeyMap.get(null);
                    if (pks == null){
                        Object[] pkValues = new Object[1];
                        pks = new CacheId(pkValues);
                        primaryKeyMap.put(null, pks);
                    }
                }
            } else {
                pks.add(object);
            }
        } else {
            XMLField tgtFld = (XMLField) getSourceToTargetKeyFieldAssociations().get(xmlField);
            String tgtXPath = tgtFld.getQualifiedName();
            HashMap primaryKeyMap = reference.getPrimaryKeyMap();
            CacheId pks = (CacheId) primaryKeyMap.get(tgtXPath);
            ClassDescriptor descriptor = session.getClassDescriptor(referenceClass);
            if (pks == null){
                pks = new CacheId(new Object[0]);
                primaryKeyMap.put(tgtXPath, pks);
            }
            Class type = descriptor.getTypedField(tgtFld).getType();
            XMLConversionManager xmlConversionManager = (XMLConversionManager) session.getDatasourcePlatform().getConversionManager();
            if(usesSingleNode) {
                for (StringTokenizer stok = new StringTokenizer((String) object); stok.hasMoreTokens();) {
                    Object value = xmlConversionManager.convertObject(stok.nextToken(), type);
                    if (value != null) {
                        pks.add(value);
                    }
                    reference = resolver.getReference(this, srcObject, xmlField);
                    if (reference == null) {
                        // if reference is null, create a new instance and set it on the resolver
                        reference = new Reference(this, srcObject, referenceClass, new HashMap(), container);
                        resolver.addReference(reference);
                    }
                    primaryKeyMap = reference.getPrimaryKeyMap();
                    pks = (CacheId) primaryKeyMap.get(null);
                    if (pks == null){
                        pks = new CacheId(new Object[0]);
                        primaryKeyMap.put(tgtXPath, pks);
                    }
                }
            } else {
                Object value = xmlConversionManager.convertObject(object, type);
                if (value != null) {
                    pks.add(value);
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Return the mapping's containerPolicy.
     */
    public ContainerPolicy getContainerPolicy() {
        return containerPolicy;
    }

    /**
     * INTERNAL:
     * The mapping is initialized with the given session. This mapping is fully initialized
     * after this.
     */
    public void initialize(AbstractSession session) throws DescriptorException {
        super.initialize(session);
        if(null != getField()) {
            setField(getDescriptor().buildField(getField()));
        }
        ContainerPolicy cp = getContainerPolicy();
        if (cp != null) {
            if (cp.getContainerClass() == null) {
                Class cls = session.getDatasourcePlatform().getConversionManager().convertClassNameToClass(cp.getContainerClassName());
                cp.setContainerClass(cls);
            }
        }

    }

    /**
     * INTERNAL:
     * Extract the primary key values from the row, then create an
     * org.eclipse.persistence.internal.oxm.Reference instance and stored 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 {
          ContainerPolicy cp = getContainerPolicy();

          Object container = null;

          if (reuseContainer) {
              Object currentObject = ((XMLRecord) databaseRow).getCurrentObject();
              container = getAttributeAccessor().getAttributeValueFromObject(currentObject);

          }
          if(container == null){
              container = cp.containerInstance();
          }
          return readFromRowIntoObject(databaseRow, joinManager, targetObject, parentCacheKey, sourceQuery, executionSession, isTargetProtected, container);

    }

    /**
     * INTERNAL:
     * Extract the primary key values from the row, then create an
     * org.eclipse.persistence.internal.oxm.Reference instance and stored 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, Object container) throws DatabaseException {
        ClassDescriptor descriptor = sourceQuery.getSession().getClassDescriptor(getReferenceClass());

        if(container == null){
            readFromRowIntoObject(databaseRow, joinManager, targetObject, parentCacheKey, sourceQuery, executionSession, isTargetProtected);
        }

        // for each source xmlField, get the value from the row and store
        for (Iterator fieldIt = getFields().iterator(); fieldIt.hasNext();) {
            XMLField fld = (XMLField) fieldIt.next();
            //
            Object fieldValue = databaseRow.getValues(fld);
            if ((fieldValue == null) || (fieldValue instanceof String) || !(fieldValue instanceof Vector)) {
                return container;
            }
            // 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.
            for (Iterator valIt = ((Vector) fieldValue).iterator(); valIt.hasNext();) {
                Object nextValue = valIt.next();
                DOMRecord domRecord = (DOMRecord) databaseRow;
                this.buildReference(domRecord.getCurrentObject(), fld, nextValue, sourceQuery.getSession(), container, domRecord.getReferenceResolver());
            }
        }
        return null;
    }


    /**
     * ADVANCED:
     * Set the mapping's containerPolicy.
     */
    public void setContainerPolicy(ContainerPolicy containerPolicy) {
        // set reference class here if necessary
        this.containerPolicy = containerPolicy;
        if (this.containerPolicy instanceof MapContainerPolicy) {
            ((MapContainerPolicy) this.containerPolicy).setElementClass(getReferenceClass());
        }
    }

    /**
     * PUBLIC:
     * Configure the mapping to use an instance of the specified container class
     * to hold the target objects.
     * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Collection interface.
     * <p>jdk1.1.x: The container class must be a subclass of Vector.
     */
    public void useCollectionClass(Class concreteContainerClass) {
        this.setContainerPolicy(ContainerPolicy.buildPolicyFor(concreteContainerClass));
    }

    public void useCollectionClassName(String concreteContainerClassName) {
        this.setContainerPolicy(new CollectionContainerPolicy(concreteContainerClassName));
    }

    public void useListClassName(String concreteContainerClassName) {
        this.setContainerPolicy(new ListContainerPolicy(concreteContainerClassName));
    }

    /**
     * PUBLIC:
     * Configure the mapping to use an instance of the specified container class
     * to hold the target objects. The key used to index the value in the Map
     * is the value returned by a call to the specified zero-argument method.
     * The method must be implemented by the class (or a superclass) of the
     * value to be inserted into the Map.
     * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Map interface.
     * <p>jdk1.1.x: The container class must be a subclass of Hashtable.
     * <p>The referenceClass must be set before calling this method.
     */
    public void useMapClass(Class concreteContainerClass, String methodName) {
        // the reference class has to be specified before coming here
        if (this.getReferenceClass() == null) {
            throw DescriptorException.referenceClassNotSpecified(this);
        }
        ContainerPolicy policy = ContainerPolicy.buildPolicyFor(concreteContainerClass);
        policy.setKeyName(methodName, getReferenceClass().getName());
        this.setContainerPolicy(policy);
    }

    /**
     * PUBLIC:
     * Configure the mapping to use an instance of the specified container class
     * to hold the target objects. The key used to index the value in the Map
     * is the value returned by a call to the specified zero-argument method.
     * The method must be implemented by the class (or a superclass) of the
     * value to be inserted into the Map.
     * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Map interface.
     * <p>jdk1.1.x: The container class must be a subclass of Hashtable.
     * <p>The referenceClass must be set before calling this method.
     */
    public void useMapClassName(String concreteContainerClass, String methodName) {
        // the reference class has to be specified before coming here
        if (this.getReferenceClass() == null) {
            throw DescriptorException.referenceClassNotSpecified(this);
        }
        MapContainerPolicy policy = new MapContainerPolicy(concreteContainerClass);
        policy.setKeyName(methodName, getReferenceClass().getName());
        this.setContainerPolicy(policy);
    }

    /**
     * INTERNAL:
     * For the purpose of XMLCollectionReferenceMappings, 'usesSingleNode'
     * refers to the fact that the source key xpath fields should all be written as
     * space-separated lists. Would be used for mapping to an IDREFS field in a schema
     */
    public boolean usesSingleNode() {
        return this.usesSingleNode;
    }

    public void setUsesSingleNode(boolean useSingleNode) {
        this.usesSingleNode = useSingleNode;
    }

    /**
     * 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
        if(this.isReadOnly()) {
            return;
        }
        for (Iterator fieldIt = getFields().iterator(); fieldIt.hasNext();) {
            XMLField xmlField = (XMLField) fieldIt.next();
            ContainerPolicy cp = getContainerPolicy();
            Object collection = getAttributeAccessor().getAttributeValueFromObject(object);
            if (collection == null) {
                return;
            }

            Object fieldValue;
            Object objectValue;
            StringBuilder stringValueBuilder = new StringBuilder();
            QName schemaType;
            Object iterator = cp.iteratorFor(collection);
            if (usesSingleNode()) {
                while (cp.hasNext(iterator)) {
                    objectValue = cp.next(iterator, session);
                    fieldValue = buildFieldValue(objectValue, xmlField, session);
                    if (fieldValue != null) {
                        schemaType = getSchemaType(xmlField, fieldValue, session);
                        String newValue = getValueToWrite(schemaType, fieldValue, session);
                        if (newValue != null) {
                            stringValueBuilder.append(newValue);
                            if (cp.hasNext(iterator)) {
                                stringValueBuilder.append(SPACE);
                            }
                        }
                    }
                }
                if (stringValueBuilder.length() > 0) {
                    row.put(xmlField, stringValueBuilder.toString());
                }
            } else {
                ArrayList keyValues = new ArrayList();
                while (cp.hasNext(iterator)) {
                    objectValue = cp.next(iterator, session);
                    fieldValue = buildFieldValue(objectValue, xmlField, session);
                    if (fieldValue != null) {
                        schemaType = getSchemaType(xmlField, fieldValue, session);
                        String stringValue = getValueToWrite(schemaType, fieldValue, session);
                        //row.add(xmlField, stringValue);
                        keyValues.add(stringValue);
                    }
                }
                row.put(xmlField, keyValues);
            }
        }
    }

    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.add(xmlField, stringValue);
            }
        }

    }

    public boolean isCollectionMapping() {
        return true;
    }

    /**
     * Return true if the original container on the object should be used if
     * present.  If it is not present then the container policy will be used to
     * create the container.
     */
    public boolean getReuseContainer() {
        return reuseContainer;
    }

    /**
     * Specify whether the original container on the object should be used if
     * present.  If it is not present then the container policy will be used to
     * create the container.
     */
    public void setReuseContainer(boolean reuseContainer) {
        this.reuseContainer = reuseContainer;
    }

    /**
     * INTERNAL
     * Return true if an empty container should be set on the object if there
     * is no presence of the collection in the XML document.
     * @since EclipseLink 2.3.3
     */
    public boolean isDefaultEmptyContainer() {
        return defaultEmptyContainer;
    }

    /**
     * INTERNAL
     * Indicate whether by default an empty container should be set on the
     * field/property if the collection is not present in the XML document.
     * @since EclipseLink 2.3.3
     */
    public void setDefaultEmptyContainer(boolean defaultEmptyContainer) {
        this.defaultEmptyContainer = defaultEmptyContainer;
    }

    public AbstractNullPolicy getWrapperNullPolicy() {
        return this.wrapperNullPolicy;
    }

    public void setWrapperNullPolicy(AbstractNullPolicy policy) {
        this.wrapperNullPolicy = policy;
    }

}
