/*
 * 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/10/2011-2.4 Guy Pelletier
//       - 357474: Address primaryKey option from tenant discriminator column
package org.eclipse.persistence.mappings.xdb;

import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.platform.database.XMLTypePlaceholder;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.DirectToFieldMapping;
import org.eclipse.persistence.platform.xml.XMLComparer;
import org.eclipse.persistence.platform.xml.XMLParser;
import org.eclipse.persistence.platform.xml.XMLPlatformFactory;
import org.eclipse.persistence.platform.xml.XMLTransformer;
import org.eclipse.persistence.sessions.Session;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * <b>Purpose</b>: Mapping used to map from a DOM (org.w3c.Document) or XML String into
 * an Oracle XMLType field, in Oracle 9i XDB.
 *
 * @since Toplink 10.1.3
 */
public class DirectToXMLTypeMapping extends DirectToFieldMapping {
    /**
     * Indicates if we should initialize the whole DOM on a read.
     * This is only used if the user is mapping from an Oracle Document implementation.
     */
    protected boolean shouldReadWholeDocument = false;

    /**
     * Used to convert the DOM to a String.
     */
    private XMLTransformer xmlTransformer;

    /**
     * Used to determine if the XML document has been modified.
     */
    private XMLComparer xmlComparer;

    /**
     * Used to convert the String to a DOM
     */
    private  XMLParser xmlParser;

    /**
     * INTERNAL:
     * Default to mutable if mapped as a DOM.
     */
    @Override
    public void preInitialize(AbstractSession session) throws DescriptorException {
        if (this.attributeClassification == null) {
            this.attributeClassification = getAttributeAccessor().getAttributeClass();
        }
        if ((this.isMutable == null) && (this.attributeClassification != ClassConstants.STRING)) {
            setIsMutable(true);
        }
        super.preInitialize(session);
    }

    /**
     * INTERNAL:
     * The mapping is initialized with the given session. This mapping is fully initialized
     * after this.
     */
    @Override
    public void initialize(AbstractSession session) throws DescriptorException {
        super.initialize(session);
        setFieldClassification(XMLTypePlaceholder.class);
    }

    public DirectToXMLTypeMapping() {
        super();
        this.xmlTransformer = XMLPlatformFactory.getInstance().getXMLPlatform().newXMLTransformer();
        this.xmlTransformer.setFormattedOutput(false);
        this.xmlParser = XMLPlatformFactory.getInstance().getXMLPlatform().newXMLParser();
        this.xmlComparer = new XMLComparer();
    }

    /**
     * PUBLIC:
     * @param readWholeDocument - determines if the Oracle XDB DOM should be fully initialized
     * on a read.
     */
    public void setShouldReadWholeDocument(boolean readWholeDocument) {
        this.shouldReadWholeDocument = readWholeDocument;
    }

    /**
     * PUBLIC:
     * @return boolean - returns true if currently initializing DOMs on reads.
     */
    public boolean shouldReadWholeDocument() {
        return shouldReadWholeDocument;
    }

    /**
     * INTERNAL:
     * Get the attribute value for the given field value. If we're mapping to a
     * Document, we need to check if we should return the Oracle DOM or build a
     * new one.
     */
    @Override
    public Object getObjectValue(Object fieldValue, Session session) throws ConversionException {
        Object attributeValue = fieldValue;
        try {
            if (attributeValue != null) {
                if (this.attributeClassification != ClassConstants.STRING) {
                    String xml = (String)attributeValue;
                    java.io.StringReader reader = new java.io.StringReader(xml);
                    return this.xmlParser.parse(reader);
                }
            }
        } catch (Exception ex) {
            throw ConversionException.couldNotBeConverted(fieldValue, this.attributeClassification, ex);
        }

        if ((attributeValue == null) && (this.nullValue != null)) {// Translate default null value
            return this.nullValue;
        }

        // Allow for user defined conversion to the object value.
        if (this.converter != null) {
            attributeValue = this.converter.convertDataValueToObjectValue(attributeValue, session);
        }
        return attributeValue;
    }

    @Override
    public boolean isDirectToXMLTypeMapping() {
        return true;
    }

    /**
     * INTERNAL:
     * Clone the DOM Document if required.
     */
    @Override
    protected Object buildCloneValue(Object attributeValue, AbstractSession session) {
        Object newAttributeValue = attributeValue;
        if (isMutable() && attributeValue != null) {
            if ((getAttributeClassification() == ClassConstants.DOCUMENT) || (getAttributeClassification() == ClassConstants.NODE)) {
                Document doc = (Document)attributeValue;
                newAttributeValue = doc.cloneNode(true);
            }
        }
        return newAttributeValue;
    }

    /**
     * INTERNAL:
     * Compare the attribute values.
     * Compare Nodes if mapped as a DOM.
     */
    @Override
    protected boolean compareObjectValues(Object firstValue, Object secondValue, AbstractSession session) {
        // PERF: Check identity before conversion.
        if (firstValue == secondValue) {
            return true;
        }
        if ((firstValue == null) || (secondValue == null)) {
            return false;
        }
        if (getAttributeClassification() == ClassConstants.STRING) {
            return firstValue.equals(secondValue);
        } else {
            Object one = getFieldValue(firstValue, session);
            Object two = getFieldValue(secondValue, session);
            if (one instanceof Node && two instanceof Node) {
                return this.xmlComparer.isNodeEqual((Node)one, (Node)two);
            }
            return one.equals(two);
        }
    }
}
