/*
 * 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;

import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.Validator;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.ErrorHandler;

import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.oxm.Root;
import org.eclipse.persistence.internal.oxm.XMLObjectBuilder;
import org.eclipse.persistence.internal.oxm.documentpreservation.XMLBinderPolicy;
import org.eclipse.persistence.internal.oxm.record.DOMReader;
import org.eclipse.persistence.internal.oxm.record.SAXUnmarshaller;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.oxm.documentpreservation.DocumentPreservationPolicy;
import org.eclipse.persistence.oxm.record.DOMRecord;
import org.eclipse.persistence.platform.xml.XMLTransformer;

/**
 * PUBLIC:
 * <p><b>Purpose:</b>Provide a runtime public interface for preserving unmapped content from an
 * XML Document.
 * <p><b>Responsibilities:</b><ul>
 * <li>Unmarshal XML into JavaObjects and maintain the associations between nodes and objects</li>
 * <li>Update the cached XML based on changes to the object</li>
 * <li>Update the cached objects based on changes to the XML Document</li>
 * <li>Provide API to access the cached Node for a given object</li>
 * <li>Provide API to access the cached Object for a given XML Node</li>
 * </ul>
 *
 * <p>The XML Binder is a runtime class that allows an association to be maintained between the
 * original XML Document and the Java Objects built from the Document. It allows unmapped content
 * (such as comments, processing instructions or other unmapped elements and attributes) to be
 * preserved. The XMLBinder is created through an XMLContext.
 *
 *  @see org.eclipse.persistence.oxm.XMLContext
 *  @author mmacivor
 */

public class XMLBinder {

    SAXUnmarshaller saxUnmarshaller;
    XMLContext context;
    XMLMarshaller marshaller;
    XMLUnmarshaller unmarshaller;
    DocumentPreservationPolicy documentPreservationPolicy;
    DOMReader reader;

    public XMLBinder(XMLContext context) {
        this.context = new XMLContext(context.getXMLContextState());
        marshaller = this.context.createMarshaller();
        unmarshaller = this.context.createUnmarshaller();
        initialize();
    }

    public XMLBinder(XMLContext context, XMLMarshaller marshaller, XMLUnmarshaller unmarshaller) {
        this.context = new XMLContext(context.getXMLContextState());
        this.marshaller = marshaller;
        this.unmarshaller = unmarshaller;
        initialize();
    }

    private void initialize() {
        saxUnmarshaller = new SAXUnmarshaller(unmarshaller, null);
        documentPreservationPolicy = new XMLBinderPolicy();
        reader = new DOMReader(unmarshaller);
    }

    /**
     * This method will unmarshal the provided node into mapped java objects. The original node
     * will be cached rather than thrown away.
     * @param node
     * @return The root object unmarshalled from the provided node.
     */
    public Object unmarshal(org.w3c.dom.Node node) {
        validateNode(node);

        reader.setDocPresPolicy(documentPreservationPolicy);
        Object toReturn = saxUnmarshaller.unmarshal(reader, node);
        return toReturn;
    }

    private void validateNode(org.w3c.dom.Node node) {
        if (getSchema() != null) {
            Validator validator = getSchema().newValidator();
            validator.setErrorHandler(getErrorHandler());
            try {
                validator.validate(new DOMSource(node));
            } catch (Exception e) {
                throw XMLMarshalException.validateException(e);
            }
        }
    }

    public XMLRoot unmarshal(org.w3c.dom.Node node, Class javaClass) {
        validateNode(node);
        reader.setDocPresPolicy(documentPreservationPolicy);
        return buildXMLRootFromObject(saxUnmarshaller.unmarshal(reader, node, javaClass));
    }

    /**
     * This method will update the cached XML node for the provided object. If no node exists for this
     * object, then no operation is performed.
     * @param obj
     */
    public void updateXML(Object obj) {
        if(obj instanceof Root) {
            obj = ((Root)obj).getObject();
        }
        Node associatedNode = documentPreservationPolicy.getNodeForObject(obj);
        if(associatedNode == null) {
            return;
        }
        updateXML(obj, associatedNode);
    }

    public void marshal(Object obj, Node node) {
        XMLDescriptor desc = null;
        boolean isXMLRoot = obj instanceof Root;
        if (isXMLRoot) {
            Object o = ((Root) obj).getObject();
            desc = (XMLDescriptor) context.getSession(o).getDescriptor(o);
        } else {
            desc = (XMLDescriptor) context.getSession(obj).getDescriptor(obj);
        }

        DOMRecord domRecord = null;
        if (!isXMLRoot) {
            domRecord = new DOMRecord(desc.getDefaultRootElement(), desc.getNamespaceResolver());
            domRecord.setDocPresPolicy(getDocumentPreservationPolicy());
        }
        Node n = this.marshaller.objectToXML(obj, node, desc, domRecord, isXMLRoot, this.getDocumentPreservationPolicy());

        validateNode(n);

        DOMResult result = new DOMResult(node);
        XMLTransformer transformer = marshaller.getTransformer();
        if (isXMLRoot) {
            String oldEncoding = transformer.getEncoding();
            String oldVersion = transformer.getVersion();
            if (((Root) obj).getEncoding() != null) {
                transformer.setEncoding(((Root) obj).getEncoding());
            }
            if (((Root) obj).getXMLVersion() != null) {
                transformer.setVersion(((Root) obj).getXMLVersion());
            }
            transformer.transform(n, result);
            if(oldEncoding != null){
                transformer.setEncoding(oldEncoding);
            }
            if(oldVersion != null){
                transformer.setVersion(oldVersion);
            }
        } else {
            transformer.transform(n, result);

        }
    }

    public void updateXML(Object obj, Node associatedNode) {
        if (obj instanceof Root) {
            obj = ((Root)obj).getObject();
        }

        Node objNode = this.getXMLNode(obj);

        AbstractSession session = context.getSession(obj);
        if (objNode == associatedNode) {
            DOMRecord root = new DOMRecord((Element)associatedNode);
            root.setMarshaller(marshaller);
            root.setDocPresPolicy(this.documentPreservationPolicy);
            XMLDescriptor rootDescriptor = (XMLDescriptor) session.getDescriptor(obj);
            ((XMLObjectBuilder)rootDescriptor.getObjectBuilder()).buildIntoNestedRow(root, obj, session);
        }
    }

    /**
     * Gets the XML Node associated with the provided object.
     * @param object
     * @return an XML Node used to construct the given object. Null if no node exists for this object.
     */
    public Node getXMLNode(Object object) {
        return documentPreservationPolicy.getNodeForObject(object);
    }

    /**
     * Gets the Java Object associated with the provided XML Node.
     * @param node
     * @return the Java Object associated with this node. If no object is associated then returns null
     */
    public Object getObject(Node node) {
        return documentPreservationPolicy.getObjectForNode(node);
    }

    /**
     * Updates the object associated with the provided node to reflect any changed made to that node.
     * If this Binder has no object associated with the given node, then no operation is performed.
     * @param node
     */
    public void updateObject(org.w3c.dom.Node node) {
        if (node.getNodeType() == Node.DOCUMENT_NODE) {
            node = ((Document) node).getDocumentElement();
        }

        Object cachedObject = documentPreservationPolicy.getObjectForNode(node);
        if (cachedObject != null) {
            unmarshal(node);
        } else {
            throw XMLMarshalException.objectNotFoundInCache(node.getNodeName());
        }
    }

    /**
     * Gets this XMLBinder's document preservation policy.
     * @return an instance of DocumentPreservationPolicy
     * @see org.eclipse.persistence.oxm.documentpreservation.DocumentPreservationPolicy
     */
    public DocumentPreservationPolicy getDocumentPreservationPolicy() {
        return documentPreservationPolicy;
    }

    public XMLMarshaller getMarshaller() {
        return marshaller;
    }

    public void setMarshaller(XMLMarshaller marshaller) {
        this.marshaller = marshaller;
    }

    public void setSchema(Schema aSchema) {
        this.unmarshaller.setSchema(aSchema);
        this.saxUnmarshaller.setSchema(aSchema);
    }

    public Schema getSchema() {
        return this.unmarshaller.getSchema();
    }

    public void setErrorHandler(ErrorHandler errorHandler) {
        this.unmarshaller.setErrorHandler(errorHandler);
        this.saxUnmarshaller.setErrorHandler(errorHandler);
    }

    public ErrorHandler getErrorHandler() {
        return this.unmarshaller.getErrorHandler();
    }

    /**
     * Create an XMLRoot instance.  If the object is an instance of XMLRoot
     * it will simply be returned.  Otherwise, we will create a new XMLRoot
     * using the object's descriptor default root element - any prefixes
     * will be resolved - and the given object
     *
     * @param obj
     * @return an XMLRoot instance encapsulating the given object
     */
    private XMLRoot buildXMLRootFromObject(Object obj) {
        if (obj instanceof XMLRoot) {
            return (XMLRoot) obj;
        }
        XMLRoot xmlRoot = new XMLRoot();
        xmlRoot.setObject(obj);

        // at this point, the default root element of the object being
        // marshalled to == the root element  - here we need to create
        // an XMLRoot instance using information from the returned
        // object
        org.eclipse.persistence.sessions.Session sess = this.unmarshaller.getXMLContext().getSession(obj);
        XMLDescriptor desc = (XMLDescriptor) sess.getClassDescriptor(obj);

        // here we are assuming that if we've gotten this far, there
        // must be a default root element set on the descriptor.  if
        // this is incorrect, we need to check for null and throw an
        // exception
        String rootName = desc.getDefaultRootElement();
        if (rootName == null) {
            return xmlRoot;
        }
        String rootNamespaceUri = null;
        int idx = rootName.indexOf(":");
        if (idx != -1) {
            rootNamespaceUri = desc.getNamespaceResolver().resolveNamespacePrefix(rootName.substring(0, idx));
            rootName = rootName.substring(idx + 1);
        }
        xmlRoot.setLocalName(rootName);
        xmlRoot.setNamespaceURI(rootNamespaceUri);
        return xmlRoot;
    }
}
