/*******************************************************************************
 * Copyright (c) 1998, 2016 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 v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/
package org.eclipse.persistence.platform.xml.xdk;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.oxm.NamespaceResolver;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.platform.xml.XMLNamespaceResolver;
import org.eclipse.persistence.platform.xml.XMLParser;
import org.eclipse.persistence.platform.xml.XMLPlatform;
import org.eclipse.persistence.platform.xml.XMLPlatformException;
import org.eclipse.persistence.platform.xml.XMLPlatformFactory;
import org.eclipse.persistence.platform.xml.XMLSchemaReference;
import org.eclipse.persistence.platform.xml.XMLTransformer;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import oracle.xml.parser.schema.XMLSchema;
import oracle.xml.parser.schema.XSDBuilder;
import oracle.xml.parser.schema.XSDComplexType;
import oracle.xml.parser.schema.XSDConstantValues;
import oracle.xml.parser.schema.XSDElement;
import oracle.xml.parser.schema.XSDNode;
import oracle.xml.parser.schema.XSDValidator;
import oracle.xml.parser.v2.XMLDocument;
import oracle.xml.parser.v2.XMLElement;
import oracle.xml.parser.v2.XMLError;
import oracle.xml.parser.v2.XMLNode;
import oracle.xml.parser.v2.XMLParseException;
import oracle.xml.parser.v2.XSLException;

/**
 * <p><b>Purpose</b>:  An implementation of XMLPlatform using Oracle XDK APIs.</p>
 */

public class XDKPlatform implements XMLPlatform {
    private Method buildSchemaMethod;

    public XDKPlatform() {
        super();
        try {
            Class[] argTypes = { URL.class };
            buildSchemaMethod = Helper.getDeclaredMethod(XSDBuilder.class, "build", argTypes);
        } catch (NoSuchMethodException e) {
        }
    }

    /**
     * Execute advanced XPath statements that are required for TopLink EIS.
     * @param contextNode
     * @param xPath
     * @param xmlNamespaceResolver
     * @return
     * @throws XMLPlatformException
     */
    @Override
    public Node selectSingleNodeAdvanced(Node contextNode, String xPath, XMLNamespaceResolver xmlNamespaceResolver) throws XMLPlatformException {
        try {
            XMLNode xmlNode = (XMLNode)contextNode;
            XDKNamespaceResolver xdkNamespaceResolver = new XDKNamespaceResolver(xmlNamespaceResolver);
            return xmlNode.selectSingleNode(xPath, xdkNamespaceResolver);
        } catch (XSLException e) {
            throw XMLPlatformException.xmlPlatformInvalidXPath(e);
        }
    }

    /**
     * Execute advanced XPath statements that are required for TopLink EIS.
     * @param  contextNode the node relative to which the XPath
     *         statement will be executed.
     *         xPath the XPath statement
     *         namespaceResolver used to resolve namespace prefixes
     *         to the corresponding namespace URI
     * @return the XPath result
     * @throws XMLPlatformException
     */
    @Override
    public NodeList selectNodesAdvanced(Node contextNode, String xPath, XMLNamespaceResolver xmlNamespaceResolver) throws XMLPlatformException {
        try {
            XMLNode xmlNode = (XMLNode)contextNode;
            XDKNamespaceResolver xdkNamespaceResolver = new XDKNamespaceResolver(xmlNamespaceResolver);
            return xmlNode.selectNodes(xPath, xdkNamespaceResolver);
        } catch (XSLException e) {
            throw XMLPlatformException.xmlPlatformInvalidXPath(e);
        }
    }

    @Override
    public Document createDocument() throws XMLPlatformException {
        return new XMLDocument();
    }

    @Override
    public Document createDocumentWithPublicIdentifier(String name, String publicIdentifier, String systemIdentifier) throws XMLPlatformException {
        try {
            XMLDocument xmlDocument = (XMLDocument)createDocument();
            Element rootElement = xmlDocument.createElement(name);
            xmlDocument.appendChild(rootElement);
            xmlDocument.setDoctype(name, systemIdentifier, publicIdentifier);
            return xmlDocument;
        } catch (Exception e) {
            throw XMLPlatformException.xmlPlatformCouldNotCreateDocument(e);
        }
    }

    @Override
    public Document createDocumentWithSystemIdentifier(String name, String systemIdentifier) throws XMLPlatformException {
        try {
            if (null == systemIdentifier) {
                Document document = createDocument();
                Element rootElement = document.createElement(name);
                document.appendChild(rootElement);
                return document;
            }

            XMLDocument xmlDocument = (XMLDocument)createDocument();
            Element rootElement = xmlDocument.createElement(name);
            xmlDocument.appendChild(rootElement);
            xmlDocument.setDoctype(name, systemIdentifier, null);
            return xmlDocument;
        } catch (Exception e) {
            throw XMLPlatformException.xmlPlatformCouldNotCreateDocument(e);
        }
    }

    @Override
    public boolean isWhitespaceNode(Text text) {
        try {
            String value = text.getNodeValue();
            if (null == value) {
                return false;
            } else {
                return value.trim().equals("");
            }
        } catch (NullPointerException e) {
            // The 9.0.4 XDK will throw a NPE on getNoderValue() if the node value is null.
            return false;
        }
    }

    @Override
    public String resolveNamespacePrefix(Node contextNode, String namespacePrefix) throws XMLPlatformException {
        if (null == namespacePrefix) {
            if (null == contextNode.getPrefix()) {
                return contextNode.getNamespaceURI();
            }
        } else if (namespacePrefix.equals(contextNode.getPrefix())) {
            return contextNode.getNamespaceURI();
        }

        if (contextNode.getNodeType() == Node.ELEMENT_NODE) {
            Element contextElement = (Element)contextNode;
            Attr namespaceDeclaration = null;
            if(namespacePrefix != null) {
                namespaceDeclaration = contextElement.getAttributeNode("xmlns:" + namespacePrefix);
            } else {
                //look for default namespace declaration for null prefix
                namespaceDeclaration = contextElement.getAttributeNode(XMLConstants.XMLNS);
            }
            if (null != namespaceDeclaration) {
                return namespaceDeclaration.getValue();
            }
        }

        Node parentNode = contextNode.getParentNode();
        if (parentNode != null && parentNode.getNodeType() == Node.ELEMENT_NODE) {
            return resolveNamespacePrefix(parentNode, namespacePrefix);
        }

        return null;
    }

    @Override
    public XMLParser newXMLParser() {
        return new XDKParser();
    }

    @Override
    public XMLParser newXMLParser(Map<String, Boolean> parserFeatures) {
        return new XDKParser();
    }

    @Override
    public XMLTransformer newXMLTransformer() {
        return new XDKTransformer();
    }

    /**
     * Validates a document against an XML schema
     *
     * @param document - the document to be validated
     * @param xmlSchemaURL - the schema URL
     * @param errorHandler - the error handler
     * @return true if the document fragment is valid, false otherwise
     */
    @Override
    public boolean validateDocument(Document document, URL xmlSchemaURL, ErrorHandler errorHandler) throws XMLPlatformException {
        XMLSchema xmlSchema = null;
        XSDValidator validator = null;
        try {
            Object[] args = { xmlSchemaURL };
            xmlSchema = (XMLSchema)buildSchemaMethod.invoke(new XSDBuilder(), args);
            validator = new XSDValidator();
        } catch (Exception e) {
            throw XMLPlatformException.xmlPlatformErrorResolvingXMLSchema(xmlSchemaURL, e);
        }

        // set the schema to be validated against
        validator.setXMLProperty(XSDValidator.FIXED_SCHEMA, xmlSchema);

        XMLError xmlErr = new XMLError();
        try {
            validator.setError(xmlErr);
        } catch (org.xml.sax.SAXException saxex) {
            throw XMLPlatformException.xmlPlatformValidationException(saxex);
        }

        try {
            ((XMLDocument)document).validateContent(validator, true);
        } catch (XMLParseException e) {
            // Ignore this exception, the XMLError will be used to determine if theree
            // were any errors.
        }
        handleErrors(xmlErr, errorHandler);

        return true;
    }

    /**
     * Validates a document fragment against a complex type or element in the XML schema
     *
     * @param elem - the document which contains the document fragment to be validated
     * @param xmlDescriptor - the path to the complex type or element to be validated against in the schema
     * @param errorHandler - the error handler
     * @return true if the document fragment is valid, false otherwise
     */
    @Override
    public boolean validate(Element elem, XMLDescriptor xmlDescriptor, ErrorHandler errorHandler) throws XMLPlatformException {
        XMLSchemaReference schemaReference = xmlDescriptor.getSchemaReference();
        NamespaceResolver nsResolver = xmlDescriptor.getNamespaceResolver();

        // build a schema using the URL in the schema reference, and setup a validator
        XMLSchema xmlSchema;
        XSDValidator validator = null;
        try {
            Object[] args = { schemaReference.getURL() };
            xmlSchema = (XMLSchema)buildSchemaMethod.invoke(new XSDBuilder(), args);
            validator = new XSDValidator();
        } catch (Exception ex) {
            throw XMLPlatformException.xmlPlatformValidationException(ex);
        }

        // set the schema to be validated against
        validator.setXMLProperty(XSDValidator.FIXED_SCHEMA, xmlSchema);

        // set the node to be validated against
        XSDNode xsdNode = getNodeFromSchemaReference(xmlSchema, schemaReference, nsResolver);

        // if xsdNode is null, the schema context string is empty or the target could not be found
        if (xsdNode == null) {
            validator.setXMLProperty(XSDNode.ROOT_NODE, null);
        }

        if (schemaReference.getType() == XMLSchemaReference.ELEMENT) {
            if (xmlDescriptor.getDefaultRootElement() != null) {
                validator.setXMLProperty(XSDNode.ROOT_NODE, xsdNode);
            } else {
                validator.setXMLProperty(XSDNode.ROOT_NODE, ((XSDElement)xsdNode).getType());
            }
        } else {
            validator.setXMLProperty(XSDNode.ROOT_NODE, xsdNode);
        }

        XMLError xmlErr = new XMLError();
        try {
            validator.setError(xmlErr);
        } catch (org.xml.sax.SAXException saxex) {
            throw XMLPlatformException.xmlPlatformValidationException(saxex);
        }

        try {
            ((XMLElement)elem).validateContent(validator, true);
        } catch (XMLParseException e) {
            // Ignore this exception, the XMLError will be used to determine if theree
            // were any errors.
        }
        handleErrors(xmlErr, errorHandler);

        return true;
    }

    private void handleErrors(XMLError xmlErr, ErrorHandler errorHandler) {
        try {
            int numberOfMessages = xmlErr.getNumMessages();
            SAXParseException saxParseException;
            for (int x = 0; x < numberOfMessages; x++) {
                saxParseException = new SAXParseException(xmlErr.getMessage(x), xmlErr.getPublicId(x), xmlErr.getSystemId(x), xmlErr.getLineNumber(x), xmlErr.getColumnNumber(x), xmlErr.getException(x));

                if (null == errorHandler) {
                    throw saxParseException;
                }
                errorHandler.fatalError(saxParseException);
            }
        } catch (SAXException xmlex) {
            throw XMLPlatformException.xmlPlatformValidationException(xmlex);
        }
    }

    /**
     * This convenience method will parse a schema reference and return the node to be
     * validated against.
     *
     * @param xmlSchema - the schema to be used for validation
     * @param schemaRef - the schema reference object
     * @return the node to be validated against, null if not found
     */
    private XSDNode getNodeFromSchemaReference(XMLSchema xmlSchema, XMLSchemaReference schemaRef, NamespaceResolver nsResolver) {
        if (schemaRef == null) {
            return null;
        }

        // schema context should be in the format '/prefix:nodeName/.."
        // tokenize the schema context to find the node that is to be validated
        StringTokenizer nodes = new StringTokenizer(schemaRef.getSchemaContext(), "/");

        // if no tokens, then invalid schema context
        if (!(nodes.hasMoreTokens())) {
            return null;
        }

        String namespace = "";
        String nodeName = nodes.nextToken();

        // look for a prefix
        StringTokenizer prefixes = new StringTokenizer(nodeName, ":");

        if (prefixes.countTokens() > 1) {
            // look for a namespace
            namespace = nsResolver.resolveNamespacePrefix(prefixes.nextToken());
            if (namespace == null) {
                namespace = "";
            }
        }

        // else no prefix
        nodeName = prefixes.nextToken();

        // handle simple/complex type definitions
        if (schemaRef.getType() == XMLSchemaReference.SIMPLE_TYPE) {
            return xmlSchema.getType(namespace, nodeName, XSDConstantValues.DATATYPE);
        }

        if (schemaRef.getType() == XMLSchemaReference.COMPLEX_TYPE) {
            return xmlSchema.getType(namespace, nodeName, XSDConstantValues.TYPE);
        }

        // handle elements
        XSDNode node = xmlSchema.getElement(namespace, nodeName);

        // loop through schema context tokens - 'node' will contain the target when completed
        while (nodes.hasMoreTokens()) {
            node = findChildNode((XSDElement)node, nodes.nextToken());

            // if node is null, couldn't find child
            if (node == null) {
                return null;
            }
        }
        return node;
    }

    /**
     * This convenience method will iterate through a parent element's children and return the
     * node corresponding to 'nodeName'.
     *
     * @param parent - the parent element
     * @param childName - the node name to be located
     * @return the child node with name matching 'childName', null if not found
     */
    protected XSDNode findChildNode(XSDElement parent, String childName) {
        XSDNode[] children;
        XSDNode node = null;
        boolean successful = false;

        // get the parent node's children
        children = ((XSDComplexType)parent.getType()).getElementSet();

        // iterate over child nodes looking for the child
        for (int i = 0; i < children.length; i++) {
            node = children[i];

            if (node.getName().equals(childName)) {
                successful = true;
                break;
            }
        }

        if (successful) {
            return node;
        }
        return null;
    }

    @Override
    public void namespaceQualifyFragment(Element next) {
        namespaceQualifyFragment(next, new ArrayList<String>());
    }

    //pass list of prefixes declared and encountered
    private void namespaceQualifyFragment(Element next, List<String> declaredPrefixes) {
        String elementUri = next.getNamespaceURI();
        String elementPrefix = next.getPrefix();
        if (elementPrefix != null) {
            //see if this prefix is already declared if yes - do nothing, if no declare
            Attr namespaceDeclaration = next.getAttributeNode(XMLConstants.XMLNS +":" + elementPrefix);
            if ((null == namespaceDeclaration) && !declaredPrefixes.contains(elementPrefix)) {
                (next).setAttributeNS(XMLConstants.XMLNS_URL, XMLConstants.XMLNS + ":" + elementPrefix, elementUri);
                declaredPrefixes.add(elementPrefix);
            }
        }

        //check all attributes prefixes and if any of them arent declared add them also.
        NamedNodeMap attributes = next.getAttributes();
        int attributesSize = attributes.getLength();
        for (int i = 0; i < attributesSize; i++) {
            Attr nextAttribute = (Attr)attributes.item(i);
            String attributePrefix = nextAttribute.getPrefix();
            if (attributePrefix != null) {
                //if attribute is a namespace declaration add to declared list
                if (XMLConstants.XMLNS_URL.equals(nextAttribute.getNamespaceURI())) {
                    declaredPrefixes.add(nextAttribute.getLocalName());
                } else {
                    Attr namespaceDeclaration = next.getAttributeNode(XMLConstants.XMLNS +":" + attributePrefix);
                    if ((null == namespaceDeclaration) && !declaredPrefixes.contains(attributePrefix)) {
                        String attributeUri = nextAttribute.getNamespaceURI();
                        (next).setAttributeNS(XMLConstants.XMLNS_URL, XMLConstants.XMLNS + ":" + attributePrefix, attributeUri);
                        declaredPrefixes.add(attributePrefix);
                    }

                    //if xsi:type declaration deal with that value
                    if (XMLConstants.SCHEMA_INSTANCE_URL.equals(nextAttribute.getNamespaceURI()) && XMLConstants.SCHEMA_TYPE_ATTRIBUTE.equals(nextAttribute.getLocalName())) {
                        String value = nextAttribute.getValue();
                        int colonIndex = value.indexOf(':');
                        if (colonIndex > -1) {
                            String prefix = value.substring(0, colonIndex);
                            namespaceDeclaration = next.getAttributeNode(XMLConstants.XMLNS +":" + prefix);
                            if ((null == namespaceDeclaration) && !declaredPrefixes.contains(prefix)) {
                                String uri = XMLPlatformFactory.getInstance().getXMLPlatform().resolveNamespacePrefix(next, prefix);
                                (next).setAttributeNS(XMLConstants.XMLNS_URL, XMLConstants.XMLNS + ":" + prefix, uri);
                                declaredPrefixes.add(prefix);
                            }
                        }
                    }
                }
            }
        }

        NodeList children = next.getChildNodes();
        int numberOfNodes = children.getLength();
        for (int i = 0; i < numberOfNodes; i++) {
            Node nextNode = children.item(i);
            if (nextNode.getNodeType() == Node.ELEMENT_NODE) {
                Element child = (Element)nextNode;
                namespaceQualifyFragment(child, declaredPrefixes);
            }
        }
    }

    @Override
    public void setDisableSecureProcessing(boolean disableSecureProcessing) {
        //no-op
    }

    @Override
    public boolean isSecureProcessingDisabled() {
        return false;
    }
}
