/* * Copyright (c) 2002-2008 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.html; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.mozilla.javascript.ScriptableObject; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.UserDataHandler; import com.gargoylesoftware.htmlunit.IncorrectnessListener; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebAssert; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.xpath.XPathUtils; import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable; import com.gargoylesoftware.htmlunit.javascript.host.CSSStyleDeclaration; import com.gargoylesoftware.htmlunit.javascript.host.HTMLElement; /** * Base class for nodes in the HTML DOM tree. This class is modeled after the * W3C DOM specification, but does not implement it. * * @version $Revision$ * @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 */ 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 owning page of this node. */ private Page 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 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 transient ScriptableObject scriptObject_; /** The ready state is is an IE-only value that is available to a large number of elements. */ private String readyState_; /** The name of the "element" property. Used when watching property change events. */ public static final String PROPERTY_ELEMENT = "element"; /** * 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 List domListeners_; private final transient Object domListeners_lock_ = new Object(); /** * Never call this, used for Serialization. * @deprecated */ @Deprecated protected DomNode() { this(null); } /** * Creates a new instance. * @param page the page which contains this node */ protected DomNode(final Page 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 */ 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 */ 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 */ public Page getPage() { return page_; } /** * {@inheritDoc} */ public Document getOwnerDocument() { return (Document) 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 setScriptObject(final ScriptableObject scriptObject) { scriptObject_ = scriptObject; } /** * {@inheritDoc} */ public DomNode getLastChild() { if (firstChild_ != null) { // last child is stored as the previous sibling of first child return firstChild_.previousSibling_; } return null; } /** * Returns this node's last child node, or null if this node doesn't have any children. * @return this node's last child node, or null if this node doesn't have any children * @deprecated As of 2.0, please use {@link #getLastChild()} instead. */ @Deprecated public DomNode getLastDomChild() { return getLastChild(); } /** * {@inheritDoc} */ public DomNode getParentNode() { return parent_; } /** * Returns this node's parent node, or null if this is the root node. * @return this node's parent node, or null if this is the root node * @deprecated As of 2.0, please use {@link #getParentNode()} instead. */ @Deprecated public DomNode getParentDomNode() { return getParentNode(); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* Sets the parent node. * @param parent the parent node */ public void setParentNode(final DomNode parent) { parent_ = parent; } /** * {@inheritDoc} */ public DomNode getPreviousSibling() { if (parent_ == null || this == parent_.firstChild_) { // previous sibling of first child points to last child return null; } return previousSibling_; } /** * Returns this node's previous sibling, or null if this node is its parent's first child. * @return this node's previous sibling, or null if this node is its parent's first child * @deprecated As of 2.0, please use {@link #getPreviousSibling()} instead. */ @Deprecated public DomNode getPreviousDomSibling() { return getPreviousSibling(); } /** * {@inheritDoc} */ public DomNode getNextSibling() { return nextSibling_; } /** * Returns this node's next sibling node, or null if this node is its parent's last child. * @return this node's next sibling node, or null if this node is its parent's last child * @deprecated As of 2.0, please use {@link #getNextSibling()} instead. */ @Deprecated public DomNode getNextDomSibling() { return getNextSibling(); } /** * {@inheritDoc} */ public DomNode getFirstChild() { return firstChild_; } /** * Returns this node's first child node, or null if this node does not have any children. * @return this node's first child node, or null if this node does not have any children * @deprecated As of 2.0, please use {@link #getFirstChild()} instead. */ @Deprecated public DomNode getFirstDomChild() { return getFirstChild(); } /** * Returns true if this node is an ancestor of the specified node. * * @param node the node to check * @return 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; } /** @param previous set the previousSibling field value */ protected void setPreviousSibling(final DomNode previous) { previousSibling_ = previous; } /** @param next set the nextSibling field value */ protected void setNextSibling(final DomNode next) { nextSibling_ = next; } /** * Returns this node's node type. * @return this node's node type */ public abstract short getNodeType(); /** * Returns this node's node name. * @return this node's node name */ public abstract String getNodeName(); /** * {@inheritDoc} */ public String getNamespaceURI() { return null; } /** * {@inheritDoc} */ public String getLocalName() { return null; } /** * {@inheritDoc} */ public String getPrefix() { return null; } /** * {@inheritDoc} */ public void setPrefix(final String prefix) { // Empty. } /** * {@inheritDoc} */ public boolean hasChildNodes() { return firstChild_ != null; } /** * {@inheritDoc} */ public NodeList getChildNodes() { return new DomNodeList(); } private class DomNodeList implements NodeList { public int getLength() { int length = 0; for (Node node = firstChild_; node != null; node = node.getNextSibling()) { length++; } return length; } public Node item(final int index) { int i = 0; for (Node node = firstChild_; node != null; node = node.getNextSibling()) { if (i == index) { return node; } i++; } return null; } } /** * {@inheritDoc} * Not yet implemented. */ public boolean isSupported(final String namespace, final String featureName) { throw new UnsupportedOperationException("DomNode.isSupported is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public void normalize() { throw new UnsupportedOperationException("DomNode.normalize is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public String getBaseURI() { throw new UnsupportedOperationException("DomNode.getBaseURI is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public short compareDocumentPosition(final Node other) { throw new UnsupportedOperationException("DomNode.compareDocumentPosition is not yet implemented."); } /** * {@inheritDoc} */ 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} */ public void setTextContent(final String textContent) { removeAllChildren(); appendChild(new DomText(getPage(), textContent)); } /** * {@inheritDoc} */ public boolean isSameNode(final Node other) { return other == this; } /** * {@inheritDoc} * Not yet implemented. */ public String lookupPrefix(final String namespaceURI) { throw new UnsupportedOperationException("DomNode.lookupPrefix is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public boolean isDefaultNamespace(final String namespaceURI) { throw new UnsupportedOperationException("DomNode.isDefaultNamespace is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public String lookupNamespaceURI(final String prefix) { throw new UnsupportedOperationException("DomNode.lookupNamespaceURI is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public boolean isEqualNode(final Node arg) { throw new UnsupportedOperationException("DomNode.isEqualNode is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public Object getFeature(final String feature, final String version) { throw new UnsupportedOperationException("DomNode.getFeature is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public Object getUserData(final String key) { throw new UnsupportedOperationException("DomNode.getUserData is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public Object setUserData(final String key, final Object data, final UserDataHandler handler) { throw new UnsupportedOperationException("DomNode.setUserData is not yet implemented."); } /** * {@inheritDoc} */ public boolean hasAttributes() { return false; } /** * Returns a flag indicating whether or not this node itself results in any space taken up * in browser windows; for instance, "<b>" affects the specified text, but does not * use up any space itself. * * @return a flag indicating whether or not this node itself results in any space taken up * in browser windows */ protected boolean isRenderedVisible() { return false; } /** * Returns a flag indicating whether or not this node should have any leading and trailing * whitespace removed when {@link #asText()} is called. This method should usually return * true, but must return false for such things as text formatting tags. * * @return a flag indicating whether or not this node should have any leading and trailing * whitespace removed when {@link #asText()} is called */ protected boolean isTrimmedText() { return true; } /** * Returns true if the node has or inherits style attribute with visibility:visible. * Node is visible if visibility is not specified or inherited. Child element can override * the parent visibility. * @see CSS2 * @see MSDN Documentation * @return true if the node is visible. */ protected boolean isStyleVisible() { boolean isVisible = true; final Page page = getPage(); if (page instanceof HtmlPage) { final boolean isNotIE = !((HtmlPage) page).getWebClient().getBrowserVersion().isIE(); DomNode node = this; do { final ScriptableObject scriptableObject = node.getScriptObject(); if (scriptableObject instanceof HTMLElement) { final CSSStyleDeclaration style = ((HTMLElement) scriptableObject).jsxGet_style(); final String visibility = style.jsxGet_visibility(); if (visibility.length() > 0) { if (visibility.equals("visible")) { isVisible = true; break; } else if (visibility.equals("hidden") || (isNotIE && visibility.equals("collapse"))) { isVisible = false; break; } } } node = node.getParentNode(); } while(node != null); } return isVisible; } /** * Returns a textual representation of this element that represents what would * be visible to the user if this page was shown in a web browser. For example, * a single-selection select element would return the currently selected value * as text. * * @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 asText() { String text = getChildrenAsText(); text = reduceWhitespace(text); if (isTrimmedText()) { text = text.trim(); } return text; } /** * Returns a text string that represents all the child elements as they would be visible in a web browser. * @return a text string that represents all the child elements as they would be visible in a web browser * @see #asText() */ protected final String getChildrenAsText() { final StringBuilder buffer = new StringBuilder(); final StringBuilder textBuffer = new StringBuilder(); boolean previousNodeWasText = false; for (final DomNode node : getChildren()) { if (node instanceof DomText) { if (node.isStyleVisible()) { textBuffer.append(((DomText) node).getData()); previousNodeWasText = true; } } else { if (previousNodeWasText) { // Whitespace between adjacent text nodes should remain as a single // space. So, append raw adjacent text and reduce it as a whole. buffer.append(reduceWhitespace(textBuffer.toString())); textBuffer.setLength(0); previousNodeWasText = false; } if (node.isRenderedVisible()) { buffer.append(" "); buffer.append(node.asText()); buffer.append(" "); } else if (node.getNodeName().equals("p")) { // this is a bit kludgey, but we can't add the space // inside the node's asText(), since it doesn't belong // with the contents of the 'p' tag buffer.append(" "); buffer.append(node.asText()); } else { buffer.append(node.asText()); } } } if (previousNodeWasText) { // we ended with text buffer.append(textBuffer.toString()); } return buffer.toString(); } /** * Removes extra whitespace from a string, similar to what a browser does when it displays text. * @param text the text to clean up * @return the clean text */ protected static String reduceWhitespace(final String text) { final StringBuilder buffer = new StringBuilder(text.length()); boolean whitespace = false; for (final char ch : text.toCharArray()) { // Translate non-breaking space to regular space. if (ch == (char) 160) { buffer.append(' '); whitespace = false; } else { if (whitespace) { if (!Character.isWhitespace(ch)) { buffer.append(ch); whitespace = false; } } else { if (Character.isWhitespace(ch)) { whitespace = true; buffer.append(' '); } else { buffer.append(ch); } } } } return buffer.toString(); } /** * Returns the log object for this element. * @return the log object for this element * @deprecated As of 2.0, use local log variables enclosed in a conditional block. */ @Deprecated protected final Log getLog() { return LogFactory.getLog(getClass()); } /** * 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() { String charsetName = null; if (getPage() instanceof HtmlPage) { charsetName = ((HtmlPage) getPage()).getPageEncoding(); } final StringWriter stringWriter = new StringWriter(); final PrintWriter printWriter = new PrintWriter(stringWriter); if (charsetName != null && this instanceof HtmlHtml) { printWriter.println(""); } printXml("", printWriter); printWriter.close(); 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.println(indent + this); 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} */ public String getNodeValue() { return null; } /** * {@inheritDoc} */ public void setNodeValue(final String x) { // Default behavior is to do nothing, overridden in some subclasses } /** * {@inheritDoc} */ 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 + "]"); } newnode.parent_ = null; newnode.nextSibling_ = null; newnode.previousSibling_ = null; newnode.firstChild_ = null; newnode.scriptObject_ = null; // if deep, clone the kids too. if (deep) { for (DomNode child = firstChild_; child != null; child = child.nextSibling_) { newnode.appendChild(child.cloneNode(true)); } } return newnode; } /** * Makes a clone of this node. * * @param deep if true, the clone will be propagated to the whole subtree * below this one. Otherwise, the new node will not have any children. The page reference * will always be the same as this node's. * @return a new node * @deprecated As of 2.0, please use {@link #cloneNode(boolean)} instead. */ @Deprecated public DomNode cloneDomNode(final boolean deep) { return cloneNode(deep); } /** * 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. * * @return the JavaScript object that corresponds to this node */ public ScriptableObject getScriptObject() { if (scriptObject_ == null) { if (this == getPage()) { throw new IllegalStateException("No script object associated with the Page"); } scriptObject_ = ((SimpleScriptable) ((DomNode) page_).getScriptObject()).makeScriptableFor(this); } return scriptObject_; } /** * {@inheritDoc} */ public DomNode appendChild(final Node node) { final DomNode domNode = (DomNode) node; 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 != this && domNode.getParentNode() != null) { domNode.remove(); } // move the node basicAppend(domNode); if (domNode.getStartLineNumber() == -1) { // dynamically added node, not parsed domNode.onAddedToPage(); domNode.onAllChildrenAddedToPage(); } // trigger events if (!(this instanceof DomDocumentFragment) && (getPage() instanceof HtmlPage)) { ((HtmlPage) getPage()).notifyNodeAdded(domNode); } fireNodeAdded(this, domNode); } return domNode; } /** * Appends a child node to this node. * @param node the node to append * @return the node added * @deprecated As of 2.0, please use {@link #appendChild(Node)} instead. */ @Deprecated public DomNode appendDomChild(final DomNode node) { return appendChild(node); } /** * 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 */ void quietlyRemoveAndMoveChildrenTo(final DomNode destination) { if (destination.getPage() != getPage()) { throw new RuntimeException("Cannot perform quiet move on nodes from different pages."); } for (DomNode child : getChildren()) { child.basicRemove(); destination.basicAppend(child); } basicRemove(); } /** * 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) { node.setPage(getPage()); if (firstChild_ == null) { firstChild_ = node; firstChild_.previousSibling_ = node; } else { final DomNode last = getLastChild(); last.nextSibling_ = node; node.previousSibling_ = last; node.nextSibling_ = null; // safety first firstChild_.previousSibling_ = node; // new last node } node.parent_ = this; } /** * 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() + "."); } } /** * {@inheritDoc} */ public Node insertBefore(final Node newChild, final Node refChild) { if (refChild == null) { appendChild(newChild); } else { 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 null; } /** * Inserts 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 * @throws IllegalStateException if this node is not a child of any other node */ public void insertBefore(final DomNode newNode) throws IllegalStateException { 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 final DomNode exParent = newNode.getParentNode(); newNode.basicRemove(); if (parent_.firstChild_ == this) { parent_.firstChild_ = newNode; } else { previousSibling_.nextSibling_ = newNode; } newNode.previousSibling_ = previousSibling_; newNode.nextSibling_ = this; previousSibling_ = newNode; newNode.parent_ = parent_; newNode.setPage(page_); if (getPage() instanceof HtmlPage) { ((HtmlPage) getPage()).notifyNodeAdded(newNode); } fireNodeAdded(this, newNode); if (exParent != null) { fireNodeDeleted(exParent, newNode); exParent.fireNodeDeleted(exParent, this); } } /** * Recursively sets the new page on the node and its children * @param newPage the new owning page */ private void setPage(final Page newPage) { if (page_ == newPage) { return; // nothing to do } page_ = newPage; for (final DomNode node : getChildren()) { node.setPage(newPage); } } /** * {@inheritDoc} */ public NamedNodeMap getAttributes() { return com.gargoylesoftware.htmlunit.javascript.NamedNodeMap.EMPTY_NODE_MAP; } /** * {@inheritDoc} */ 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 this node from all relationships with other nodes. */ public void remove() { final DomNode exParent = parent_; basicRemove(); if (getPage() instanceof HtmlPage) { ((HtmlPage) getPage()).notifyNodeRemoved(this); } if (exParent != null) { fireNodeDeleted(exParent, this); //ask ex-parent to fire event (because we don't have parent now) exParent.fireNodeDeleted(exParent, this); } } /** * Cuts off all relationships this node has with siblings and parents. */ private 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; } /** * {@inheritDoc} */ 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 * @throws IllegalStateException if this node is not a child of any other node */ public void replace(final DomNode newNode) throws IllegalStateException { if (newNode != this) { newNode.remove(); insertBefore(newNode); remove(); } } /** * 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. */ protected void onAllChildrenAddedToPage() { // Empty by default. } /** * @return an Iterable over the children of this node */ public final Iterable getChildren() { return new Iterable() { public Iterator iterator() { return new ChildIterator(); } }; } /** * @return an iterator over the children of this node * @deprecated As of 2.0, use {@link #getChildren()}. */ @Deprecated public Iterator getChildIterator() { return new ChildIterator(); } /** * An iterator over all children of this node. */ protected class ChildIterator implements Iterator { private DomNode nextNode_ = firstChild_; private DomNode currentNode_ = null; /** @return whether there is a next object */ public boolean hasNext() { return nextNode_ != null; } /** @return the next object */ public DomNode next() { if (nextNode_ != null) { currentNode_ = nextNode_; nextNode_ = nextNode_.nextSibling_; return currentNode_; } throw new NoSuchElementException(); } /** Removes the current object. */ 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. * @param the sub-element * @return an {@link Iterable} that will recursively iterate over all of this node's descendants */ public final Iterable getAllHtmlChildElements() { return new Iterable() { public Iterator iterator() { return new DescendantElementsIterator(); } }; } /** * An iterator over all HtmlElement descendants in document order. */ protected class DescendantElementsIterator implements Iterator { private X nextElement_ = getFirstChildElement(DomNode.this); /** @return is there a next one? */ public boolean hasNext() { return nextElement_ != null; } /** @return the next one */ public X next() { return nextElement(); } /** @throws UnsupportedOperationException always */ public void remove() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** @return is there a next one? */ public X nextElement() { final X result = nextElement_; setNextElement(); return result; } private void setNextElement() { X next = getFirstChildElement(nextElement_); if (next == null) { next = getNextDomSibling(nextElement_); } if (next == null) { next = getNextElementUpwards(nextElement_); } nextElement_ = next; } private X getNextElementUpwards(final DomNode startingNode) { if (startingNode == DomNode.this) { return null; } final DomNode parent = startingNode.getParentNode(); if (parent == DomNode.this) { return null; } DomNode next = parent.getNextSibling(); while (next != null && next instanceof HtmlElement == false) { next = next.getNextSibling(); } if (next == null) { return getNextElementUpwards(parent); } return (X) next; } private X getFirstChildElement(final DomNode parent) { if (parent instanceof HtmlNoScript && getPage().getEnclosingWindow().getWebClient().isJavaScriptEnabled()) { return null; } DomNode node = parent.getFirstChild(); while (node != null && node instanceof HtmlElement == false) { node = node.getNextSibling(); } return (X) node; } private X getNextDomSibling(final HtmlElement element) { DomNode node = element.getNextSibling(); while (node != null && node instanceof HtmlElement == false) { node = node.getNextSibling(); } return (X) 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; } /** * Removes all of this node's children. */ public void removeAllChildren() { if (getFirstChild() == null) { return; } for (final Iterator it = getChildren().iterator(); it.hasNext();) { it.next().removeAllChildren(); it.remove(); } } /** * Evaluates the specified XPath expression from this node, returning the matching elements. * * @param xpathExpr the XPath expression to evaluate * @return the elements which match the specified XPath expression * @see #getFirstByXPath(String) * @see #getCanonicalXPath() */ public List< ? extends Object> getByXPath(final String xpathExpr) { return XPathUtils.getByXPath(this, xpathExpr); } /** * Evaluates the specified XPath expression from this node, returning the first matching element, * or null if no node matches the specified XPath expression. * * @param xpathExpr the XPath expression * @return the first element matching the specified XPath expression * @see #getByXPath(String) * @see #getCanonicalXPath() */ public Object getFirstByXPath(final String xpathExpr) { final List< ? > results = getByXPath(xpathExpr); if (results.isEmpty()) { return null; } return 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() { final DomNode parent = getParentNode(); if (parent == null) { return ""; } return parent.getCanonicalXPath() + '/' + getXPathToken(); } /** * Returns the XPath token for this node only. */ private String getXPathToken() { final DomNode parent = getParentNode(); int total = 0; int nodeIndex = 0; for (final DomNode child : parent.getChildren()) { if (child.getNodeName().equals(getNodeName())) { total++; } if (child == this) { nodeIndex = total; } } if (nodeIndex == 1 && total == 1) { return getNodeName(); } return getNodeName() + '[' + nodeIndex + ']'; } /** * 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 (domListeners_lock_) { if (domListeners_ == null) { domListeners_ = new ArrayList(); } if (!domListeners_.contains(listener)) { 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 (domListeners_lock_) { 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(DomNode, DomNode)}. * * @param parentNode the parent of the node that was added * @param addedNode the node that was added */ protected void fireNodeAdded(final DomNode parentNode, final DomNode addedNode) { final List listeners = safeGetDomListeners(); if (listeners != null) { final DomChangeEvent event = new DomChangeEvent(parentNode, addedNode); for (final DomChangeListener listener : listeners) { listener.nodeAdded(event); } } if (parent_ != null) { parent_.fireNodeAdded(parentNode, addedNode); } } /** * 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(DomNode, DomNode)}. * * @param parentNode the parent of the node that was deleted * @param deletedNode the node that was deleted */ protected void fireNodeDeleted(final DomNode parentNode, final DomNode deletedNode) { final List listeners = safeGetDomListeners(); if (listeners != null) { final DomChangeEvent event = new DomChangeEvent(parentNode, deletedNode); for (final DomChangeListener listener : listeners) { listener.nodeDeleted(event); } } if (parent_ != null) { parent_.fireNodeDeleted(parentNode, deletedNode); } } private List safeGetDomListeners() { synchronized (domListeners_lock_) { if (domListeners_ != null) { return new ArrayList(domListeners_); } return null; } } /** * Custom serialization logic which ensures that {@link NonSerializable} {@link DomChangeListener}s * are not serialized. * @param stream the stream to write the object to * @throws IOException if an error occurs during writing */ private void writeObject(final ObjectOutputStream stream) throws IOException { synchronized (domListeners_lock_) { // Store the original list of listeners in a temporary variable, and then // modify the listener list by removing any NonSerializable listeners. final List temp; if (domListeners_ != null) { temp = new ArrayList(domListeners_); for (final Iterator i = domListeners_.iterator(); i.hasNext();) { final DomChangeListener listener = i.next(); if (listener instanceof NonSerializable) { i.remove(); } } } else { temp = null; } // Perform object serialization, now that NonSerializable listeners have been removed. stream.defaultWriteObject(); // Restore the old listeners, now that serialization has been performed. domListeners_ = temp; } } }