All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.htmlunit.html.DomNode Maven / Gradle / Ivy

Go to download

XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.

There is a newer version: 8.4.1
Show newest version
/*
 * 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.html;

import static org.htmlunit.BrowserVersionFeatures.DOM_NORMALIZE_REMOVE_CHILDREN;
import static org.htmlunit.BrowserVersionFeatures.QUERYSELECTORALL_NOT_IN_QUIRKS;
import static org.htmlunit.BrowserVersionFeatures.XPATH_SELECTION_NAMESPACES;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import org.htmlunit.BrowserVersionFeatures;
import org.htmlunit.IncorrectnessListener;
import org.htmlunit.Page;
import org.htmlunit.SgmlPage;
import org.htmlunit.WebAssert;
import org.htmlunit.WebClient;
import org.htmlunit.WebClient.PooledCSS3Parser;
import org.htmlunit.WebWindow;
import org.htmlunit.activex.javascript.msxml.XMLDOMDocument;
import org.htmlunit.css.ComputedCssStyleDeclaration;
import org.htmlunit.css.CssStyleSheet;
import org.htmlunit.css.StyleAttributes;
import org.htmlunit.cssparser.parser.CSSErrorHandler;
import org.htmlunit.cssparser.parser.CSSException;
import org.htmlunit.cssparser.parser.CSSOMParser;
import org.htmlunit.cssparser.parser.CSSParseException;
import org.htmlunit.cssparser.parser.selector.Selector;
import org.htmlunit.cssparser.parser.selector.SelectorList;
import org.htmlunit.html.HtmlElement.DisplayStyle;
import org.htmlunit.html.serializer.HtmlSerializerNormalizedText;
import org.htmlunit.html.serializer.HtmlSerializerVisibleText;
import org.htmlunit.html.xpath.XPathHelper;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.JavaScriptEngine;
import org.htmlunit.javascript.host.event.Event;
import org.htmlunit.javascript.host.html.HTMLDocument;
import org.htmlunit.util.StringUtils;
import org.htmlunit.xml.XmlPage;
import org.htmlunit.xpath.xml.utils.PrefixResolver;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.UserDataHandler;
import org.xml.sax.SAXException;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Base class for nodes in the HTML DOM tree. This class is modeled after the
 * W3C DOM specification, but does not implement it.
 *
 * @author Mike Bowler
 * @author Mike J. Bresnahan
 * @author David K. Taylor
 * @author Christian Sell
 * @author Chris Erskine
 * @author Mike Williams
 * @author Marc Guillemot
 * @author Denis N. Antonioli
 * @author Daniel Gredler
 * @author Ahmed Ashour
 * @author Rodney Gitzel
 * @author Sudhan Moghe
 * @author Tom Anderson
 * @author Ronald Brill
 * @author Chuck Dumont
 * @author Frank Danek
 */
public abstract class DomNode implements Cloneable, Serializable, Node {

    /** A ready state constant for IE (state 1). */
    public static final String READY_STATE_UNINITIALIZED = "uninitialized";

    /** A ready state constant for IE (state 2). */
    public static final String READY_STATE_LOADING = "loading";

    /** A ready state constant for IE (state 3). */
    public static final String READY_STATE_LOADED = "loaded";

    /** A ready state constant for IE (state 4). */
    public static final String READY_STATE_INTERACTIVE = "interactive";

    /** A ready state constant for IE (state 5). */
    public static final String READY_STATE_COMPLETE = "complete";

    /** The name of the "element" property. Used when watching property change events. */
    public static final String PROPERTY_ELEMENT = "element";

    /** The owning page of this node. */
    private SgmlPage page_;

    /** The parent node. */
    private DomNode parent_;

    /**
     * The previous sibling. The first child's previousSibling points
     * to the end of the list
     */
    private DomNode previousSibling_;

    /**
     * The next sibling. The last child's nextSibling is {@code null}
     */
    private DomNode nextSibling_;

    /** Start of the child list. */
    private DomNode firstChild_;

    /**
     * This is the JavaScript object corresponding to this DOM node. It may
     * be null if there isn't a corresponding JavaScript object.
     */
    private HtmlUnitScriptable scriptObject_;

    /** The ready state is an IE-only value that is available to a large number of elements. */
    private String readyState_;

    /**
     * The line number in the source page where the DOM node starts.
     */
    private int startLineNumber_ = -1;

    /**
     * The column number in the source page where the DOM node starts.
     */
    private int startColumnNumber_ = -1;

    /**
     * The line number in the source page where the DOM node ends.
     */
    private int endLineNumber_ = -1;

    /**
     * The column number in the source page where the DOM node ends.
     */
    private int endColumnNumber_ = -1;

    private boolean attachedToPage_;

    /** The listeners which are to be notified of characterData change. */
    private List characterDataListeners_;
    private List domListeners_;

    private Map userData_;

    /**
     * Creates a new instance.
     * @param page the page which contains this node
     */
    protected DomNode(final SgmlPage page) {
        readyState_ = READY_STATE_LOADING;
        page_ = page;
    }

    /**
     * Sets the line and column numbers in the source page where the DOM node starts.
     *
     * @param startLineNumber the line number where the DOM node starts
     * @param startColumnNumber the column number where the DOM node starts
     */
    public void setStartLocation(final int startLineNumber, final int startColumnNumber) {
        startLineNumber_ = startLineNumber;
        startColumnNumber_ = startColumnNumber;
    }

    /**
     * Sets the line and column numbers in the source page where the DOM node ends.
     *
     * @param endLineNumber the line number where the DOM node ends
     * @param endColumnNumber the column number where the DOM node ends
     */
    public void setEndLocation(final int endLineNumber, final int endColumnNumber) {
        endLineNumber_ = endLineNumber;
        endColumnNumber_ = endColumnNumber;
    }

    /**
     * Returns the line number in the source page where the DOM node starts.
     * @return the line number in the source page where the DOM node starts
     */
    public int getStartLineNumber() {
        return startLineNumber_;
    }

    /**
     * Returns the column number in the source page where the DOM node starts.
     * @return the column number in the source page where the DOM node starts
     */
    public int getStartColumnNumber() {
        return startColumnNumber_;
    }

    /**
     * Returns the line number in the source page where the DOM node ends.
     * @return 0 if no information on the line number is available (for instance for nodes dynamically added),
     * -1 if the end tag has not yet been parsed (during page loading)
     */
    public int getEndLineNumber() {
        return endLineNumber_;
    }

    /**
     * Returns the column number in the source page where the DOM node ends.
     * @return 0 if no information on the line number is available (for instance for nodes dynamically added),
     * -1 if the end tag has not yet been parsed (during page loading)
     */
    public int getEndColumnNumber() {
        return endColumnNumber_;
    }

    /**
     * Returns the page that contains this node.
     * @return the page that contains this node
     */
    @SuppressFBWarnings("EI_EXPOSE_REP")
    public SgmlPage getPage() {
        return page_;
    }

    /**
     * Returns the page that contains this node.
     * @return the page that contains this node
     */
    @SuppressFBWarnings("EI_EXPOSE_REP")
    public HtmlPage getHtmlPageOrNull() {
        if (page_ == null || !page_.isHtmlPage()) {
            return null;
        }
        return (HtmlPage) page_;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Document getOwnerDocument() {
        return getPage();
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Sets the JavaScript object that corresponds to this node. This is not guaranteed to be set even if * there is a JavaScript object for this DOM node. * * @param scriptObject the JavaScript object */ public void setScriptableObject(final HtmlUnitScriptable scriptObject) { scriptObject_ = scriptObject; } /** * {@inheritDoc} */ @Override @SuppressFBWarnings("EI_EXPOSE_REP") public DomNode getLastChild() { if (firstChild_ != null) { // last child is stored as the previous sibling of first child return firstChild_.previousSibling_; } return null; } /** * {@inheritDoc} */ @Override @SuppressFBWarnings("EI_EXPOSE_REP") public DomNode getParentNode() { return parent_; } /** * Sets the parent node. * @param parent the parent node */ protected void setParentNode(final DomNode parent) { parent_ = parent; } /** * Returns this node's index within its parent's child nodes (zero-based). * @return this node's index within its parent's child nodes (zero-based) */ public int getIndex() { int index = 0; for (DomNode n = previousSibling_; n != null && n.nextSibling_ != null; n = n.previousSibling_) { index++; } return index; } /** * {@inheritDoc} */ @Override @SuppressFBWarnings("EI_EXPOSE_REP") public DomNode getPreviousSibling() { if (parent_ == null || this == parent_.firstChild_) { // previous sibling of first child points to last child return null; } return previousSibling_; } /** * {@inheritDoc} */ @Override @SuppressFBWarnings("EI_EXPOSE_REP") public DomNode getNextSibling() { return nextSibling_; } /** * {@inheritDoc} */ @Override @SuppressFBWarnings("EI_EXPOSE_REP") public DomNode getFirstChild() { return firstChild_; } /** * Returns {@code true} if this node is an ancestor of the specified node. * * @param node the node to check * @return {@code true} if this node is an ancestor of the specified node */ public boolean isAncestorOf(DomNode node) { while (node != null) { if (node == this) { return true; } node = node.getParentNode(); } return false; } /** * Returns {@code true} if this node is an ancestor of the specified nodes. * * @param nodes the nodes to check * @return {@code true} if this node is an ancestor of the specified nodes */ public boolean isAncestorOfAny(final DomNode... nodes) { for (final DomNode node : nodes) { if (isAncestorOf(node)) { return true; } } return false; } /** * {@inheritDoc} */ @Override public String getNamespaceURI() { return null; } /** * {@inheritDoc} */ @Override public String getLocalName() { return null; } /** * {@inheritDoc} */ @Override public String getPrefix() { return null; } /** * {@inheritDoc} */ @Override public boolean hasChildNodes() { return firstChild_ != null; } /** * {@inheritDoc} */ @Override public DomNodeList getChildNodes() { return new SiblingDomNodeList(this); } /** * {@inheritDoc} * Not yet implemented. */ @Override public boolean isSupported(final String namespace, final String featureName) { throw new UnsupportedOperationException("DomNode.isSupported is not yet implemented."); } /** * {@inheritDoc} */ @Override public void normalize() { for (DomNode child = getFirstChild(); child != null; child = child.getNextSibling()) { if (child instanceof DomText) { final boolean removeChildTextNodes = hasFeature(DOM_NORMALIZE_REMOVE_CHILDREN); final StringBuilder dataBuilder = new StringBuilder(); DomNode toRemove = child; DomText firstText = null; //IE removes all child text nodes, but FF preserves the first while (toRemove instanceof DomText && !(toRemove instanceof DomCDataSection)) { final DomNode nextChild = toRemove.getNextSibling(); dataBuilder.append(toRemove.getTextContent()); if (removeChildTextNodes || firstText != null) { toRemove.remove(); } if (firstText == null) { firstText = (DomText) toRemove; } toRemove = nextChild; } if (firstText != null) { if (removeChildTextNodes) { final DomText newText = new DomText(getPage(), dataBuilder.toString()); insertBefore(newText, toRemove); } else { firstText.setData(dataBuilder.toString()); } } } } } /** * {@inheritDoc} */ @Override public String getBaseURI() { return getPage().getUrl().toExternalForm(); } /** * {@inheritDoc} */ @Override public short compareDocumentPosition(final Node other) { if (other == this) { return 0; // strange, no constant available? } // get ancestors of both final List myAncestors = getAncestors(); final List otherAncestors = ((DomNode) other).getAncestors(); final int max = Math.min(myAncestors.size(), otherAncestors.size()); int i = 1; while (i < max && myAncestors.get(i) == otherAncestors.get(i)) { i++; } if (i != 1 && i == max) { if (myAncestors.size() == max) { return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING; } return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING; } if (max == 1) { if (myAncestors.contains(other)) { return DOCUMENT_POSITION_CONTAINS; } if (otherAncestors.contains(this)) { return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING; } return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC; } // neither contains nor contained by final Node myAncestor = myAncestors.get(i); final Node otherAncestor = otherAncestors.get(i); Node node = myAncestor; while (node != otherAncestor && node != null) { node = node.getPreviousSibling(); } if (node == null) { return DOCUMENT_POSITION_FOLLOWING; } return DOCUMENT_POSITION_PRECEDING; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Gets the ancestors of the node. * @return a list of the ancestors with the root at the first position */ public List getAncestors() { final List list = new ArrayList<>(); list.add(this); Node node = getParentNode(); while (node != null) { list.add(0, node); node = node.getParentNode(); } return list; } /** * {@inheritDoc} */ @Override public String getTextContent() { switch (getNodeType()) { case ELEMENT_NODE: case ATTRIBUTE_NODE: case ENTITY_NODE: case ENTITY_REFERENCE_NODE: case DOCUMENT_FRAGMENT_NODE: final StringBuilder builder = new StringBuilder(); for (final DomNode child : getChildren()) { final short childType = child.getNodeType(); if (childType != COMMENT_NODE && childType != PROCESSING_INSTRUCTION_NODE) { builder.append(child.getTextContent()); } } return builder.toString(); case TEXT_NODE: case CDATA_SECTION_NODE: case COMMENT_NODE: case PROCESSING_INSTRUCTION_NODE: return getNodeValue(); default: return null; } } /** * {@inheritDoc} */ @Override public void setTextContent(final String textContent) { removeAllChildren(); if (textContent != null && !textContent.isEmpty()) { appendChild(new DomText(getPage(), textContent)); } } /** * {@inheritDoc} */ @Override public boolean isSameNode(final Node other) { return other == this; } /** * {@inheritDoc} * Not yet implemented. */ @Override public String lookupPrefix(final String namespaceURI) { throw new UnsupportedOperationException("DomNode.lookupPrefix is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public boolean isDefaultNamespace(final String namespaceURI) { throw new UnsupportedOperationException("DomNode.isDefaultNamespace is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public String lookupNamespaceURI(final String prefix) { throw new UnsupportedOperationException("DomNode.lookupNamespaceURI is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public boolean isEqualNode(final Node arg) { throw new UnsupportedOperationException("DomNode.isEqualNode is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public Object getFeature(final String feature, final String version) { throw new UnsupportedOperationException("DomNode.getFeature is not yet implemented."); } /** * {@inheritDoc} */ @Override public Object getUserData(final String key) { Object value = null; if (userData_ != null) { value = userData_.get(key); } return value; } /** * {@inheritDoc} */ @Override public Object setUserData(final String key, final Object data, final UserDataHandler handler) { if (userData_ == null) { userData_ = new HashMap<>(); } return userData_.put(key, data); } /** * {@inheritDoc} */ @Override public boolean hasAttributes() { return false; } /** * {@inheritDoc} */ @Override public NamedNodeMap getAttributes() { return NamedAttrNodeMapImpl.EMPTY_MAP; } /** *

Returns {@code true} if this node is displayed and can be visible to the user * (ignoring screen size, scrolling limitations, color, font-size, or overlapping nodes).

* *

NOTE: If CSS is * {@link org.htmlunit.WebClientOptions#setCssEnabled(boolean) disabled}, this method * does not take this element's style into consideration!

* * @see CSS2 Visibility * @see CSS2 Display * @see MSDN Documentation * @return {@code true} if the node is visible to the user, {@code false} otherwise * @see #mayBeDisplayed() */ public boolean isDisplayed() { if (!mayBeDisplayed()) { return false; } final Page page = getPage(); final WebWindow window = page.getEnclosingWindow(); final WebClient webClient = window.getWebClient(); if (webClient.getOptions().isCssEnabled()) { // display: iterate top to bottom, because if a parent is display:none, // there's nothing that a child can do to override it final List ancestors = getAncestors(); final ArrayList styles = new ArrayList<>(ancestors.size()); for (final Node node : ancestors) { if (node instanceof HtmlElement) { final HtmlElement elem = (HtmlElement) node; if (elem.isHidden()) { return false; } if (elem instanceof HtmlDialog) { if (!((HtmlDialog) elem).isOpen()) { return false; } } else { final ComputedCssStyleDeclaration style = window.getComputedStyle(elem, null); if (DisplayStyle.NONE.value().equals(style.getDisplay())) { return false; } styles.add(style); } } } // visibility: iterate bottom to top, because children can override // the visibility used by parent nodes for (int i = styles.size() - 1; i >= 0; i--) { final ComputedCssStyleDeclaration style = styles.get(i); final String visibility = style.getStyleAttribute(StyleAttributes.Definition.VISIBILITY, true); if (visibility.length() > 5) { if ("visible".equals(visibility)) { return true; } if ("hidden".equals(visibility) || "collapse".equals(visibility)) { return false; } } } } return true; } /** * Returns {@code true} if nodes of this type can ever be displayed, {@code false} otherwise. Examples of nodes * that can never be displayed are <head>, * <meta>, <script>, etc. * @return {@code true} if nodes of this type can ever be displayed, {@code false} otherwise * @see #isDisplayed() */ public boolean mayBeDisplayed() { return true; } /** * Returns a normalized textual representation of this element that represents * what would be visible to the user if this page was shown in a web browser. * Whitespace is normalized like in the browser and block tags are separated by '\n'. * * @return a normalized textual representation of this element */ public String asNormalizedText() { final HtmlSerializerNormalizedText ser = new HtmlSerializerNormalizedText(); return ser.asText(this); } /** * Returns a textual representation of this element in the same way as * the selenium/WebDriver WebElement#getText() property does.
* see get-element-text and * dfn-bot-dom-getvisibletext * Note: this is different from {@link #asNormalizedText()} * * @return a textual representation of this element that represents what would * be visible to the user if this page was shown in a web browser */ public String getVisibleText() { final HtmlSerializerVisibleText ser = new HtmlSerializerVisibleText(); return ser.asText(this); } /** * Returns a string representation of the XML document from this element and all it's children (recursively). * The charset used is the current page encoding. * @return the XML string */ public String asXml() { Charset charsetName = null; final HtmlPage htmlPage = getHtmlPageOrNull(); if (htmlPage != null) { charsetName = htmlPage.getCharset(); } final StringWriter stringWriter = new StringWriter(); try (PrintWriter printWriter = new PrintWriter(stringWriter)) { if (charsetName != null && this instanceof HtmlHtml) { printWriter.print("\r\n"); } printXml("", printWriter); return stringWriter.toString(); } } /** * Recursively writes the XML data for the node tree starting at node. * * @param indent white space to indent child nodes * @param printWriter writer where child nodes are written */ protected void printXml(final String indent, final PrintWriter printWriter) { printWriter.print(indent); printWriter.print(this); printWriter.print("\r\n"); printChildrenAsXml(indent, printWriter); } /** * Recursively writes the XML data for the node tree starting at node. * * @param indent white space to indent child nodes * @param printWriter writer where child nodes are written */ protected void printChildrenAsXml(final String indent, final PrintWriter printWriter) { DomNode child = getFirstChild(); while (child != null) { child.printXml(indent + " ", printWriter); child = child.getNextSibling(); } } /** * {@inheritDoc} */ @Override public String getNodeValue() { return null; } /** * {@inheritDoc} */ @Override public DomNode cloneNode(final boolean deep) { final DomNode newnode; try { newnode = (DomNode) clone(); } catch (final CloneNotSupportedException e) { throw new IllegalStateException("Clone not supported for node [" + this + "]", e); } newnode.parent_ = null; newnode.nextSibling_ = null; newnode.previousSibling_ = null; newnode.scriptObject_ = null; newnode.firstChild_ = null; newnode.attachedToPage_ = false; // if deep, clone the children too. if (deep) { for (DomNode child = firstChild_; child != null; child = child.nextSibling_) { newnode.appendChild(child.cloneNode(true)); } } return newnode; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* *

Returns the JavaScript object that corresponds to this node, lazily initializing a new one if necessary.

* *

The logic of when and where the JavaScript object is created needs a clean up: functions using * a DOM node's JavaScript object should not have to check if they should create it first.

* * @param the object type * @return the JavaScript object that corresponds to this node */ @SuppressWarnings("unchecked") public T getScriptableObject() { if (scriptObject_ == null) { final SgmlPage page = getPage(); if (this == page) { final StringBuilder msg = new StringBuilder("No script object associated with the Page."); // because this is a strange case we like to provide as much info as possible msg.append(" class: '") .append(page.getClass().getName()) .append('\''); try { msg.append(" url: '") .append(page.getUrl()).append("' content: ") .append(page.getWebResponse().getContentAsString()); } catch (final Exception e) { // ok bad luck with detail msg.append(" no details: '").append(e).append('\''); } throw new IllegalStateException(msg.toString()); } scriptObject_ = page.getScriptableObject().makeScriptableFor(this); } return (T) scriptObject_; } /** * {@inheritDoc} */ @Override public DomNode appendChild(final Node node) { if (node == this) { throw JavaScriptEngine.throwAsScriptRuntimeEx(new Exception("Can not add not to itself " + this)); } final DomNode domNode = (DomNode) node; if (domNode.isAncestorOf(this)) { throw JavaScriptEngine.throwAsScriptRuntimeEx(new Exception("Can not add (grand)parent to itself " + this)); } if (domNode instanceof DomDocumentFragment) { final DomDocumentFragment fragment = (DomDocumentFragment) domNode; for (final DomNode child : fragment.getChildren()) { appendChild(child); } } else { // clean up the new node, in case it is being moved if (domNode.getParentNode() != null) { domNode.detach(); } basicAppend(domNode); fireAddition(domNode); } return domNode; } /** * Appends the specified node to the end of this node's children, assuming the specified * node is clean (doesn't have preexisting relationships to other nodes). * * @param node the node to append to this node's children */ private void basicAppend(final DomNode node) { // try to make the node setup as complete as possible // before the node is reachable node.setPage(getPage()); node.parent_ = this; if (firstChild_ == null) { firstChild_ = node; } else { final DomNode last = getLastChild(); node.previousSibling_ = last; node.nextSibling_ = null; // safety first last.nextSibling_ = node; } firstChild_.previousSibling_ = node; } /** * {@inheritDoc} */ @Override public Node insertBefore(final Node newChild, final Node refChild) { if (newChild instanceof DomDocumentFragment) { final DomDocumentFragment fragment = (DomDocumentFragment) newChild; for (final DomNode child : fragment.getChildren()) { insertBefore(child, refChild); } return newChild; } if (refChild == null) { appendChild(newChild); return newChild; } if (refChild.getParentNode() != this) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Reference node is not a child of this node."); } ((DomNode) refChild).insertBefore((DomNode) newChild); return newChild; } /** * Inserts the specified node as a new child node before this node into the child relationship this node is a * part of. If the specified node is this node, this method is a no-op. * * @param newNode the new node to insert */ public void insertBefore(final DomNode newNode) { if (previousSibling_ == null) { throw new IllegalStateException("Previous sibling for " + this + " is null."); } if (newNode == this) { return; } // clean up the new node, in case it is being moved if (newNode.getParentNode() != null) { newNode.detach(); } basicInsertBefore(newNode); fireAddition(newNode); } /** * Inserts the specified node into this node's parent's children right before this node, assuming the specified * node is clean (doesn't have preexisting relationships to other nodes). * * @param node the node to insert before this node */ private void basicInsertBefore(final DomNode node) { // try to make the node setup as complete as possible // before the node is reachable node.setPage(page_); node.parent_ = parent_; node.previousSibling_ = previousSibling_; node.nextSibling_ = this; if (parent_.firstChild_ == this) { parent_.firstChild_ = node; } else { previousSibling_.nextSibling_ = node; } previousSibling_ = node; } private void fireAddition(final DomNode domNode) { final boolean wasAlreadyAttached = domNode.isAttachedToPage(); domNode.attachedToPage_ = isAttachedToPage(); if (domNode.attachedToPage_) { // trigger events final Page page = getPage(); if (null != page && page.isHtmlPage()) { ((HtmlPage) page).notifyNodeAdded(domNode); } // a node that is already "complete" (ie not being parsed) and not yet attached if (!domNode.isBodyParsed() && !wasAlreadyAttached) { for (final DomNode child : domNode.getDescendants()) { child.attachedToPage_ = true; child.onAllChildrenAddedToPage(true); } domNode.onAllChildrenAddedToPage(true); } } if (this instanceof DomDocumentFragment) { onAddedToDocumentFragment(); } fireNodeAdded(new DomChangeEvent(this, domNode)); } /** * Indicates if the current node is being parsed. This means that the opening tag has already been * parsed but not the body and end tag. */ private boolean isBodyParsed() { return getStartLineNumber() != -1 && getEndLineNumber() == -1; } /** * Recursively sets the new page on the node and its children * @param newPage the new owning page */ private void setPage(final SgmlPage newPage) { if (page_ == newPage) { return; // nothing to do } page_ = newPage; for (final DomNode node : getChildren()) { node.setPage(newPage); } } /** * {@inheritDoc} */ @Override public Node removeChild(final Node child) { if (child.getParentNode() != this) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node."); } ((DomNode) child).remove(); return child; } /** * Removes all of this node's children. */ public void removeAllChildren() { while (getFirstChild() != null) { getFirstChild().remove(); } } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Parses the specified HTML source code, appending the resulting content at the specified target location. * @param source the HTML code extract to parse * @throws IOException in case of error * @throws SAXException in case of error */ public void parseHtmlSnippet(final String source) throws SAXException, IOException { getPage().getWebClient().getPageCreator().getHtmlParser().parseFragment(this, source); } /** * Removes this node from all relationships with other nodes. */ public void remove() { // same as detach for the moment detach(); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Detach this node from all relationships with other nodes. * This is the first step of a move. */ protected void detach() { final DomNode exParent = parent_; basicRemove(); fireRemoval(exParent); } /** * Cuts off all relationships this node has with siblings and parents. */ protected void basicRemove() { if (parent_ != null && parent_.firstChild_ == this) { parent_.firstChild_ = nextSibling_; } else if (previousSibling_ != null && previousSibling_.nextSibling_ == this) { previousSibling_.nextSibling_ = nextSibling_; } if (nextSibling_ != null && nextSibling_.previousSibling_ == this) { nextSibling_.previousSibling_ = previousSibling_; } if (parent_ != null && this == parent_.getLastChild()) { parent_.firstChild_.previousSibling_ = previousSibling_; } nextSibling_ = null; previousSibling_ = null; parent_ = null; attachedToPage_ = false; for (final DomNode descendant : getDescendants()) { descendant.attachedToPage_ = false; } } private void fireRemoval(final DomNode exParent) { final HtmlPage htmlPage = getHtmlPageOrNull(); if (htmlPage != null) { // some actions executed on removal need an intact parent relationship (e.g. for the // DocumentPositionComparator) so we have to restore it temporarily parent_ = exParent; htmlPage.notifyNodeRemoved(this); parent_ = null; } if (exParent != null) { final DomChangeEvent event = new DomChangeEvent(exParent, this); fireNodeDeleted(event); // ask ex-parent to fire event (because we don't have parent now) exParent.fireNodeDeleted(event); } } /** * {@inheritDoc} */ @Override public Node replaceChild(final Node newChild, final Node oldChild) { if (oldChild.getParentNode() != this) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node."); } ((DomNode) oldChild).replace((DomNode) newChild); return oldChild; } /** * Replaces this node with another node. If the specified node is this node, this * method is a no-op. * @param newNode the node to replace this one */ public void replace(final DomNode newNode) { if (newNode != this) { final DomNode exParent = parent_; final DomNode exNextSibling = nextSibling_; remove(); exParent.insertBefore(newNode, exNextSibling); } } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Quietly removes this node and moves its children to the specified destination. "Quietly" means * that no node events are fired. This method is not appropriate for most use cases. It should * only be used in specific cases for HTML parsing hackery. * * @param destination the node to which this node's children should be moved before this node is removed */ public void quietlyRemoveAndMoveChildrenTo(final DomNode destination) { if (destination.getPage() != getPage()) { throw new RuntimeException("Cannot perform quiet move on nodes from different pages."); } for (final DomNode child : getChildren()) { child.basicRemove(); destination.basicAppend(child); } basicRemove(); } /** * Check for insertion errors for a new child node. This is overridden by derived * classes to enforce which types of children are allowed. * * @param newChild the new child node that is being inserted below this node * @throws DOMException HIERARCHY_REQUEST_ERR: Raised if this node is of a type that does * not allow children of the type of the newChild node, or if the node to insert is one of * this node's ancestors or this node itself, or if this node is of type Document and the * DOM application attempts to insert a second DocumentType or Element node. * WRONG_DOCUMENT_ERR: Raised if newChild was created from a different document than the * one that created this node. */ protected void checkChildHierarchy(final Node newChild) throws DOMException { Node parentNode = this; while (parentNode != null) { if (parentNode == newChild) { throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Child node is already a parent."); } parentNode = parentNode.getParentNode(); } final Document thisDocument = getOwnerDocument(); final Document childDocument = newChild.getOwnerDocument(); if (childDocument != thisDocument && childDocument != null) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Child node " + newChild.getNodeName() + " is not in the same Document as this " + getNodeName() + "."); } } /** * Lifecycle method invoked whenever a node is added to a page. Intended to * be overridden by nodes which need to perform custom logic when they are * added to a page. This method is recursive, so if you override it, please * be sure to call super.onAddedToPage(). */ protected void onAddedToPage() { if (firstChild_ != null) { for (final DomNode child : getChildren()) { child.onAddedToPage(); } } } /** * Lifecycle method invoked after a node and all its children have been added to a page, during * parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic * after they and all their child nodes have been processed by the HTML parser. This method is * not recursive, and the default implementation is empty, so there is no need to call * super.onAllChildrenAddedToPage() if you implement this method. * @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no */ public void onAllChildrenAddedToPage(final boolean postponed) { // Empty by default. } /** * Lifecycle method invoked whenever a node is added to a document fragment. Intended to * be overridden by nodes which need to perform custom logic when they are * added to a fragment. This method is recursive, so if you override it, please * be sure to call super.onAddedToDocumentFragment(). */ protected void onAddedToDocumentFragment() { if (firstChild_ != null) { for (final DomNode child : getChildren()) { child.onAddedToDocumentFragment(); } } } /** * @return an {@link Iterable} over the children of this node */ public final Iterable getChildren() { return () -> new ChildIterator(firstChild_); } /** * An iterator over all children of this node. */ protected static class ChildIterator implements Iterator { private DomNode nextNode_; private DomNode currentNode_; public ChildIterator(final DomNode nextNode) { nextNode_ = nextNode; } /** {@inheritDoc} */ @Override public boolean hasNext() { return nextNode_ != null; } /** {@inheritDoc} */ @Override public DomNode next() { if (nextNode_ != null) { currentNode_ = nextNode_; nextNode_ = nextNode_.nextSibling_; return currentNode_; } throw new NoSuchElementException(); } /** {@inheritDoc} */ @Override public void remove() { if (currentNode_ == null) { throw new IllegalStateException(); } currentNode_.remove(); } } /** * Returns an {@link Iterable} that will recursively iterate over all of this node's descendants, * including {@link DomText} elements, {@link DomComment} elements, etc. If you want to iterate * only over {@link HtmlElement} descendants, please use {@link #getHtmlElementDescendants()}. * @return an {@link Iterable} that will recursively iterate over all of this node's descendants */ public final Iterable getDescendants() { return () -> new DescendantElementsIterator<>(DomNode.class); } /** * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement} * descendants. If you want to iterate over all descendants (including {@link DomText} elements, * {@link DomComment} elements, etc.), please use {@link #getDescendants()}. * @return an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement} * descendants * @see #getDomElementDescendants() */ public final Iterable getHtmlElementDescendants() { return () -> new DescendantElementsIterator<>(HtmlElement.class); } /** * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement} * descendants. If you want to iterate over all descendants (including {@link DomText} elements, * {@link DomComment} elements, etc.), please use {@link #getDescendants()}. * @return an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement} * descendants * @see #getHtmlElementDescendants() */ public final Iterable getDomElementDescendants() { return () -> new DescendantElementsIterator<>(DomElement.class); } /** * Iterates over all descendants of a specific type, in document order. * @param the type of nodes over which to iterate */ protected class DescendantElementsIterator implements Iterator { private DomNode currentNode_; private DomNode nextNode_; private final Class type_; /** * Creates a new instance which iterates over the specified node type. * @param type the type of nodes over which to iterate */ public DescendantElementsIterator(final Class type) { type_ = type; nextNode_ = getFirstChildElement(DomNode.this); } /** {@inheritDoc} */ @Override public boolean hasNext() { return nextNode_ != null; } /** {@inheritDoc} */ @Override public T next() { return nextNode(); } /** {@inheritDoc} */ @Override public void remove() { if (currentNode_ == null) { throw new IllegalStateException("Unable to remove current node, because there is no current node."); } final DomNode current = currentNode_; while (nextNode_ != null && current.isAncestorOf(nextNode_)) { next(); } current.remove(); } /** @return the next node, if there is one */ @SuppressWarnings("unchecked") public T nextNode() { currentNode_ = nextNode_; setNextElement(); return (T) currentNode_; } private void setNextElement() { DomNode next = getFirstChildElement(nextNode_); if (next == null) { next = getNextDomSibling(nextNode_); } if (next == null) { next = getNextElementUpwards(nextNode_); } nextNode_ = next; } private DomNode getNextElementUpwards(final DomNode startingNode) { if (startingNode == DomNode.this) { return null; } DomNode parent = startingNode.getParentNode(); while (parent != null && parent != DomNode.this) { DomNode next = parent.getNextSibling(); while (next != null && !isAccepted(next)) { next = next.getNextSibling(); } if (next != null) { return next; } parent = parent.getParentNode(); } return null; } private DomNode getFirstChildElement(final DomNode parent) { DomNode node = parent.getFirstChild(); while (node != null && !isAccepted(node)) { node = node.getNextSibling(); } return node; } /** * Indicates if the node is accepted. If not it won't be explored at all. * @param node the node to test * @return {@code true} if accepted */ protected boolean isAccepted(final DomNode node) { return type_.isAssignableFrom(node.getClass()); } private DomNode getNextDomSibling(final DomNode element) { DomNode node = element.getNextSibling(); while (node != null && !isAccepted(node)) { node = node.getNextSibling(); } return node; } } /** * Returns this node's ready state (IE only). * @return this node's ready state */ public String getReadyState() { return readyState_; } /** * Sets this node's ready state (IE only). * @param state this node's ready state */ public void setReadyState(final String state) { readyState_ = state; } /** * Parses the SelectionNamespaces property into a map of prefix/namespace pairs. * The default namespace (specified by xmlns=) is placed in the map using the * empty string ("") key. * * @param selectionNS the value of the SelectionNamespaces property * @return map of prefix/namespace value pairs */ private static Map parseSelectionNamespaces(final String selectionNS) { final Map result = new HashMap<>(); final String[] toks = StringUtils.splitAtJavaWhitespace(selectionNS); for (final String tok : toks) { if (tok.startsWith("xmlns=")) { result.put("", tok.substring(7, tok.length() - 7)); } else if (tok.startsWith("xmlns:")) { final String[] prefix = tok.substring(6).split("="); result.put(prefix[0], prefix[1].substring(1, prefix[1].length() - 1)); } } return result.isEmpty() ? null : result; } /** * Evaluates the specified XPath expression from this node, returning the matching elements. *
* Note: This implies that the ',' point to this node but the general axis like '//' are still * looking at the whole document. E.g. if you like to get all child h1 nodes from the current one * you have to use './/h1' instead of '//h1' because the latter matches all h1 nodes of the# * whole document. * * @param the expected type * @param xpathExpr the XPath expression to evaluate * @return the elements which match the specified XPath expression * @see #getFirstByXPath(String) * @see #getCanonicalXPath() */ public List getByXPath(final String xpathExpr) { // strange feature of the old IE XML support if (!hasFeature(XPATH_SELECTION_NAMESPACES)) { return XPathHelper.getByXPath(this, xpathExpr, null); } // See if the document has the SelectionNamespaces property defined. If so, // create a PrefixResolver that resolves the defined namespaces. PrefixResolver prefixResolver = null; final Document doc = getOwnerDocument(); if (doc instanceof XmlPage) { final HtmlUnitScriptable scriptable = ((XmlPage) doc).getScriptableObject(); if (scriptable instanceof XMLDOMDocument) { final String selectionNS = ((XMLDOMDocument) scriptable).getProperty("SelectionNamespaces"); if (selectionNS != null && !selectionNS.isEmpty()) { final Map namespaces = parseSelectionNamespaces(selectionNS.toString()); if (namespaces != null) { prefixResolver = new PrefixResolver() { @Override public String getNamespaceForPrefix(final String prefix) { return namespaces.get(prefix); } @Override public String getNamespaceForPrefix(final String prefix, final Node node) { throw new UnsupportedOperationException(); } @Override public boolean handlesNullPrefixes() { return false; } }; } } } } return XPathHelper.getByXPath(this, xpathExpr, prefixResolver); } /** * Evaluates the specified XPath expression from this node, returning the matching elements. * * @param xpathExpr the XPath expression to evaluate * @param resolver the prefix resolver to use for resolving namespace prefixes, or null * @return the elements which match the specified XPath expression * @see #getFirstByXPath(String) * @see #getCanonicalXPath() */ public List getByXPath(final String xpathExpr, final PrefixResolver resolver) { return XPathHelper.getByXPath(this, xpathExpr, resolver); } /** * Evaluates the specified XPath expression from this node, returning the first matching element, * or {@code null} if no node matches the specified XPath expression. * * @param xpathExpr the XPath expression * @param the expression type * @return the first element matching the specified XPath expression * @see #getByXPath(String) * @see #getCanonicalXPath() */ public X getFirstByXPath(final String xpathExpr) { return getFirstByXPath(xpathExpr, null); } /** * Evaluates the specified XPath expression from this node, returning the first matching element, * or {@code null} if no node matches the specified XPath expression. * * @param xpathExpr the XPath expression * @param the expression type * @param resolver the prefix resolver to use for resolving namespace prefixes, or null * @return the first element matching the specified XPath expression * @see #getByXPath(String) * @see #getCanonicalXPath() */ @SuppressWarnings("unchecked") public X getFirstByXPath(final String xpathExpr, final PrefixResolver resolver) { final List results = getByXPath(xpathExpr, resolver); if (results.isEmpty()) { return null; } return (X) results.get(0); } /** *

Returns the canonical XPath expression which identifies this node, for instance * "/html/body/table[3]/tbody/tr[5]/td[2]/span/a[3]".

* *

WARNING: This sort of automated XPath expression * is often quite bad at identifying a node, as it is highly sensitive to changes in * the DOM tree.

* * @return the canonical XPath expression which identifies this node * @see #getByXPath(String) */ public String getCanonicalXPath() { throw new RuntimeException("Method getCanonicalXPath() not implemented for nodes of type " + getNodeType()); } /** * Notifies the registered {@link IncorrectnessListener} of something that is not fully correct. * @param message the notification to send to the registered {@link IncorrectnessListener} */ protected void notifyIncorrectness(final String message) { final WebClient client = getPage().getEnclosingWindow().getWebClient(); final IncorrectnessListener incorrectnessListener = client.getIncorrectnessListener(); incorrectnessListener.notify(message, this); } /** * Adds a {@link DomChangeListener} to the listener list. The listener is registered for * all descendants of this node. * * @param listener the DOM structure change listener to be added * @see #removeDomChangeListener(DomChangeListener) */ public void addDomChangeListener(final DomChangeListener listener) { WebAssert.notNull("listener", listener); synchronized (this) { if (domListeners_ == null) { domListeners_ = new ArrayList<>(); } domListeners_.add(listener); } } /** * Removes a {@link DomChangeListener} from the listener list. The listener is deregistered for * all descendants of this node. * * @param listener the DOM structure change listener to be removed * @see #addDomChangeListener(DomChangeListener) */ public void removeDomChangeListener(final DomChangeListener listener) { WebAssert.notNull("listener", listener); synchronized (this) { if (domListeners_ != null) { domListeners_.remove(listener); } } } /** * Support for reporting DOM changes. This method can be called when a node has been added, and it * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s. * *

Note that this method recursively calls this node's parent's {@link #fireNodeAdded(DomChangeEvent)}.

* * @param event the DomChangeEvent to be propagated */ protected void fireNodeAdded(final DomChangeEvent event) { DomNode toInform = this; while (toInform != null) { final List listeners = toInform.safeGetDomListeners(); if (listeners != null) { // iterate by index and safe on an iterator copy for (int i = 0; i < listeners.size(); i++) { listeners.get(i).nodeAdded(event); } } toInform = toInform.getParentNode(); } } /** * Adds a {@link CharacterDataChangeListener} to the listener list. The listener is registered for * all descendants of this node. * * @param listener the character data change listener to be added * @see #removeCharacterDataChangeListener(CharacterDataChangeListener) */ public void addCharacterDataChangeListener(final CharacterDataChangeListener listener) { WebAssert.notNull("listener", listener); synchronized (this) { if (characterDataListeners_ == null) { characterDataListeners_ = new ArrayList<>(); } characterDataListeners_.add(listener); } } /** * Removes a {@link CharacterDataChangeListener} from the listener list. The listener is deregistered for * all descendants of this node. * * @param listener the Character Data change listener to be removed * @see #addCharacterDataChangeListener(CharacterDataChangeListener) */ public void removeCharacterDataChangeListener(final CharacterDataChangeListener listener) { WebAssert.notNull("listener", listener); synchronized (this) { if (characterDataListeners_ != null) { characterDataListeners_.remove(listener); } } } /** * Support for reporting Character Data changes. * *

Note that this method recursively calls this node's parent's {@link #fireCharacterDataChanged}.

* * @param event the CharacterDataChangeEvent to be propagated */ protected void fireCharacterDataChanged(final CharacterDataChangeEvent event) { DomNode toInform = this; while (toInform != null) { final List listeners = toInform.safeGetCharacterDataListeners(); if (listeners != null) { // iterate by index and safe on an iterator copy for (int i = 0; i < listeners.size(); i++) { listeners.get(i).characterDataChanged(event); } } toInform = toInform.getParentNode(); } } /** * Support for reporting DOM changes. This method can be called when a node has been deleted, and it * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s. * *

Note that this method recursively calls this node's parent's {@link #fireNodeDeleted(DomChangeEvent)}.

* * @param event the DomChangeEvent to be propagated */ protected void fireNodeDeleted(final DomChangeEvent event) { DomNode toInform = this; while (toInform != null) { final List listeners = toInform.safeGetDomListeners(); if (listeners != null) { // iterate by index and safe on an iterator copy for (int i = 0; i < listeners.size(); i++) { listeners.get(i).nodeDeleted(event); } } toInform = toInform.getParentNode(); } } private List safeGetDomListeners() { synchronized (this) { return domListeners_ == null ? null : new ArrayList<>(domListeners_); } } private List safeGetCharacterDataListeners() { synchronized (this) { return characterDataListeners_ == null ? null : new ArrayList<>(characterDataListeners_); } } /** * Retrieves all element nodes from descendants of the starting element node that match any selector * within the supplied selector strings. * @param selectors one or more CSS selectors separated by commas * @return list of all found nodes */ public DomNodeList querySelectorAll(final String selectors) { try { final WebClient webClient = getPage().getWebClient(); final SelectorList selectorList = getSelectorList(selectors, webClient); final List elements = new ArrayList<>(); if (selectorList != null) { for (final DomElement child : getDomElementDescendants()) { for (final Selector selector : selectorList) { if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, child, null, true, true)) { elements.add(child); break; } } } } return new StaticDomNodeList(elements); } catch (final IOException e) { throw new CSSException("Error parsing CSS selectors from '" + selectors + "': " + e.getMessage()); } } /** * Returns the {@link SelectorList}. * @param selectors the selectors * @param webClient the {@link WebClient} * @return the {@link SelectorList} * @throws IOException if an error occurs */ protected SelectorList getSelectorList(final String selectors, final WebClient webClient) throws IOException { // get us a CSS3Parser from the pool so the chance of reusing it are high try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) { final CSSOMParser parser = new CSSOMParser(pooledParser); final CheckErrorHandler errorHandler = new CheckErrorHandler(); parser.setErrorHandler(errorHandler); final SelectorList selectorList = parser.parseSelectors(selectors); // in case of error parseSelectors returns null if (errorHandler.errorDetected()) { throw new CSSException("Invalid selectors: '" + selectors + "'"); } if (selectorList != null) { int documentMode = 9; if (webClient.getBrowserVersion().hasFeature(QUERYSELECTORALL_NOT_IN_QUIRKS)) { final HtmlUnitScriptable sobj = getPage().getScriptableObject(); if (sobj instanceof HTMLDocument) { documentMode = ((HTMLDocument) sobj).getDocumentMode(); } } CssStyleSheet.validateSelectors(selectorList, documentMode, this); } return selectorList; } } /** * Returns the first element within the document that matches the specified group of selectors. * @param selectors one or more CSS selectors separated by commas * @param the node type * @return null if no matches are found; otherwise, it returns the first matching element */ @SuppressWarnings("unchecked") public N querySelector(final String selectors) { final DomNodeList list = querySelectorAll(selectors); if (!list.isEmpty()) { return (N) list.get(0); } return null; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Indicates if this node is currently attached to the page. * @return {@code true} if the page is one ancestor of the node. */ public boolean isAttachedToPage() { return attachedToPage_; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Lifecycle method to support special processing for js method importNode. * @param doc the import target document * @see org.htmlunit.javascript.host.dom.Document#importNode( * org.htmlunit.javascript.host.dom.Node, boolean) * @see HtmlScript#processImportNode(org.htmlunit.javascript.host.dom.Document) */ public void processImportNode(final org.htmlunit.javascript.host.dom.Document doc) { page_ = (SgmlPage) doc.getDomNodeOrDie(); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Helper for a common call sequence. * @param feature the feature to check * @return {@code true} if the currently emulated browser has this feature. */ public boolean hasFeature(final BrowserVersionFeatures feature) { return getPage().getWebClient().getBrowserVersion().hasFeature(feature); } private static final class CheckErrorHandler implements CSSErrorHandler { private boolean errorDetected_; private CheckErrorHandler() { errorDetected_ = false; } boolean errorDetected() { return errorDetected_; } @Override public void warning(final CSSParseException exception) throws CSSException { // ignore } @Override public void fatalError(final CSSParseException exception) throws CSSException { errorDetected_ = true; } @Override public void error(final CSSParseException exception) throws CSSException { errorDetected_ = true; } } /** * Indicates if the provided event can be applied to this node. * Overwrite this. * @param event the event * @return {@code false} if the event can't be applied */ public boolean handles(final Event event) { return true; } /** * Returns the previous sibling element node of this element. * null if this element has no element sibling nodes that come before this one in the document tree. * @return the previous sibling element node of this element. * null if this element has no element sibling nodes that come before this one in the document tree */ public DomElement getPreviousElementSibling() { DomNode node = getPreviousSibling(); while (node != null && !(node instanceof DomElement)) { node = node.getPreviousSibling(); } return (DomElement) node; } /** * Returns the next sibling element node of this element. * null if this element has no element sibling nodes that come after this one in the document tree. * @return the next sibling element node of this element. * null if this element has no element sibling nodes that come after this one in the document tree */ public DomElement getNextElementSibling() { DomNode node = getNextSibling(); while (node != null && !(node instanceof DomElement)) { node = node.getNextSibling(); } return (DomElement) node; } /** * @param selectorString the selector to test * @return the selected {@link DomElement} or null. */ public DomElement closest(final String selectorString) { try { final WebClient webClient = getPage().getWebClient(); final SelectorList selectorList = getSelectorList(selectorString, webClient); DomNode current = this; if (selectorList != null) { do { for (final Selector selector : selectorList) { final DomElement elem = (DomElement) current; if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, elem, null, true, true)) { return elem; } } do { current = current.getParentNode(); } while (current != null && !(current instanceof DomElement)); } while (current != null); } return null; } catch (final IOException e) { throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy