/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.xerces.jaxp.validation;

import java.io.IOException;
import java.util.Enumeration;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

import org.apache.xerces.dom.NodeImpl;
import org.apache.xerces.impl.Constants;
import org.apache.xerces.impl.XMLErrorReporter;
import org.apache.xerces.impl.validation.EntityState;
import org.apache.xerces.impl.validation.ValidationManager;
import org.apache.xerces.impl.xs.XMLSchemaValidator;
import org.apache.xerces.impl.xs.util.SimpleLocator;
import org.apache.xerces.util.NamespaceSupport;
import org.apache.xerces.util.SymbolTable;
import org.apache.xerces.util.XMLAttributesImpl;
import org.apache.xerces.util.XMLSymbols;
import org.apache.xerces.xni.NamespaceContext;
import org.apache.xerces.xni.QName;
import org.apache.xerces.xni.XMLString;
import org.apache.xerces.xni.XNIException;
import org.apache.xerces.xni.parser.XMLParseException;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Entity;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

/**
 * <p>A validator helper for <code>DOMSource</code>s.</p>
 * 
 * @author Michael Glavassevich, IBM
 * @version $Id: DOMValidatorHelper.java 1868214 2019-10-10 05:15:17Z mukulg $
 */
final class DOMValidatorHelper implements ValidatorHelper, EntityState {
    
    //
    // Constants
    //

    /** Chunk size (1024). */
    private static final int CHUNK_SIZE = (1 << 10);
    
    /** Chunk mask (CHUNK_SIZE - 1). */
    private static final int CHUNK_MASK = CHUNK_SIZE - 1;
    
    // property identifiers
    
    /** Property identifier: error reporter. */
    private static final String ERROR_REPORTER =
        Constants.XERCES_PROPERTY_PREFIX + Constants.ERROR_REPORTER_PROPERTY;
    
    /** Property identifier: namespace context. */
    private static final String NAMESPACE_CONTEXT =
        Constants.XERCES_PROPERTY_PREFIX + Constants.NAMESPACE_CONTEXT_PROPERTY;
    
    /** Property identifier: XML Schema validator. */
    private static final String SCHEMA_VALIDATOR =
        Constants.XERCES_PROPERTY_PREFIX + Constants.SCHEMA_VALIDATOR_PROPERTY;
    
    /** Property identifier: symbol table. */
    private static final String SYMBOL_TABLE =
        Constants.XERCES_PROPERTY_PREFIX + Constants.SYMBOL_TABLE_PROPERTY;
    
    /** Property identifier: validation manager. */
    private static final String VALIDATION_MANAGER =
        Constants.XERCES_PROPERTY_PREFIX + Constants.VALIDATION_MANAGER_PROPERTY;
    
    /** Property identifier: xml schema version. */
    private static final String XML_SCHEMA_VERSION =
        Constants.XERCES_PROPERTY_PREFIX + Constants.XML_SCHEMA_VERSION_PROPERTY;
    
    //
    // Data
    //
    
    /** Error reporter. */
    private final XMLErrorReporter fErrorReporter;
    
    /** The namespace context of this document: stores namespaces in scope. **/
    private final NamespaceSupport fNamespaceContext;
    
    /** The namespace context of the DOMSource, includes context from ancestor nodes. **/
    private final DOMNamespaceContext fDOMNamespaceContext = new DOMNamespaceContext();
    
    /** Schema validator. **/
    private final XMLSchemaValidator fSchemaValidator;
    
    /** Symbol table **/
    private final SymbolTable fSymbolTable;
    
    /** Validation manager. **/
    private final ValidationManager fValidationManager;
    
    /** Component manager. **/
    private final XMLSchemaValidatorComponentManager fComponentManager;
    
    /** Simple Locator. **/
    private final SimpleLocator fXMLLocator = new SimpleLocator(null, null, -1, -1, -1);
    
    /** DOM document handler. **/
    private DOMDocumentHandler fDOMValidatorHandler;
    
    /** DOM result augmentor. **/
    private final DOMResultAugmentor fDOMResultAugmentor = new DOMResultAugmentor(this);
    
    /** DOM result builder. **/
    private final DOMResultBuilder fDOMResultBuilder = new DOMResultBuilder();
    
    /** Map for tracking unparsed entities. **/
    private NamedNodeMap fEntities = null;
    
    /** Array for holding character data. **/
    private final char [] fCharBuffer = new char[CHUNK_SIZE];
    
    /** Root node. **/
    private Node fRoot;
    
    /** Current element. **/
    private Node fCurrentElement;
    
    /** Fields for start element, end element, characters, comments and processing instructions. **/
    final QName fElementQName = new QName();
    final QName fAttributeQName = new QName();
    final XMLAttributesImpl fAttributes = new XMLAttributesImpl(); 
    final XMLString fTempString = new XMLString();
    
    /** Flag indicating whether the schema version is 1.1. */
    private final boolean fIsXSD11;
    
    public DOMValidatorHelper(XMLSchemaValidatorComponentManager componentManager) {
        fComponentManager = componentManager;
        fErrorReporter = (XMLErrorReporter) fComponentManager.getProperty(ERROR_REPORTER);
        fNamespaceContext = (NamespaceSupport) fComponentManager.getProperty(NAMESPACE_CONTEXT);
        fSchemaValidator = (XMLSchemaValidator) fComponentManager.getProperty(SCHEMA_VALIDATOR);
        fSymbolTable = (SymbolTable) fComponentManager.getProperty(SYMBOL_TABLE);        
        fValidationManager = (ValidationManager) fComponentManager.getProperty(VALIDATION_MANAGER);
        fIsXSD11 = Constants.W3C_XML_SCHEMA11_NS_URI.equals(fComponentManager.getProperty(XML_SCHEMA_VERSION));
    }
    
    /*
     * ValidatorHelper methods
     */
    
    public void validate(Source source, Result result) 
        throws SAXException, IOException {
        if (result instanceof DOMResult || result == null) {
            final DOMSource domSource = (DOMSource) source;
            final DOMResult domResult = (DOMResult) result;
            Node node = domSource.getNode();
            fRoot = node;
            if (node != null) {
                fComponentManager.reset();
                fValidationManager.setEntityState(this);
                fDOMNamespaceContext.reset();
                String systemId = domSource.getSystemId();
                fXMLLocator.setLiteralSystemId(systemId);
                fXMLLocator.setExpandedSystemId(systemId);
                fErrorReporter.setDocumentLocator(fXMLLocator);
                try {
                    // regardless of what type of node this is, fire start and end document events
                    setupEntityMap((node.getNodeType() == Node.DOCUMENT_NODE) ? (Document) node : node.getOwnerDocument());
                    setupDOMResultHandler(domSource, domResult);
                    fSchemaValidator.startDocument(fXMLLocator, null, fDOMNamespaceContext, null);
                    validate(node);
                    fSchemaValidator.endDocument(null);
                }
                catch (XMLParseException e) {
                    throw Util.toSAXParseException(e);
                }
                catch (XNIException e) {
                    throw Util.toSAXException(e);
                }
                finally {
                    // Release references to application objects
                    fRoot = null;
                    fCurrentElement = null;
                    fEntities = null;
                    if (fDOMValidatorHandler != null) {
                        fDOMValidatorHandler.setDOMResult(null);
                    }
                }
            }
            return;
        }
        throw new IllegalArgumentException(JAXPValidationMessageFormatter.formatMessage(fComponentManager.getLocale(), 
                "SourceResultMismatch", 
                new Object [] {source.getClass().getName(), result.getClass().getName()}));
    }
    
    /*
     * EntityState methods
     */
    
    public boolean isEntityDeclared(String name) {
        return false;
    }
    
    public boolean isEntityUnparsed(String name) {
        if (fEntities != null) {
            Entity entity = (Entity) fEntities.getNamedItem(name);
            if (entity != null) {
                return (entity.getNotationName() != null);
            }
        }
        return false;
    }
    
    /*
     * Other methods
     */
    
    /** Traverse the DOM and fire events to the schema validator. */
    private void validate(Node node) {
        final Node top = node;
        final boolean useIsSameNode = useIsSameNode(top);
        // Performs a non-recursive traversal of the DOM. This
        // will avoid a stack overflow for DOMs with high depth.
        while (node != null) {
            beginNode(node);
            Node next = node.getFirstChild();
            while (next == null) {
                finishNode(node);           
                if (top == node) {
                    break;
                }
                next = node.getNextSibling();
                if (next == null) {
                    node = node.getParentNode();
                    if (node == null || ((useIsSameNode) ? 
                        top.isSameNode(node) : top == node)) {
                        if (node != null) {
                            finishNode(node);
                        }
                        next = null;
                        break;
                    }
                }
            }
            node = next;
        }
    }
    
    /** Do processing for the start of a node. */
    private void beginNode(Node node) {
        switch (node.getNodeType()) {
            case Node.ELEMENT_NODE:
                fCurrentElement = node;
                // push namespace context
                fNamespaceContext.pushContext();
                // start element
                fillQName(fElementQName, node);
                processAttributes(node.getAttributes());
                fSchemaValidator.startElement(fElementQName, fAttributes, null);
                break;
            case Node.TEXT_NODE:
                if (fDOMValidatorHandler != null) {
                    fDOMValidatorHandler.setIgnoringCharacters(true);
                    sendCharactersToValidator(node.getNodeValue());
                    fDOMValidatorHandler.setIgnoringCharacters(false);
                    fDOMValidatorHandler.characters((Text) node);
                }
                else {
                    sendCharactersToValidator(node.getNodeValue());
                }
                break;
            case Node.CDATA_SECTION_NODE:
                if (fDOMValidatorHandler != null) {
                    fDOMValidatorHandler.setIgnoringCharacters(true);
                    fSchemaValidator.startCDATA(null);
                    sendCharactersToValidator(node.getNodeValue());
                    fSchemaValidator.endCDATA(null);
                    fDOMValidatorHandler.setIgnoringCharacters(false);
                    fDOMValidatorHandler.cdata((CDATASection) node);
                }
                else {
                    fSchemaValidator.startCDATA(null);
                    sendCharactersToValidator(node.getNodeValue());
                    fSchemaValidator.endCDATA(null); 
                }
                break;
            case Node.PROCESSING_INSTRUCTION_NODE:
                // The XSD 1.0 validator does nothing with processing instructions so bypass it unless it's 1.1.
                if (fIsXSD11) {
                    fillXMLString(fTempString, node.getNodeValue());
                    fSchemaValidator.processingInstruction(node.getNodeName(), fTempString, null);
                }
                // Send the ProcessingInstruction node directly to the result builder.
                if (fDOMValidatorHandler != null) {
                    fDOMValidatorHandler.processingInstruction((ProcessingInstruction) node);
                }
                break;
            case Node.COMMENT_NODE:
                // The XSD 1.0 validator does nothing with comments so bypass it unless it's 1.1.
                if (fIsXSD11) {
                    fillXMLString(fTempString, node.getNodeValue());
                    fSchemaValidator.comment(fTempString, null);
                }
                // Send the Comment node directly to the result builder.
                if (fDOMValidatorHandler != null) {
                    fDOMValidatorHandler.comment((Comment) node);
                }
                break;
            case Node.DOCUMENT_TYPE_NODE:
                /** 
                 * Send the DocumentType node directly to the result builder.
                 */
                if (fDOMValidatorHandler != null) {
                    fDOMValidatorHandler.doctypeDecl((DocumentType) node);
                }
                break;
            default: // Ignore other node types.
                break;
        }
    }
    
    /** Do processing for the end of a node. */
    private void finishNode(Node node) {
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            fCurrentElement = node;
            // end element
            fillQName(fElementQName, node);
            fSchemaValidator.endElement(fElementQName, null);
            // pop namespace context
            fNamespaceContext.popContext();
        }
    }
    
    /**
     * Extracts NamedNodeMap of entities. We need this to validate
     * elements and attributes of type xs:ENTITY, xs:ENTITIES or 
     * types dervied from them.
     */
    private void setupEntityMap(Document doc) {
        if (doc != null) {
            DocumentType docType = doc.getDoctype();
            if (docType != null) {
                fEntities = docType.getEntities();
                return;
            }
        }
        fEntities = null;
    }
    
    /**
     * Sets up handler for <code>DOMResult</code>.
     */
    private void setupDOMResultHandler(DOMSource source, DOMResult result) throws SAXException {
        // If there's no DOMResult, unset the validator handler
        if (result == null) {
            fDOMValidatorHandler = null;
            fSchemaValidator.setDocumentHandler(null);
            return;
        }
        final Node nodeResult = result.getNode();
        // If the source node and result node are the same use the DOMResultAugmentor.
        // Otherwise use the DOMResultBuilder.
        if (source.getNode() == nodeResult) {
            fDOMValidatorHandler = fDOMResultAugmentor;
            fDOMResultAugmentor.setDOMResult(result);
            fSchemaValidator.setDocumentHandler(fDOMResultAugmentor);
            return;
        }
        if (result.getNode() == null) {
            try {
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                factory.setNamespaceAware(true);
                DocumentBuilder builder = factory.newDocumentBuilder();
                result.setNode(builder.newDocument());
            }
            catch (ParserConfigurationException e) {
                throw new SAXException(e);
            }
        }
        fDOMValidatorHandler = fDOMResultBuilder;
        fDOMResultBuilder.setDOMResult(result);
        fSchemaValidator.setDocumentHandler(fDOMResultBuilder);
    }
    
    private void fillQName(QName toFill, Node node) {
        final String prefix = node.getPrefix();
        final String localName = node.getLocalName();
        final String rawName = node.getNodeName();
        final String namespace = node.getNamespaceURI();
        toFill.prefix = (prefix != null) ? fSymbolTable.addSymbol(prefix) : XMLSymbols.EMPTY_STRING;
        toFill.localpart = (localName != null) ? fSymbolTable.addSymbol(localName) : XMLSymbols.EMPTY_STRING;
        toFill.rawname = (rawName != null) ? fSymbolTable.addSymbol(rawName) : XMLSymbols.EMPTY_STRING; 
        toFill.uri = (namespace != null && namespace.length() > 0) ? fSymbolTable.addSymbol(namespace) : null;
    }
    
    private void processAttributes(NamedNodeMap attrMap) {
        final int attrCount = attrMap.getLength();
        fAttributes.removeAllAttributes();
        for (int i = 0; i < attrCount; ++i) {
            Attr attr = (Attr) attrMap.item(i);
            String value = attr.getValue();
            if (value == null) {
                value = XMLSymbols.EMPTY_STRING;
            }
            fillQName(fAttributeQName, attr);
            // REVISIT: Assuming all attributes are of type CDATA. The actual type may not matter. -- mrglavas
            fAttributes.addAttributeNS(fAttributeQName, XMLSymbols.fCDATASymbol, value);
            fAttributes.setSpecified(i, attr.getSpecified());
            // REVISIT: Should we be looking at non-namespace attributes
            // for additional mappings? Should we detect illegal namespace
            // declarations and exclude them from the context? -- mrglavas
            if (fAttributeQName.uri == NamespaceContext.XMLNS_URI) {
                // process namespace attribute
                if (fAttributeQName.prefix == XMLSymbols.PREFIX_XMLNS) {
                    fNamespaceContext.declarePrefix(fAttributeQName.localpart, value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
                }
                else {
                    fNamespaceContext.declarePrefix(XMLSymbols.EMPTY_STRING, value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
                }
            }
        }
    }
    
    private void sendCharactersToValidator(String str) {
        if (str != null) {
            final int length = str.length();
            final int remainder = length & CHUNK_MASK;
            if (remainder > 0) {
                str.getChars(0, remainder, fCharBuffer, 0);
                fTempString.setValues(fCharBuffer, 0, remainder);
                fSchemaValidator.characters(fTempString, null);
            }
            int i = remainder;
            while (i < length) {
                str.getChars(i, i += CHUNK_SIZE, fCharBuffer, 0);
                fTempString.setValues(fCharBuffer, 0, CHUNK_SIZE);
                fSchemaValidator.characters(fTempString, null);
            }
        }
    }
    
    private void fillXMLString(XMLString toFill, String str) {
        final int length = str.length();
        final char[] strArray;
        if (length <= fCharBuffer.length) {
            str.getChars(0, length, fCharBuffer, 0);
            strArray = fCharBuffer;
        }
        else {
            strArray = str.toCharArray();
        }
        toFill.setValues(strArray, 0, length);
    }
    
    /**
     * Use isSameNode() for testing node identity if the DOM implementation
     * supports DOM Level 3 core and it isn't the Xerces implementation.
     */
    private boolean useIsSameNode(Node node) {
        if (node instanceof NodeImpl) {
            return false;
        }
        Document doc = node.getNodeType() == Node.DOCUMENT_NODE 
            ? (Document) node : node.getOwnerDocument();
        return (doc != null && doc.getImplementation().hasFeature("Core", "3.0"));
    }
    
    /**
     * Returns the current element node.
     */
    Node getCurrentElement() {
        return fCurrentElement;
    }
    
    /**
     * NamespaceContext for the DOMSource, includes context for ancestor nodes.
     */
    final class DOMNamespaceContext implements NamespaceContext {
        
        //
        // Data
        //

        /** 
         * Namespace binding information. This array is composed of a
         * series of tuples containing the namespace binding information:
         * &lt;prefix, uri&gt;.
         */
        protected String[] fNamespace = new String[16 * 2];

        /** The size of the namespace information array. */
        protected int fNamespaceSize = 0;
        
        /** 
         * Flag indicating whether the namespace context 
         * has been from the root node's ancestors.
         */
        protected boolean fDOMContextBuilt = false;
        
        //
        // Methods
        //

        public void pushContext() {
            fNamespaceContext.pushContext();
        }

        public void popContext() {
            fNamespaceContext.popContext();
        }

        public boolean declarePrefix(String prefix, String uri) {
            return fNamespaceContext.declarePrefix(prefix, uri);
        }
        
        public boolean deletePrefix(String prefix) {
            return fNamespaceContext.deletePrefix(prefix);
        }

        public String getURI(String prefix) {
            String uri = fNamespaceContext.getURI(prefix);
            if (uri == null) {
                if (!fDOMContextBuilt) {
                    fillNamespaceContext();
                    fDOMContextBuilt = true;
                }
                if (fNamespaceSize > 0 && 
                    !fNamespaceContext.containsPrefix(prefix)) {
                    uri = getURI0(prefix);
                }
            }
            return uri;
        }

        public String getPrefix(String uri) {
            return fNamespaceContext.getPrefix(uri);
        }

        public int getDeclaredPrefixCount() {
            return fNamespaceContext.getDeclaredPrefixCount();
        }

        public String getDeclaredPrefixAt(int index) {
            return fNamespaceContext.getDeclaredPrefixAt(index);
        }

        public Enumeration getAllPrefixes() {
            return fNamespaceContext.getAllPrefixes();
        }

        public void reset() {
            fDOMContextBuilt = false;
            fNamespaceSize = 0; 
        }
        
        private void fillNamespaceContext() {
            if (fRoot != null) {
                Node currentNode = fRoot.getParentNode();
                while (currentNode != null) {
                    if (Node.ELEMENT_NODE == currentNode.getNodeType()) {
                        NamedNodeMap attributes = currentNode.getAttributes();
                        final int attrCount = attributes.getLength();
                        for (int i = 0; i < attrCount; ++i) {
                            Attr attr = (Attr) attributes.item(i);
                            String value = attr.getValue();
                            if (value == null) {
                                value = XMLSymbols.EMPTY_STRING;
                            }
                            fillQName(fAttributeQName, attr);
                            // REVISIT: Should we be looking at non-namespace attributes
                            // for additional mappings? Should we detect illegal namespace
                            // declarations and exclude them from the context? -- mrglavas
                            if (fAttributeQName.uri == NamespaceContext.XMLNS_URI) {
                                // process namespace attribute
                                if (fAttributeQName.prefix == XMLSymbols.PREFIX_XMLNS) {
                                    declarePrefix0(fAttributeQName.localpart, value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
                                }
                                else {
                                    declarePrefix0(XMLSymbols.EMPTY_STRING, value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
                                }
                            }
                        }
                        
                    }
                    currentNode = currentNode.getParentNode();
                }
            }
        }
        
        private void declarePrefix0(String prefix, String uri) {           
            // resize array, if needed
            if (fNamespaceSize == fNamespace.length) {
                String[] namespacearray = new String[fNamespaceSize * 2];
                System.arraycopy(fNamespace, 0, namespacearray, 0, fNamespaceSize);
                fNamespace = namespacearray;
            }

            // bind prefix to uri in current context
            fNamespace[fNamespaceSize++] = prefix;
            fNamespace[fNamespaceSize++] = uri;
        }
        
        private String getURI0(String prefix) {
            // find prefix in the DOM context
            for (int i = 0; i < fNamespaceSize; i += 2) {
                if (fNamespace[i] == prefix) {
                    return fNamespace[i + 1];
                }
            }
            // prefix not found
            return null;
        }
    }
    
} // DOMValidatorHelper
