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

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.eclipse.persistence.internal.helper.XMLHelper;
import org.eclipse.persistence.internal.oxm.Constants;
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.XMLTransformer;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
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;

/**
 * <p><b>Purpose</b>:  An implementation of XMLPlatform using JAXP 1.3 APIs.</p>
 */

public class JAXPPlatform implements XMLPlatform {

    private XPathFactory xPathFactory;
    private SchemaFactory schemaFactory;
    private DocumentBuilderFactory documentBuilderFactory;
    private boolean disableSecureProcessing = false;

    public JAXPPlatform() {
        super();
    }

    private DocumentBuilderFactory getDocumentBuilderFactory() {
        if(null == documentBuilderFactory) {
            documentBuilderFactory = XMLHelper.createDocumentBuilderFactory(isSecureProcessingDisabled());
        }
        return documentBuilderFactory;
    }

    public XPathFactory getXPathFactory() {
        if(null == xPathFactory) {
            xPathFactory = XMLHelper.createXPathFactory(isSecureProcessingDisabled());
        }
        return xPathFactory;
    }

    public SchemaFactory getSchemaFactory() {
        if(null == schemaFactory) {
            schemaFactory = XMLHelper.createSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI, isSecureProcessingDisabled());
        }
        return schemaFactory;
    }

    /**
     * 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 xPathString, XMLNamespaceResolver xmlNamespaceResolver) throws XMLPlatformException {
        try {
            XPath xPath = getXPathFactory().newXPath();
            if(null != xmlNamespaceResolver) {
                JAXPNamespaceContext namespaceContext = new JAXPNamespaceContext(xmlNamespaceResolver);
                xPath.setNamespaceContext(namespaceContext);
            }
            XPathExpression xPathExpression = xPath.compile(xPathString);
            return (NodeList) xPathExpression.evaluate(contextNode, XPathConstants.NODESET);
        } catch(XPathException e) {
            throw XMLPlatformException.xmlPlatformInvalidXPath(e);
        }
    }

    /**
     * Execute advanced XPath statements that are required for TopLink EIS.
     * @param contextNode
     * @param xPathString
     * @param xmlNamespaceResolver
     * @return
     * @throws XMLPlatformException
     */
    @Override
    public Node selectSingleNodeAdvanced(Node contextNode, String xPathString, XMLNamespaceResolver xmlNamespaceResolver) throws XMLPlatformException {
        try {
            XPath xPath = getXPathFactory().newXPath();
            if(null != xmlNamespaceResolver) {
                JAXPNamespaceContext namespaceContext = new JAXPNamespaceContext(xmlNamespaceResolver);
                xPath.setNamespaceContext(namespaceContext);
            }
            XPathExpression xPathExpression = xPath.compile(xPathString);
            return (Node) xPathExpression.evaluate(contextNode, XPathConstants.NODE);
        } catch(XPathException e) {
            throw XMLPlatformException.xmlPlatformInvalidXPath(e);
        }
    }

    @Override
    public boolean isWhitespaceNode(Text text) {
        String value = text.getNodeValue();
        if (null == value) {
            return false;
        } else {
            return value.trim().equals("");
        }
    }

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

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

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

    @Override
    public Document createDocument() throws XMLPlatformException {
        try {
            DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
            return documentBuilder.newDocument();
        } catch (Exception e) {
            throw XMLPlatformException.xmlPlatformCouldNotCreateDocument(e);
        }
    }

    @Override
    public Document createDocumentWithPublicIdentifier(String name, String publicIdentifier, String systemIdentifier) throws XMLPlatformException {
        try {
            if (null == publicIdentifier) {
                return createDocumentWithSystemIdentifier(name, systemIdentifier);
            }

            DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
            DOMImplementation domImpl = documentBuilder.getDOMImplementation();
            DocumentType docType = domImpl.createDocumentType(name, publicIdentifier, systemIdentifier);
            Document document = domImpl.createDocument(null, name, docType);
            return document;
        } catch (Exception e) {
            throw XMLPlatformException.xmlPlatformCouldNotCreateDocument(e);
        }
    }

    @Override
    public Document createDocumentWithSystemIdentifier(String name, String systemIdentifier) throws XMLPlatformException {
        try {
            Document document = null;

            if (null == systemIdentifier) {
                document = createDocument();
                Element rootElement = document.createElement(name);
                document.appendChild(rootElement);
                return document;
            }

            DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
            DOMImplementation domImpl = documentBuilder.getDOMImplementation();
            DocumentType docType = domImpl.createDocumentType(name, null, systemIdentifier);
            document = domImpl.createDocument(null, name, docType);
            return document;
        } catch (Exception e) {
            throw XMLPlatformException.xmlPlatformCouldNotCreateDocument(e);
        }
    }

    @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(javax.xml.XMLConstants.XMLNS_ATTRIBUTE);
            }
            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 boolean validateDocument(Document document, URL xmlSchemaURL, ErrorHandler errorHandler) throws XMLPlatformException {
        Schema xmlSchema;
        try {
            xmlSchema = getSchemaFactory().newSchema(xmlSchemaURL);
        } catch(SAXException e) {
            throw XMLPlatformException.xmlPlatformErrorResolvingXMLSchema(xmlSchemaURL, e);
        }
        try {
            Validator validator = xmlSchema.newValidator();
            validator.setErrorHandler(errorHandler);
            validator.validate(new DOMSource(document));
        } catch(Exception e) {
            throw XMLPlatformException.xmlPlatformValidationException(e);
        }
        return true;
    }

    @Override
    public boolean validate(Element elem, org.eclipse.persistence.oxm.XMLDescriptor xmlDescriptor, ErrorHandler handler) throws XMLPlatformException {
        return true;
    }
      @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(javax.xml.XMLConstants.XMLNS_ATTRIBUTE+":" + elementPrefix);
            if ((null == namespaceDeclaration) && !declaredPrefixes.contains(elementPrefix)) {
                (next).setAttributeNS(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, javax.xml.XMLConstants.XMLNS_ATTRIBUTE+ ":" + 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 (javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(nextAttribute.getNamespaceURI())) {
                    declaredPrefixes.add(nextAttribute.getLocalName());
                } else {
                    Attr namespaceDeclaration = next.getAttributeNode(javax.xml.XMLConstants.XMLNS_ATTRIBUTE +":" + attributePrefix);
                    if ((null == namespaceDeclaration) && !declaredPrefixes.contains(attributePrefix)) {
                        String attributeUri = nextAttribute.getNamespaceURI();
                        (next).setAttributeNS(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, javax.xml.XMLConstants.XMLNS_ATTRIBUTE + ":" + attributePrefix, attributeUri);
                        declaredPrefixes.add(attributePrefix);
                    }

                    //if xsi:type declaration deal with that value
                    if (javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(nextAttribute.getNamespaceURI()) && Constants.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(javax.xml.XMLConstants.XMLNS_ATTRIBUTE +":" + prefix);
                            if ((null == namespaceDeclaration) && !declaredPrefixes.contains(prefix)) {
                                String uri = XMLPlatformFactory.getInstance().getXMLPlatform().resolveNamespacePrefix(next, prefix);
                                (next).setAttributeNS(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, javax.xml.XMLConstants.XMLNS_ATTRIBUTE + ":" + 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) {
        boolean shouldReset = this.disableSecureProcessing ^ disableSecureProcessing;
        this.disableSecureProcessing = disableSecureProcessing;
        if (shouldReset) {
            documentBuilderFactory = null;
            schemaFactory = null;
            xPathFactory = null;
        }
    }

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

}
