
org.htmlunit.activex.javascript.msxml.XMLDOMDocument Maven / Gradle / Ivy
/*
* Copyright (c) 2002-2024 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
* https://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.htmlunit.activex.javascript.msxml;
import static org.htmlunit.javascript.configuration.SupportedBrowser.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.htmlunit.ElementNotFoundException;
import org.htmlunit.SgmlPage;
import org.htmlunit.StringWebResponse;
import org.htmlunit.WebRequest;
import org.htmlunit.WebResponse;
import org.htmlunit.WebWindow;
import org.htmlunit.html.DomAttr;
import org.htmlunit.html.DomComment;
import org.htmlunit.html.DomDocumentFragment;
import org.htmlunit.html.DomElement;
import org.htmlunit.html.DomNode;
import org.htmlunit.html.DomProcessingInstruction;
import org.htmlunit.html.DomText;
import org.htmlunit.html.Html;
import org.htmlunit.html.HtmlElement;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.JavaScriptEngine;
import org.htmlunit.javascript.configuration.JsxClass;
import org.htmlunit.javascript.configuration.JsxFunction;
import org.htmlunit.javascript.configuration.JsxGetter;
import org.htmlunit.javascript.configuration.JsxSetter;
import org.htmlunit.xml.XmlPage;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Node;
/**
* A JavaScript object for MSXML's (ActiveX) XMLDOMDocument.
* Represents the top level of the XML source. Includes members for retrieving and creating all other XML objects.
* @see MSDN documentation
*
* @author Ahmed Ashour
* @author Marc Guillemot
* @author Sudhan Moghe
* @author Ronald Brill
* @author Chuck Dumont
* @author Frank Danek
*/
@JsxClass(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 final Map 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(null, enclosingWindow, true, false);
setDomNode(page);
}
catch (final IOException e) {
throw JavaScriptEngine.reportRuntimeError("IOException: " + e);
}
}
}
/**
* Returns if asynchronous download is permitted.
* @return if asynchronous download is permitted
*/
@JsxGetter
public boolean isAsync() {
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);
}
/**
* Overwritten to throw also in non strict mode.
* @param ignored ignored param
*/
@JsxSetter
public void setDoctype(final Object ignored) {
throw JavaScriptEngine.typeError("Wrong number of arguments or invalid property assignment");
}
/**
* 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 JavaScriptEngine.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_;
}
/**
* Overwritten to throw also in non strict mode.
* @param ignored ignored param
*/
@JsxSetter
public void setImplementation(final Object ignored) {
throw JavaScriptEngine.typeError("Wrong number of arguments or invalid property assignment");
}
/**
* 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 JavaScriptEngine.reportRuntimeError("Type mismatch.");
}
throw JavaScriptEngine.reportRuntimeError("This operation cannot be performed with a node of type DOCUMENT.");
}
/**
* {@inheritDoc}
*/
@Override
public HtmlUnitScriptable 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_;
}
/**
* Overwritten to throw also in non strict mode.
* @param ignored ignored param
*/
@JsxSetter
public void setParseError(final Object ignored) {
throw JavaScriptEngine.typeError("Wrong number of arguments or invalid property assignment");
}
/**
* Returns the default white space handling.
* @return the default white space handling
*/
@JsxGetter
public boolean isPreserveWhiteSpace() {
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 JavaScriptEngine.reportRuntimeError("Type mismatch.");
}
throw JavaScriptEngine.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) {
verifyChild(newChild);
return super.appendChild(newChild);
}
private void verifyChild(final Object newChild) {
if (!(newChild instanceof XMLDOMNode)) {
throw JavaScriptEngine.reportRuntimeError("Type mismatch.");
}
if (newChild instanceof XMLDOMCDATASection) {
throw JavaScriptEngine.reportRuntimeError("This operation cannot be performed with a node of type CDATA.");
}
if (newChild instanceof XMLDOMText) {
throw JavaScriptEngine.reportRuntimeError("This operation cannot be performed with a node of type TEXT.");
}
if (newChild instanceof XMLDOMElement && getDocumentElement() != null) {
throw JavaScriptEngine.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 JavaScriptEngine.reportRuntimeError(
"This operation cannot be performed with a node of type CDATA.");
}
if (child instanceof XMLDOMText) {
throw JavaScriptEngine.reportRuntimeError(
"This operation cannot be performed with a node of type TEXT.");
}
if (child instanceof XMLDOMElement) {
if (elementFound) {
throw JavaScriptEngine.reportRuntimeError(
"Only one top level element is allowed in an XML document.");
}
elementFound = true;
}
child = child.getNextSibling();
}
}
}
/**
* 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 JavaScriptEngine.reportRuntimeError("Type mismatch.");
}
if (StringUtils.isBlank(name) || name.indexOf('<') >= 0 || name.indexOf('>') >= 0) {
throw JavaScriptEngine.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 nodeValue
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 nodeValue
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 NOT_FOUND
if the tag is not supported
*/
@JsxFunction
public Object createElement(final String tagName) {
if (tagName == null || "null".equals(tagName)) {
throw JavaScriptEngine.reportRuntimeError("Type mismatch.");
}
if (StringUtils.isBlank(tagName) || tagName.indexOf('<') >= 0 || tagName.indexOf('>') >= 0) {
throw JavaScriptEngine.reportRuntimeError("To create a node of type ELEMENT a valid name must be given.");
}
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());
}
}
return jsElement;
}
catch (final ElementNotFoundException e) {
// Just fall through - result is already set to NOT_FOUND
}
return NOT_FOUND;
}
/**
* 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 nodeName
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) JavaScriptEngine.toNumber(type)) {
case Node.ELEMENT_NODE:
return createElementNS((String) namespaceURI, name);
case Node.ATTRIBUTE_NODE:
return createAttribute(name);
default:
throw JavaScriptEngine.reportRuntimeError("xmlDoc.createNode(): Unsupported type "
+ (short) JavaScriptEngine.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 JavaScriptEngine.reportRuntimeError("XUL not available");
}
else if (Html.XHTML_NAMESPACE.equals(namespaceURI)
|| Html.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 ?> 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 nodeValue
property
* @return the new text object or NOT_FOUND
if there is an error
*/
@JsxFunction
public Object createTextNode(final String data) {
try {
final DomText domText = new DomText(getPage(), data);
final HtmlUnitScriptable jsElement = makeScriptableFor(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());
}
}
return jsElement;
}
catch (final ElementNotFoundException e) {
// Just fall through - result is already set to NOT_FOUND
}
return NOT_FOUND;
}
/**
* Returns a collection of elements that have the specified name.
* @param tagName the element name to find; the tagName
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);
}
return new XMLDOMNodeList(getDomNodeOrDie(), false, "XMLDOMDocument.getElementsByTagName") {
@Override
protected boolean isMatching(final DomNode node) {
return node.getNodeName().equals(tagName);
}
};
}
/**
* 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];
verifyChild(newChild);
if (args.length != 2) {
throw JavaScriptEngine.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_ && LOG.isDebugEnabled()) {
LOG.debug("XMLDOMDocument.load(): 'async' is true, currently treated as false.");
}
try {
final WebWindow ww = getWindow().getWebWindow();
final HtmlPage htmlPage = (HtmlPage) ww.getEnclosedPage();
final URL fullyQualifiedURL = htmlPage.getFullyQualifiedUrl(xmlSource);
final WebRequest request = new WebRequest(fullyQualifiedURL);
final WebResponse webResponse = ww.getWebClient().loadWebResponse(request);
final XmlPage page = new XmlPage(webResponse, ww, 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 HtmlUnitScriptable scriptable) {
scriptable.setParentScope(getParentScope());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy