com.gargoylesoftware.htmlunit.activex.javascript.msxml.XMLDOMDocument.java Source code

Java tutorial

Introduction

Here is the source code for com.gargoylesoftware.htmlunit.activex.javascript.msxml.XMLDOMDocument.java

Source

/*
 * Copyright (c) 2002-2016 Gargoyle Software Inc.
 *
 * Licensed 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 com.gargoylesoftware.htmlunit.activex.javascript.msxml;

import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.IE;

import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Node;

import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.StringWebResponse;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.DomAttr;
import com.gargoylesoftware.htmlunit.html.DomComment;
import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNode;
import com.gargoylesoftware.htmlunit.html.DomProcessingInstruction;
import com.gargoylesoftware.htmlunit.html.DomText;
import com.gargoylesoftware.htmlunit.html.HTMLParser;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
import com.gargoylesoftware.htmlunit.javascript.configuration.WebBrowser;
import com.gargoylesoftware.htmlunit.xml.XmlPage;

import net.sourceforge.htmlunit.corejs.javascript.Context;

/**
 * A JavaScript object for MSXML's (ActiveX) XMLDOMDocument.<br>
 * Represents the top level of the XML source. Includes members for retrieving and creating all other XML objects.
 * @see <a href="http://msdn.microsoft.com/en-us/library/ms756987.aspx">MSDN documentation</a>
 *
 * @author Ahmed Ashour
 * @author Marc Guillemot
 * @author Sudhan Moghe
 * @author Ronald Brill
 * @author Chuck Dumont
 * @author Frank Danek
 */
@JsxClass(browsers = @WebBrowser(IE))
public class XMLDOMDocument extends XMLDOMNode {

    private static final Log LOG = LogFactory.getLog(XMLDOMDocument.class);

    private boolean async_ = true;
    private XMLDOMImplementation implementation_;
    private boolean preserveWhiteSpace_;
    private boolean preserveWhiteSpaceDuringLoad_ = true;
    private XMLDOMParseError parseError_;
    private Map<String, String> properties_ = new HashMap<>();
    private String url_ = "";

    /**
     * Creates a new instance.
     */
    public XMLDOMDocument() {
        this(null);
    }

    /**
     * Creates a new instance, with associated {@link XmlPage}.
     * @param enclosingWindow the window
     */
    public XMLDOMDocument(final WebWindow enclosingWindow) {
        if (enclosingWindow != null) {
            try {
                final XmlPage page = new XmlPage((WebResponse) null, enclosingWindow, true, false);
                setDomNode(page);
            } catch (final IOException e) {
                throw Context.reportRuntimeError("IOException: " + e);
            }
        }
    }

    /**
     * Returns if asynchronous download is permitted.
     * @return if asynchronous download is permitted
     */
    @JsxGetter
    public boolean getAsync() {
        return async_;
    }

    /**
     * Sets if asynchronous download is permitted.
     * @param async if asynchronous download is permitted
     */
    @JsxSetter
    public void setAsync(final boolean async) {
        async_ = async;
    }

    /**
     * Returns the document type node that specifies the DTD for this document.
     * @return the document type node that specifies the DTD for this document
     */
    @JsxGetter
    public XMLDOMDocumentType getDoctype() {
        final Object documentType = getPage().getDoctype();
        if (documentType == null) {
            return null;
        }
        return (XMLDOMDocumentType) getScriptableFor(documentType);
    }

    /**
     * Returns the root element of the document.
     * @return the root element of the document
     */
    @JsxGetter
    public XMLDOMElement getDocumentElement() {
        final Object documentElement = getPage().getDocumentElement();
        if (documentElement == null) {
            // for instance with an XML document with parsing error
            return null;
        }
        return (XMLDOMElement) getScriptableFor(documentElement);
    }

    /**
     * Sets the root element of the document.
     * @param element the root element of the document
     */
    @JsxSetter
    public void setDocumentElement(final XMLDOMElement element) {
        if (element == null) {
            throw Context.reportRuntimeError("Type mismatch.");
        }

        final XMLDOMElement documentElement = getDocumentElement();
        if (documentElement != null) {
            documentElement.getDomNodeOrDie().remove();
        }

        appendChild(element);
    }

    /**
     * Returns the implementation object for the document.
     * @return the implementation object for the document
     */
    @JsxGetter
    public XMLDOMImplementation getImplementation() {
        if (implementation_ == null) {
            implementation_ = new XMLDOMImplementation();
            implementation_.setParentScope(getWindow());
            implementation_.setPrototype(getPrototype(implementation_.getClass()));
        }
        return implementation_;
    }

    /**
     * Attempting to set the value of documents generates an error.
     * @param value the new value to set
     */
    @Override
    public void setNodeValue(final String value) {
        if (value == null || "null".equals(value)) {
            throw Context.reportRuntimeError("Type mismatch.");
        }
        throw Context.reportRuntimeError("This operation cannot be performed with a node of type DOCUMENT.");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getOwnerDocument() {
        return null;
    }

    /**
     * Returns a parse error object that contains information about the last parsing error.
     * @return a parse error object
     */
    @JsxGetter
    public XMLDOMParseError getParseError() {
        if (parseError_ == null) {
            parseError_ = new XMLDOMParseError();
            parseError_.setParentScope(getParentScope());
            parseError_.setPrototype(getPrototype(parseError_.getClass()));
        }
        return parseError_;
    }

    /**
     * Returns the default white space handling.
     * @return the default white space handling
     */
    @JsxGetter
    public boolean getPreserveWhiteSpace() {
        return preserveWhiteSpace_;
    }

    /**
     * Set the default white space handling.
     * @param preserveWhiteSpace the default white space handling
     */
    @JsxSetter
    public void setPreserveWhiteSpace(final boolean preserveWhiteSpace) {
        preserveWhiteSpace_ = preserveWhiteSpace;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getText() {
        final XMLDOMElement element = getDocumentElement();
        if (element == null) {
            return "";
        }
        return element.getText();
    }

    /**
     * Attempting to set the text of documents generates an error.
     * @param text the new text of this node
     */
    @Override
    public void setText(final Object text) {
        if (text == null || "null".equals(text)) {
            throw Context.reportRuntimeError("Type mismatch.");
        }
        throw Context.reportRuntimeError("This operation cannot be performed with a node of type DOCUMENT.");
    }

    /**
     * Returns the URL for the last loaded XML document.
     * @return the URL for the last loaded XML document
     */
    @JsxGetter
    public String getUrl() {
        return url_;
    }

    /**
     * Returns the XML representation of the node and all its descendants.
     * @return an XML representation of this node and all its descendants
     */
    @Override
    @JsxGetter
    public String getXml() {
        final XMLSerializer seralizer = new XMLSerializer(preserveWhiteSpaceDuringLoad_);
        return seralizer.serializeToString(getDocumentElement());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object appendChild(final Object newChild) {
        if (newChild == null || "null".equals(newChild) || !(newChild instanceof XMLDOMNode)) {
            throw Context.reportRuntimeError("Type mismatch.");
        }
        if (newChild instanceof XMLDOMCDATASection) {
            throw Context.reportRuntimeError("This operation cannot be performed with a node of type CDATA.");
        }
        if (newChild instanceof XMLDOMText) {
            throw Context.reportRuntimeError("This operation cannot be performed with a node of type TEXT.");
        }
        if (newChild instanceof XMLDOMElement && getDocumentElement() != null) {
            throw Context.reportRuntimeError("Only one top level element is allowed in an XML document.");
        }
        if (newChild instanceof XMLDOMDocumentFragment) {
            boolean elementFound = false;
            XMLDOMNode child = ((XMLDOMDocumentFragment) newChild).getFirstChild();
            while (child != null) {
                if (child instanceof XMLDOMCDATASection) {
                    throw Context
                            .reportRuntimeError("This operation cannot be performed with a node of type CDATA.");
                }
                if (child instanceof XMLDOMText) {
                    throw Context
                            .reportRuntimeError("This operation cannot be performed with a node of type TEXT.");
                }
                if (child instanceof XMLDOMElement) {
                    if (elementFound) {
                        throw Context
                                .reportRuntimeError("Only one top level element is allowed in an XML document.");
                    }
                    elementFound = true;
                }
                child = child.getNextSibling();
            }
        }

        return super.appendChild(newChild);
    }

    /**
     * Creates a new attribute with the specified name.
     *
     * @param name the name of the new attribute object
     * @return the new attribute object
     */
    @JsxFunction
    public Object createAttribute(final String name) {
        if (name == null || "null".equals(name)) {
            throw Context.reportRuntimeError("Type mismatch.");
        }
        if (StringUtils.isBlank(name) || name.indexOf('<') >= 0 || name.indexOf('>') >= 0) {
            throw Context.reportRuntimeError("To create a node of type ATTR a valid name must be given.");
        }

        final DomAttr domAttr = getPage().createAttribute(name);
        return getScriptableFor(domAttr);
    }

    /**
     * Creates a CDATA section node that contains the supplied data.
     * @param data the value to be supplied to the new CDATA section object's <code>nodeValue</code> property
     * @return the new CDATA section object
     */
    @JsxFunction
    public Object createCDATASection(final String data) {
        final CDATASection domCDATASection = getPage().createCDATASection(data);
        return getScriptableFor(domCDATASection);
    }

    /**
     * Creates a comment node that contains the supplied data.
     * @param data the value to be supplied to the new comment object's <code>nodeValue</code> property
     * @return the new comment object
     */
    @JsxFunction
    public Object createComment(final String data) {
        final DomComment domComment = new DomComment(getPage(), data);
        return getScriptableFor(domComment);
    }

    /**
     * Creates an empty document fragment object.
     * @return the new document fragment object
     */
    @JsxFunction
    public Object createDocumentFragment() {
        final DomDocumentFragment domDocumentFragment = new DomDocumentFragment(getPage());
        return getScriptableFor(domDocumentFragment);
    }

    /**
     * Creates an element node using the specified name.
     * @param tagName the name for the new element node
     * @return the new element object or <code>NOT_FOUND</code> if the tag is not supported
     */
    @JsxFunction
    public Object createElement(final String tagName) {
        if (tagName == null || "null".equals(tagName)) {
            throw Context.reportRuntimeError("Type mismatch.");
        }
        if (StringUtils.isBlank(tagName) || tagName.indexOf('<') >= 0 || tagName.indexOf('>') >= 0) {
            throw Context.reportRuntimeError("To create a node of type ELEMENT a valid name must be given.");
        }

        Object result = NOT_FOUND;
        try {
            final DomElement domElement = (DomElement) getPage().createElement(tagName);
            final Object jsElement = getScriptableFor(domElement);

            if (jsElement == NOT_FOUND) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("createElement(" + tagName
                            + ") cannot return a result as there isn't a JavaScript object for the element "
                            + domElement.getClass().getName());
                }
            } else {
                result = jsElement;
            }
        } catch (final ElementNotFoundException e) {
            // Just fall through - result is already set to NOT_FOUND
        }
        return result;
    }

    /**
     * Creates a node using the supplied type, name, and namespace.
     * @param type a value that uniquely identifies the node type
     * @param name the value for the new node's <code>nodeName</code> property
     * @param namespaceURI the namespace URI.
     *        If specified, the node is created in the context of the namespaceURI parameter
     *        with the prefix specified on the node name.
     *        If the name parameter does not have a prefix, this is treated as the default namespace.
     * @return the newly created node
     */
    @JsxFunction
    public Object createNode(final Object type, final String name, final Object namespaceURI) {
        switch ((short) Context.toNumber(type)) {
        case Node.ELEMENT_NODE:
            return createElementNS((String) namespaceURI, name);
        case Node.ATTRIBUTE_NODE:
            return createAttribute(name);

        default:
            throw Context
                    .reportRuntimeError("xmlDoc.createNode(): Unsupported type " + (short) Context.toNumber(type));
        }
    }

    /**
     * Creates a new HTML element with the given tag name, and name.
     * @param namespaceURI the URI that identifies an XML namespace
     * @param qualifiedName the qualified name of the element type to instantiate
     * @return the new element or NOT_FOUND if the tag is not supported
     */
    private Object createElementNS(final String namespaceURI, final String qualifiedName) {
        final org.w3c.dom.Element element;
        if ("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul".equals(namespaceURI)) {
            throw Context.reportRuntimeError("XUL not available");
        } else if (HTMLParser.XHTML_NAMESPACE.equals(namespaceURI)
                || HTMLParser.SVG_NAMESPACE.equals(namespaceURI)) {
            element = getPage().createElementNS(namespaceURI, qualifiedName);
        } else {
            element = new DomElement(namespaceURI, qualifiedName, getPage(), null);
        }
        return getScriptableFor(element);
    }

    /**
     * Creates a processing instruction node that contains the supplied target and data.
     * @param target the target part of the processing instruction
     * @param data the rest of the processing instruction preceding the closing ?&gt; characters
     * @return the new processing instruction object
     */
    @JsxFunction
    public Object createProcessingInstruction(final String target, final String data) {
        final DomProcessingInstruction domProcessingInstruction = ((XmlPage) getPage())
                .createProcessingInstruction(target, data);
        return getScriptableFor(domProcessingInstruction);
    }

    /**
     * Creates a text node that contains the supplied data.
     * @param data the value to be supplied to the new text object's <code>nodeValue</code> property
     * @return the new text object or <code>NOT_FOUND</code> if there is an error
     */
    @JsxFunction
    public Object createTextNode(final String data) {
        Object result = NOT_FOUND;
        try {
            final DomText domText = new DomText(getPage(), data);
            final Object jsElement = getScriptableFor(domText);

            if (jsElement == NOT_FOUND) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("createTextNode(" + data
                            + ") cannot return a result as there isn't a JavaScript object for the DOM node "
                            + domText.getClass().getName());
                }
            } else {
                result = jsElement;
            }
        } catch (final ElementNotFoundException e) {
            // Just fall through - result is already set to NOT_FOUND
        }
        return result;
    }

    /**
     * Returns a collection of elements that have the specified name.
     * @param tagName the element name to find; the <code>tagName</code> value '*' returns all elements in the document
     * @return a collection of elements that match the specified name
     */
    @JsxFunction
    public XMLDOMNodeList getElementsByTagName(final String tagName) {
        final DomNode firstChild = getDomNodeOrDie().getFirstChild();
        if (firstChild == null) {
            return XMLDOMNodeList.emptyCollection(this);
        }

        final XMLDOMNodeList collection = new XMLDOMNodeList(getDomNodeOrDie(), false,
                "XMLDOMDocument.getElementsByTagName") {
            @Override
            protected boolean isMatching(final DomNode node) {
                return node.getNodeName().equals(tagName);
            }
        };

        return collection;
    }

    /**
     * Retrieves the value of one of the second-level properties that are set either by default or using the
     * {@link #setProperty(String, String)} method.
     * @param name the name of the property
     * @return the property value
     */
    @JsxFunction
    public String getProperty(final String name) {
        return properties_.get(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected Object insertBeforeImpl(final Object[] args) {
        final Object newChild = args[0];
        if (newChild == null || "null".equals(newChild) || !(newChild instanceof XMLDOMNode)) {
            throw Context.reportRuntimeError("Type mismatch.");
        }
        if (newChild instanceof XMLDOMCDATASection) {
            throw Context.reportRuntimeError("This operation cannot be performed with a node of type CDATA.");
        }
        if (newChild instanceof XMLDOMText) {
            throw Context.reportRuntimeError("This operation cannot be performed with a node of type TEXT.");
        }
        if (newChild instanceof XMLDOMElement && getDocumentElement() != null) {
            throw Context.reportRuntimeError("Only one top level element is allowed in an XML document.");
        }
        if (newChild instanceof XMLDOMDocumentFragment) {
            boolean elementFound = false;
            XMLDOMNode child = ((XMLDOMDocumentFragment) newChild).getFirstChild();
            while (child != null) {
                if (child instanceof XMLDOMCDATASection) {
                    throw Context
                            .reportRuntimeError("This operation cannot be performed with a node of type CDATA.");
                }
                if (child instanceof XMLDOMText) {
                    throw Context
                            .reportRuntimeError("This operation cannot be performed with a node of type TEXT.");
                }
                if (child instanceof XMLDOMElement) {
                    if (elementFound) {
                        throw Context
                                .reportRuntimeError("Only one top level element is allowed in an XML document.");
                    }
                    elementFound = true;
                }
                child = child.getNextSibling();
            }
        }
        if (args.length != 2) {
            throw Context.reportRuntimeError("Wrong number of arguments or invalid property assignment.");
        }

        return super.insertBeforeImpl(args);
    }

    /**
     * Loads an XML document from the specified location.
     * @param xmlSource a URL that specifies the location of the XML file
     * @return {@code true} if the load succeeded; {@code false} if the load failed
     */
    @JsxFunction
    public boolean load(final String xmlSource) {
        if (async_) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("XMLDOMDocument.load(): 'async' is true, currently treated as false.");
            }
        }
        try {
            final HtmlPage htmlPage = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
            final URL fullyQualifiedURL = htmlPage.getFullyQualifiedUrl(xmlSource);
            final WebRequest request = new WebRequest(fullyQualifiedURL);
            final WebResponse webResponse = getWindow().getWebWindow().getWebClient().loadWebResponse(request);
            final XmlPage page = new XmlPage(webResponse, getWindow().getWebWindow(), false, false);
            setDomNode(page);

            preserveWhiteSpaceDuringLoad_ = preserveWhiteSpace_;
            url_ = fullyQualifiedURL.toExternalForm();
            return true;
        } catch (final IOException e) {
            final XMLDOMParseError parseError = getParseError();
            parseError.setErrorCode(-1);
            parseError.setFilepos(1);
            parseError.setLine(1);
            parseError.setLinepos(1);
            parseError.setReason(e.getMessage());
            parseError.setSrcText("xml");
            parseError.setUrl(xmlSource);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Error parsing XML from '" + xmlSource + "'", e);
            }
            return false;
        }
    }

    /**
     * Loads an XML document using the supplied string.
     * @param strXML the XML string to load into this XML document object;
     *     this string can contain an entire XML document or a well-formed fragment
     * @return {@code true} if the load succeeded; {@code false} if the load failed
     */
    @JsxFunction
    public boolean loadXML(final String strXML) {
        try {
            final WebWindow webWindow = getWindow().getWebWindow();

            final WebResponse webResponse = new StringWebResponse(strXML, webWindow.getEnclosedPage().getUrl());
            final XmlPage page = new XmlPage(webResponse, webWindow, false, false);
            setDomNode(page);

            preserveWhiteSpaceDuringLoad_ = preserveWhiteSpace_;
            url_ = "";
            return true;
        } catch (final IOException e) {
            final XMLDOMParseError parseError = getParseError();
            parseError.setErrorCode(-1);
            parseError.setFilepos(1);
            parseError.setLine(1);
            parseError.setLinepos(1);
            parseError.setReason(e.getMessage());
            parseError.setSrcText("xml");
            parseError.setUrl("");
            if (LOG.isDebugEnabled()) {
                LOG.debug("Error parsing XML\n" + strXML, e);
            }
            return false;
        }
    }

    /**
     * Returns the node that matches the ID attribute.
     * @param id the value of the ID to match
     * @return since we are not processing DTD, this method always returns {@code null}
     */
    @JsxFunction
    public Object nodeFromID(final String id) {
        return null;
    }

    /**
     * This method is used to set second-level properties on the DOM object.
     * @param name the name of the property to be set
     * @param value the value of the specified property
     */
    @JsxFunction
    public void setProperty(final String name, final String value) {
        properties_.put(name, value);
    }

    /**
     * @return the preserveWhiteSpaceDuringLoad
     */
    public boolean isPreserveWhiteSpaceDuringLoad() {
        return preserveWhiteSpaceDuringLoad_;
    }

    /**
     * @return the page that this document is modeling
     */
    protected SgmlPage getPage() {
        return (SgmlPage) getDomNodeOrDie();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MSXMLScriptable makeScriptableFor(final DomNode domNode) {
        final MSXMLScriptable scriptable;

        if (domNode instanceof DomElement && !(domNode instanceof HtmlElement)) {
            scriptable = new XMLDOMElement();
        } else if (domNode instanceof DomAttr) {
            scriptable = new XMLDOMAttribute();
        } else {
            return (MSXMLScriptable) super.makeScriptableFor(domNode);
        }

        scriptable.setParentScope(this);
        scriptable.setPrototype(getPrototype(scriptable.getClass()));
        scriptable.setDomNode(domNode);

        return scriptable;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void initParentScope(final DomNode domNode, final SimpleScriptable scriptable) {
        scriptable.setParentScope(getParentScope());
    }
}