View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import java.io.IOException;
18  import java.io.PrintWriter;
19  import java.io.Serializable;
20  import java.io.StringWriter;
21  import java.nio.charset.Charset;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.NoSuchElementException;
28  
29  import org.htmlunit.BrowserVersionFeatures;
30  import org.htmlunit.IncorrectnessListener;
31  import org.htmlunit.Page;
32  import org.htmlunit.SgmlPage;
33  import org.htmlunit.WebAssert;
34  import org.htmlunit.WebClient;
35  import org.htmlunit.WebClient.PooledCSS3Parser;
36  import org.htmlunit.WebWindow;
37  import org.htmlunit.css.ComputedCssStyleDeclaration;
38  import org.htmlunit.css.CssStyleSheet;
39  import org.htmlunit.css.StyleAttributes;
40  import org.htmlunit.cssparser.parser.CSSErrorHandler;
41  import org.htmlunit.cssparser.parser.CSSException;
42  import org.htmlunit.cssparser.parser.CSSOMParser;
43  import org.htmlunit.cssparser.parser.CSSParseException;
44  import org.htmlunit.cssparser.parser.selector.Selector;
45  import org.htmlunit.cssparser.parser.selector.SelectorList;
46  import org.htmlunit.html.HtmlElement.DisplayStyle;
47  import org.htmlunit.html.serializer.HtmlSerializerNormalizedText;
48  import org.htmlunit.html.serializer.HtmlSerializerVisibleText;
49  import org.htmlunit.html.xpath.XPathHelper;
50  import org.htmlunit.javascript.HtmlUnitScriptable;
51  import org.htmlunit.javascript.host.event.Event;
52  import org.htmlunit.xpath.xml.utils.PrefixResolver;
53  import org.w3c.dom.DOMException;
54  import org.w3c.dom.Document;
55  import org.w3c.dom.NamedNodeMap;
56  import org.w3c.dom.Node;
57  import org.w3c.dom.UserDataHandler;
58  import org.xml.sax.SAXException;
59  
60  /**
61   * Base class for nodes in the HTML DOM tree. This class is modeled after the
62   * W3C DOM specification, but does not implement it.
63   *
64   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
65   * @author <a href="mailto:gudujarlson@sf.net">Mike J. Bresnahan</a>
66   * @author David K. Taylor
67   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
68   * @author Chris Erskine
69   * @author Mike Williams
70   * @author Marc Guillemot
71   * @author Denis N. Antonioli
72   * @author Daniel Gredler
73   * @author Ahmed Ashour
74   * @author Rodney Gitzel
75   * @author Sudhan Moghe
76   * @author <a href="mailto:tom.anderson@univ.oxon.org">Tom Anderson</a>
77   * @author Ronald Brill
78   * @author Chuck Dumont
79   * @author Frank Danek
80   */
81  public abstract class DomNode implements Cloneable, Serializable, Node {
82  
83      /** A ready state constant (state 1). */
84      public static final String READY_STATE_UNINITIALIZED = "uninitialized";
85  
86      /** A ready state constant (state 2). */
87      public static final String READY_STATE_LOADING = "loading";
88  
89      /** A ready state constant (state 3). */
90      public static final String READY_STATE_LOADED = "loaded";
91  
92      /** A ready state constant (state 4). */
93      public static final String READY_STATE_INTERACTIVE = "interactive";
94  
95      /** A ready state constant (state 5). */
96      public static final String READY_STATE_COMPLETE = "complete";
97  
98      /** The name of the "element" property. Used when watching property change events. */
99      public static final String PROPERTY_ELEMENT = "element";
100 
101     private static final NamedNodeMap EMPTY_NAMED_NODE_MAP = new ReadOnlyEmptyNamedNodeMapImpl();
102 
103     /** The owning page of this node. */
104     private SgmlPage page_;
105 
106     /** The parent node. */
107     private DomNode parent_;
108 
109     /**
110      * The previous sibling. The first child's <code>previousSibling</code> points
111      * to the end of the list
112      */
113     private DomNode previousSibling_;
114 
115     /**
116      * The next sibling. The last child's <code>nextSibling</code> is {@code null}
117      */
118     private DomNode nextSibling_;
119 
120     /** Start of the child list. */
121     private DomNode firstChild_;
122 
123     /**
124      * This is the JavaScript object corresponding to this DOM node. It may
125      * be null if there isn't a corresponding JavaScript object.
126      */
127     private HtmlUnitScriptable scriptObject_;
128 
129     /** The ready state is an value that is available to a large number of elements. */
130     private String readyState_;
131 
132     /**
133      * The line number in the source page where the DOM node starts.
134      */
135     private int startLineNumber_ = -1;
136 
137     /**
138      * The column number in the source page where the DOM node starts.
139      */
140     private int startColumnNumber_ = -1;
141 
142     /**
143      * The line number in the source page where the DOM node ends.
144      */
145     private int endLineNumber_ = -1;
146 
147     /**
148      * The column number in the source page where the DOM node ends.
149      */
150     private int endColumnNumber_ = -1;
151 
152     private boolean attachedToPage_;
153 
154     /** The listeners which are to be notified of characterData change. */
155     private List<CharacterDataChangeListener> characterDataListeners_;
156     private List<DomChangeListener> domListeners_;
157 
158     private Map<String, Object> userData_;
159 
160     /**
161      * Creates a new instance.
162      * @param page the page which contains this node
163      */
164     protected DomNode(final SgmlPage page) {
165         readyState_ = READY_STATE_LOADING;
166         page_ = page;
167     }
168 
169     /**
170      * Sets the line and column numbers in the source page where the DOM node starts.
171      *
172      * @param startLineNumber the line number where the DOM node starts
173      * @param startColumnNumber the column number where the DOM node starts
174      */
175     public void setStartLocation(final int startLineNumber, final int startColumnNumber) {
176         startLineNumber_ = startLineNumber;
177         startColumnNumber_ = startColumnNumber;
178     }
179 
180     /**
181      * Sets the line and column numbers in the source page where the DOM node ends.
182      *
183      * @param endLineNumber the line number where the DOM node ends
184      * @param endColumnNumber the column number where the DOM node ends
185      */
186     public void setEndLocation(final int endLineNumber, final int endColumnNumber) {
187         endLineNumber_ = endLineNumber;
188         endColumnNumber_ = endColumnNumber;
189     }
190 
191     /**
192      * Returns the line number in the source page where the DOM node starts.
193      * @return the line number in the source page where the DOM node starts
194      */
195     public int getStartLineNumber() {
196         return startLineNumber_;
197     }
198 
199     /**
200      * Returns the column number in the source page where the DOM node starts.
201      * @return the column number in the source page where the DOM node starts
202      */
203     public int getStartColumnNumber() {
204         return startColumnNumber_;
205     }
206 
207     /**
208      * Returns the line number in the source page where the DOM node ends.
209      * @return 0 if no information on the line number is available (for instance for nodes dynamically added),
210      *         -1 if the end tag has not yet been parsed (during page loading)
211      */
212     public int getEndLineNumber() {
213         return endLineNumber_;
214     }
215 
216     /**
217      * Returns the column number in the source page where the DOM node ends.
218      * @return 0 if no information on the line number is available (for instance for nodes dynamically added),
219      *         -1 if the end tag has not yet been parsed (during page loading)
220      */
221     public int getEndColumnNumber() {
222         return endColumnNumber_;
223     }
224 
225     /**
226      * Returns the page that contains this node.
227      * @return the page that contains this node
228      */
229     public SgmlPage getPage() {
230         return page_;
231     }
232 
233     /**
234      * Returns the page that contains this node.
235      * @return the page that contains this node
236      */
237     public HtmlPage getHtmlPageOrNull() {
238         if (page_ == null || !page_.isHtmlPage()) {
239             return null;
240         }
241         return (HtmlPage) page_;
242     }
243 
244     /**
245      * {@inheritDoc}
246      */
247     @Override
248     public Document getOwnerDocument() {
249         return getPage();
250     }
251 
252     /**
253      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
254      *
255      * Sets the JavaScript object that corresponds to this node. This is not guaranteed to be set even if
256      * there is a JavaScript object for this DOM node.
257      *
258      * @param scriptObject the JavaScript object
259      */
260     public void setScriptableObject(final HtmlUnitScriptable scriptObject) {
261         scriptObject_ = scriptObject;
262     }
263 
264     /**
265      * {@inheritDoc}
266      */
267     @Override
268     public DomNode getLastChild() {
269         if (firstChild_ != null) {
270             // last child is stored as the previous sibling of first child
271             return firstChild_.previousSibling_;
272         }
273         return null;
274     }
275 
276     /**
277      * {@inheritDoc}
278      */
279     @Override
280     public DomNode getParentNode() {
281         return parent_;
282     }
283 
284     /**
285      * Sets the parent node.
286      * @param parent the parent node
287      */
288     protected void setParentNode(final DomNode parent) {
289         parent_ = parent;
290     }
291 
292     /**
293      * Returns this node's index within its parent's child nodes (zero-based).
294      * @return this node's index within its parent's child nodes (zero-based)
295      */
296     public int getIndex() {
297         int index = 0;
298         for (DomNode n = previousSibling_; n != null && n.nextSibling_ != null; n = n.previousSibling_) {
299             index++;
300         }
301         return index;
302     }
303 
304     /**
305      * {@inheritDoc}
306      */
307     @Override
308     public DomNode getPreviousSibling() {
309         if (parent_ == null || this == parent_.firstChild_) {
310             // previous sibling of first child points to last child
311             return null;
312         }
313         return previousSibling_;
314     }
315 
316     /**
317      * {@inheritDoc}
318      */
319     @Override
320     public DomNode getNextSibling() {
321         return nextSibling_;
322     }
323 
324     /**
325      * {@inheritDoc}
326      */
327     @Override
328     public DomNode getFirstChild() {
329         return firstChild_;
330     }
331 
332     /**
333      * Returns {@code true} if this node is an ancestor of the specified node.
334      *
335      * @param node the node to check
336      * @return {@code true} if this node is an ancestor of the specified node
337      */
338     public boolean isAncestorOf(DomNode node) {
339         while (node != null) {
340             if (node == this) {
341                 return true;
342             }
343             node = node.getParentNode();
344         }
345         return false;
346     }
347 
348     /**
349      * Returns {@code true} if this node is an ancestor of the specified nodes.
350      *
351      * @param nodes the nodes to check
352      * @return {@code true} if this node is an ancestor of the specified nodes
353      */
354     public boolean isAncestorOfAny(final DomNode... nodes) {
355         for (final DomNode node : nodes) {
356             if (isAncestorOf(node)) {
357                 return true;
358             }
359         }
360         return false;
361     }
362 
363     /**
364      * {@inheritDoc}
365      */
366     @Override
367     public String getNamespaceURI() {
368         return null;
369     }
370 
371     /**
372      * {@inheritDoc}
373      */
374     @Override
375     public String getLocalName() {
376         return null;
377     }
378 
379     /**
380      * {@inheritDoc}
381      */
382     @Override
383     public String getPrefix() {
384         return null;
385     }
386 
387     /**
388      * {@inheritDoc}
389      */
390     @Override
391     public boolean hasChildNodes() {
392         return firstChild_ != null;
393     }
394 
395     /**
396      * {@inheritDoc}
397      */
398     @Override
399     public DomNodeList<DomNode> getChildNodes() {
400         return new SiblingDomNodeList(this);
401     }
402 
403     /**
404      * {@inheritDoc}
405      * Not yet implemented.
406      */
407     @Override
408     public boolean isSupported(final String namespace, final String featureName) {
409         throw new UnsupportedOperationException("DomNode.isSupported is not yet implemented.");
410     }
411 
412     /**
413      * {@inheritDoc}
414      */
415     @Override
416     public void normalize() {
417         for (DomNode child = getFirstChild(); child != null; child = child.getNextSibling()) {
418             if (child instanceof DomText) {
419                 final StringBuilder dataBuilder = new StringBuilder();
420                 DomNode toRemove = child;
421                 DomText firstText = null;
422                 //IE removes all child text nodes, but FF preserves the first
423                 while (toRemove instanceof DomText && !(toRemove instanceof DomCDataSection)) {
424                     final DomNode nextChild = toRemove.getNextSibling();
425                     dataBuilder.append(toRemove.getTextContent());
426                     if (firstText != null) {
427                         toRemove.remove();
428                     }
429                     if (firstText == null) {
430                         firstText = (DomText) toRemove;
431                     }
432                     toRemove = nextChild;
433                 }
434                 if (firstText != null) {
435                     firstText.setData(dataBuilder.toString());
436                 }
437             }
438         }
439     }
440 
441     /**
442      * {@inheritDoc}
443      */
444     @Override
445     public String getBaseURI() {
446         return getPage().getUrl().toExternalForm();
447     }
448 
449     /**
450      * {@inheritDoc}
451      */
452     @Override
453     public short compareDocumentPosition(final Node other) {
454         if (other == this) {
455             return 0; // strange, no constant available?
456         }
457 
458         // get ancestors of both
459         final List<Node> myAncestors = getAncestors();
460         final List<Node> otherAncestors = ((DomNode) other).getAncestors();
461 
462         if (!myAncestors.get(0).equals(otherAncestors.get(0))) {
463             // spec likes to have a consistent order
464             //
465             // If ... node1’s root is not node2’s root, then return the result of adding
466             // DOCUMENT_POSITION_DISCONNECTED, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,
467             // and either DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING,
468             // with the constraint that this is to be consistent...
469             if (this.hashCode() < other.hashCode()) {
470                 return DOCUMENT_POSITION_DISCONNECTED
471                         | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
472                         | DOCUMENT_POSITION_PRECEDING;
473             }
474 
475             return DOCUMENT_POSITION_DISCONNECTED
476                     | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
477                     | DOCUMENT_POSITION_FOLLOWING;
478         }
479 
480         final int max = Math.min(myAncestors.size(), otherAncestors.size());
481 
482         int i = 1;
483         while (i < max && myAncestors.get(i) == otherAncestors.get(i)) {
484             i++;
485         }
486 
487         if (i != 1 && i == max) {
488             if (myAncestors.size() == max) {
489                 return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING;
490             }
491             return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING;
492         }
493 
494         if (max == 1) {
495             if (myAncestors.contains(other)) {
496                 return DOCUMENT_POSITION_CONTAINS;
497             }
498             if (otherAncestors.contains(this)) {
499                 return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING;
500             }
501             return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
502         }
503 
504         // neither contains nor contained by
505         final Node myAncestor = myAncestors.get(i);
506         final Node otherAncestor = otherAncestors.get(i);
507         Node node = myAncestor;
508         while (node != otherAncestor && node != null) {
509             node = node.getPreviousSibling();
510         }
511         if (node == null) {
512             return DOCUMENT_POSITION_FOLLOWING;
513         }
514         return DOCUMENT_POSITION_PRECEDING;
515     }
516 
517     /**
518      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
519      *
520      * Gets the ancestors of the node.
521      * @return a list of the ancestors with the root at the first position
522      */
523     public List<Node> getAncestors() {
524         final List<Node> list = new ArrayList<>();
525         list.add(this);
526 
527         Node node = getParentNode();
528         while (node != null) {
529             list.add(0, node);
530             node = node.getParentNode();
531         }
532         return list;
533     }
534 
535     /**
536      * {@inheritDoc}
537      */
538     @Override
539     public String getTextContent() {
540         switch (getNodeType()) {
541             case ELEMENT_NODE:
542             case ATTRIBUTE_NODE:
543             case ENTITY_NODE:
544             case ENTITY_REFERENCE_NODE:
545             case DOCUMENT_FRAGMENT_NODE:
546                 final StringBuilder builder = new StringBuilder();
547                 for (final DomNode child : getChildren()) {
548                     final short childType = child.getNodeType();
549                     if (childType != COMMENT_NODE && childType != PROCESSING_INSTRUCTION_NODE) {
550                         builder.append(child.getTextContent());
551                     }
552                 }
553                 return builder.toString();
554 
555             case TEXT_NODE:
556             case CDATA_SECTION_NODE:
557             case COMMENT_NODE:
558             case PROCESSING_INSTRUCTION_NODE:
559                 return getNodeValue();
560 
561             default:
562                 return null;
563         }
564     }
565 
566     /**
567      * {@inheritDoc}
568      */
569     @Override
570     public void setTextContent(final String textContent) {
571         removeAllChildren();
572         if (textContent != null && !textContent.isEmpty()) {
573             appendChild(new DomText(getPage(), textContent));
574         }
575     }
576 
577     /**
578      * {@inheritDoc}
579      */
580     @Override
581     public boolean isSameNode(final Node other) {
582         return other == this;
583     }
584 
585     /**
586      * {@inheritDoc}
587      * Not yet implemented.
588      */
589     @Override
590     public String lookupPrefix(final String namespaceURI) {
591         throw new UnsupportedOperationException("DomNode.lookupPrefix is not yet implemented.");
592     }
593 
594     /**
595      * {@inheritDoc}
596      * Not yet implemented.
597      */
598     @Override
599     public boolean isDefaultNamespace(final String namespaceURI) {
600         throw new UnsupportedOperationException("DomNode.isDefaultNamespace is not yet implemented.");
601     }
602 
603     /**
604      * {@inheritDoc}
605      * Not yet implemented.
606      */
607     @Override
608     public String lookupNamespaceURI(final String prefix) {
609         throw new UnsupportedOperationException("DomNode.lookupNamespaceURI is not yet implemented.");
610     }
611 
612     /**
613      * {@inheritDoc}
614      * Not yet implemented.
615      */
616     @Override
617     public boolean isEqualNode(final Node arg) {
618         throw new UnsupportedOperationException("DomNode.isEqualNode is not yet implemented.");
619     }
620 
621     /**
622      * {@inheritDoc}
623      * Not yet implemented.
624      */
625     @Override
626     public Object getFeature(final String feature, final String version) {
627         throw new UnsupportedOperationException("DomNode.getFeature is not yet implemented.");
628     }
629 
630     /**
631      * {@inheritDoc}
632      */
633     @Override
634     public Object getUserData(final String key) {
635         Object value = null;
636         if (userData_ != null) {
637             value = userData_.get(key);
638         }
639         return value;
640     }
641 
642     /**
643      * {@inheritDoc}
644      */
645     @Override
646     public Object setUserData(final String key, final Object data, final UserDataHandler handler) {
647         if (userData_ == null) {
648             userData_ = new HashMap<>();
649         }
650         return userData_.put(key, data);
651     }
652 
653     /**
654      * {@inheritDoc}
655      */
656     @Override
657     public boolean hasAttributes() {
658         return false;
659     }
660 
661     /**
662      * {@inheritDoc}
663      */
664     @Override
665     public NamedNodeMap getAttributes() {
666         return EMPTY_NAMED_NODE_MAP;
667     }
668 
669     /**
670      * <p>Returns {@code true} if this node is displayed and can be visible to the user
671      * (ignoring screen size, scrolling limitations, color, font-size, or overlapping nodes).</p>
672      *
673      * <p><b>NOTE:</b> If CSS is
674      * {@link org.htmlunit.WebClientOptions#setCssEnabled(boolean) disabled}, this method
675      * does <b>not</b> take this element's style into consideration!</p>
676      *
677      * @see <a href="http://www.w3.org/TR/CSS2/visufx.html#visibility">CSS2 Visibility</a>
678      * @see <a href="http://www.w3.org/TR/CSS2/visuren.html#propdef-display">CSS2 Display</a>
679      * @see <a href="http://msdn.microsoft.com/en-us/library/ms531180.aspx">MSDN Documentation</a>
680      * @return {@code true} if the node is visible to the user, {@code false} otherwise
681      * @see #mayBeDisplayed()
682      */
683     public boolean isDisplayed() {
684         if (!mayBeDisplayed()) {
685             return false;
686         }
687 
688         final Page page = getPage();
689         final WebWindow window = page.getEnclosingWindow();
690         final WebClient webClient = window.getWebClient();
691         if (webClient.getOptions().isCssEnabled()) {
692             // display: iterate top to bottom, because if a parent is display:none,
693             // there's nothing that a child can do to override it
694             final List<Node> ancestors = getAncestors();
695             final ArrayList<ComputedCssStyleDeclaration> styles = new ArrayList<>(ancestors.size());
696 
697             for (final Node node : ancestors) {
698                 if (node instanceof HtmlElement) {
699                     final HtmlElement elem = (HtmlElement) node;
700                     if (elem.isHidden()) {
701                         return false;
702                     }
703 
704                     if (elem instanceof HtmlDialog) {
705                         if (!((HtmlDialog) elem).isOpen()) {
706                             return false;
707                         }
708                     }
709                     else {
710                         final ComputedCssStyleDeclaration style = window.getComputedStyle(elem, null);
711                         if (DisplayStyle.NONE.value().equals(style.getDisplay())) {
712                             return false;
713                         }
714                         styles.add(style);
715                     }
716                 }
717             }
718 
719             // visibility: iterate bottom to top, because children can override
720             // the visibility used by parent nodes
721             for (int i = styles.size() - 1; i >= 0; i--) {
722                 final ComputedCssStyleDeclaration style = styles.get(i);
723                 final String visibility = style.getStyleAttribute(StyleAttributes.Definition.VISIBILITY, true);
724                 if (visibility.length() > 5) {
725                     if ("visible".equals(visibility)) {
726                         return true;
727                     }
728                     if ("hidden".equals(visibility) || "collapse".equals(visibility)) {
729                         return false;
730                     }
731                 }
732             }
733         }
734         return true;
735     }
736 
737     /**
738      * Returns {@code true} if nodes of this type can ever be displayed, {@code false} otherwise. Examples of nodes
739      * that can never be displayed are <code>&lt;head&gt;</code>,
740      * <code>&lt;meta&gt;</code>, <code>&lt;script&gt;</code>, etc.
741      * @return {@code true} if nodes of this type can ever be displayed, {@code false} otherwise
742      * @see #isDisplayed()
743      */
744     public boolean mayBeDisplayed() {
745         return true;
746     }
747 
748     /**
749      * Returns a normalized textual representation of this element that represents
750      * what would be visible to the user if this page was shown in a web browser.
751      * Whitespace is normalized like in the browser and block tags are separated by '\n'.
752      *
753      * @return a normalized textual representation of this element
754      */
755     public String asNormalizedText() {
756         final HtmlSerializerNormalizedText ser = new HtmlSerializerNormalizedText();
757         return ser.asText(this);
758     }
759 
760     /**
761      * Returns a textual representation of this element in the same way as
762      * the selenium/WebDriver WebElement#getText() property does.<br>
763      * see <a href="https://w3c.github.io/webdriver/#get-element-text">get-element-text</a> and
764      * <a href="https://w3c.github.io/webdriver/#dfn-bot-dom-getvisibletext">dfn-bot-dom-getvisibletext</a>
765      * Note: this is different from {@link #asNormalizedText()}
766      *
767      * @return a textual representation of this element that represents what would
768      *         be visible to the user if this page was shown in a web browser
769      */
770     public String getVisibleText() {
771         final HtmlSerializerVisibleText ser = new HtmlSerializerVisibleText();
772         return ser.asText(this);
773     }
774 
775     /**
776      * Returns a string representation as XML document from this element and all it's children (recursively).<br>
777      * The charset used in the xml header is the current page encoding; but the result is still a string.
778      * You have to make sure to use the correct (in fact the same) encoding if you write this to a file.<br>
779      * This serializes the current state of the DomTree - this implies that the content of noscript tags
780      * usually serialized as string because the content is converted during parsing (if js was enabled at that time).
781      * @return the XML string
782      */
783     public String asXml() {
784         Charset charsetName = null;
785         final HtmlPage htmlPage = getHtmlPageOrNull();
786         if (htmlPage != null) {
787             charsetName = htmlPage.getCharset();
788         }
789 
790         final StringWriter stringWriter = new StringWriter();
791         try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
792             if (charsetName != null && this instanceof HtmlHtml) {
793                 printWriter.print("<?xml version=\"1.0\" encoding=\"");
794                 printWriter.print(charsetName);
795                 printWriter.print("\"?>\r\n");
796             }
797             printXml("", printWriter);
798             return stringWriter.toString();
799         }
800     }
801 
802     /**
803      * Recursively writes the XML data for the node tree starting at <code>node</code>.
804      *
805      * @param indent white space to indent child nodes
806      * @param printWriter writer where child nodes are written
807      */
808     protected void printXml(final String indent, final PrintWriter printWriter) {
809         printWriter.print(indent);
810         printWriter.print(this);
811         printWriter.print("\r\n");
812         printChildrenAsXml(indent, printWriter);
813     }
814 
815     /**
816      * Recursively writes the XML data for the node tree starting at <code>node</code>.
817      *
818      * @param indent white space to indent child nodes
819      * @param printWriter writer where child nodes are written
820      */
821     protected void printChildrenAsXml(final String indent, final PrintWriter printWriter) {
822         DomNode child = getFirstChild();
823         while (child != null) {
824             child.printXml(indent + "  ", printWriter);
825             child = child.getNextSibling();
826         }
827     }
828 
829     /**
830      * {@inheritDoc}
831      */
832     @Override
833     public String getNodeValue() {
834         return null;
835     }
836 
837     /**
838      * {@inheritDoc}
839      */
840     @Override
841     public DomNode cloneNode(final boolean deep) {
842         final DomNode newnode;
843         try {
844             newnode = (DomNode) clone();
845         }
846         catch (final CloneNotSupportedException e) {
847             throw new IllegalStateException("Clone not supported for node [" + this + "]", e);
848         }
849 
850         newnode.parent_ = null;
851         newnode.nextSibling_ = null;
852         newnode.previousSibling_ = null;
853         newnode.scriptObject_ = null;
854         newnode.firstChild_ = null;
855         newnode.attachedToPage_ = false;
856 
857         // if deep, clone the children too.
858         if (deep) {
859             for (DomNode child = firstChild_; child != null; child = child.nextSibling_) {
860                 newnode.appendChild(child.cloneNode(true));
861             }
862         }
863 
864         return newnode;
865     }
866 
867     /**
868      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
869      *
870      * <p>Returns the JavaScript object that corresponds to this node, lazily initializing a new one if necessary.</p>
871      *
872      * <p>The logic of when and where the JavaScript object is created needs a clean up: functions using
873      * a DOM node's JavaScript object should not have to check if they should create it first.</p>
874      *
875      * @param <T> the object type
876      * @return the JavaScript object that corresponds to this node
877      */
878     @SuppressWarnings("unchecked")
879     public <T extends HtmlUnitScriptable> T getScriptableObject() {
880         if (scriptObject_ == null) {
881             final SgmlPage page = getPage();
882             if (this == page) {
883                 final StringBuilder msg = new StringBuilder("No script object associated with the Page.");
884                 // because this is a strange case we like to provide as much info as possible
885                 msg.append(" class: '")
886                     .append(page.getClass().getName())
887                     .append('\'');
888                 try {
889                     msg.append(" url: '")
890                         .append(page.getUrl()).append("' content: ")
891                         .append(page.getWebResponse().getContentAsString());
892                 }
893                 catch (final Exception e) {
894                     // ok bad luck with detail
895                     msg.append(" no details: '").append(e).append('\'');
896                 }
897                 throw new IllegalStateException(msg.toString());
898             }
899             scriptObject_ = page.getScriptableObject().makeScriptableFor(this);
900         }
901         return (T) scriptObject_;
902     }
903 
904     /**
905      * {@inheritDoc}
906      */
907     @Override
908     public DomNode appendChild(final Node node) {
909         if (node == this) {
910             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add not to itself " + this);
911         }
912         final DomNode domNode = (DomNode) node;
913         if (domNode.isAncestorOf(this)) {
914             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add (grand)parent to itself " + this);
915         }
916 
917         if (domNode instanceof DomDocumentFragment) {
918             final DomDocumentFragment fragment = (DomDocumentFragment) domNode;
919             for (final DomNode child : fragment.getChildren()) {
920                 appendChild(child);
921             }
922         }
923         else {
924             // clean up the new node, in case it is being moved
925             if (domNode.getParentNode() != null) {
926                 domNode.detach();
927             }
928 
929             basicAppend(domNode);
930 
931             fireAddition(domNode);
932         }
933 
934         return domNode;
935     }
936 
937     /**
938      * Appends the specified node to the end of this node's children, assuming the specified
939      * node is clean (doesn't have preexisting relationships to other nodes).
940      *
941      * @param node the node to append to this node's children
942      */
943     private void basicAppend(final DomNode node) {
944         // try to make the node setup as complete as possible
945         // before the node is reachable
946         node.setPage(getPage());
947         node.parent_ = this;
948 
949         if (firstChild_ == null) {
950             firstChild_ = node;
951         }
952         else {
953             final DomNode last = getLastChild();
954             node.previousSibling_ = last;
955             node.nextSibling_ = null; // safety first
956 
957             last.nextSibling_ = node;
958         }
959         firstChild_.previousSibling_ = node;
960     }
961 
962     /**
963      * {@inheritDoc}
964      */
965     @Override
966     public Node insertBefore(final Node newChild, final Node refChild) {
967         if (newChild instanceof DomDocumentFragment) {
968             final DomDocumentFragment fragment = (DomDocumentFragment) newChild;
969             for (final DomNode child : fragment.getChildren()) {
970                 insertBefore(child, refChild);
971             }
972             return newChild;
973         }
974 
975         if (refChild == null) {
976             appendChild(newChild);
977             return newChild;
978         }
979 
980         if (refChild.getParentNode() != this) {
981             throw new DOMException(DOMException.NOT_FOUND_ERR, "Reference node is not a child of this node.");
982         }
983 
984         ((DomNode) refChild).insertBefore((DomNode) newChild);
985         return newChild;
986     }
987 
988     /**
989      * Inserts the specified node as a new child node before this node into the child relationship this node is a
990      * part of. If the specified node is this node, this method is a no-op.
991      *
992      * @param newNode the new node to insert
993      */
994     public void insertBefore(final DomNode newNode) {
995         if (previousSibling_ == null) {
996             throw new IllegalStateException("Previous sibling for " + this + " is null.");
997         }
998 
999         if (newNode == this) {
1000             return;
1001         }
1002 
1003         // clean up the new node, in case it is being moved
1004         if (newNode.getParentNode() != null) {
1005             newNode.detach();
1006         }
1007 
1008         basicInsertBefore(newNode);
1009 
1010         fireAddition(newNode);
1011     }
1012 
1013     /**
1014      * Inserts the specified node into this node's parent's children right before this node, assuming the specified
1015      * node is clean (doesn't have preexisting relationships to other nodes).
1016      *
1017      * @param node the node to insert before this node
1018      */
1019     private void basicInsertBefore(final DomNode node) {
1020         // try to make the node setup as complete as possible
1021         // before the node is reachable
1022         node.setPage(page_);
1023         node.parent_ = parent_;
1024         node.previousSibling_ = previousSibling_;
1025         node.nextSibling_ = this;
1026 
1027         if (parent_.firstChild_ == this) {
1028             parent_.firstChild_ = node;
1029         }
1030         else {
1031             previousSibling_.nextSibling_ = node;
1032         }
1033         previousSibling_ = node;
1034     }
1035 
1036     private void fireAddition(final DomNode domNode) {
1037         final boolean wasAlreadyAttached = domNode.isAttachedToPage();
1038         domNode.attachedToPage_ = isAttachedToPage();
1039 
1040         final SgmlPage page = getPage();
1041         if (domNode.attachedToPage_) {
1042             // trigger events
1043             if (null != page && page.isHtmlPage()) {
1044                 ((HtmlPage) page).notifyNodeAdded(domNode);
1045             }
1046 
1047             // a node that is already "complete" (ie not being parsed) and not yet attached
1048             if (!domNode.isBodyParsed() && !wasAlreadyAttached) {
1049                 if (domNode.getFirstChild() != null) {
1050                     for (final Iterator<DomNode> iterator =
1051                             domNode.new DescendantDomNodesIterator(); iterator.hasNext();) {
1052                         final DomNode child = iterator.next();
1053                         child.attachedToPage_ = true;
1054                         child.onAllChildrenAddedToPage(true);
1055                     }
1056                 }
1057                 domNode.onAllChildrenAddedToPage(true);
1058             }
1059         }
1060 
1061         if (this instanceof DomDocumentFragment) {
1062             onAddedToDocumentFragment();
1063         }
1064 
1065         if (page == null || page.isDomChangeListenerInUse()) {
1066             fireNodeAdded(this, domNode);
1067         }
1068     }
1069 
1070     /**
1071      * Indicates if the current node is being parsed. This means that the opening tag has already been
1072      * parsed but not the body and end tag.
1073      */
1074     private boolean isBodyParsed() {
1075         return getStartLineNumber() != -1 && getEndLineNumber() == -1;
1076     }
1077 
1078     /**
1079      * Recursively sets the new page on the node and its children
1080      * @param newPage the new owning page
1081      */
1082     private void setPage(final SgmlPage newPage) {
1083         if (page_ == newPage) {
1084             return; // nothing to do
1085         }
1086 
1087         page_ = newPage;
1088         for (final DomNode node : getChildren()) {
1089             node.setPage(newPage);
1090         }
1091     }
1092 
1093     /**
1094      * {@inheritDoc}
1095      */
1096     @Override
1097     public Node removeChild(final Node child) {
1098         if (child.getParentNode() != this) {
1099             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1100         }
1101         ((DomNode) child).remove();
1102         return child;
1103     }
1104 
1105     /**
1106      * Removes all of this node's children.
1107      */
1108     public void removeAllChildren() {
1109         while (getFirstChild() != null) {
1110             getFirstChild().remove();
1111         }
1112     }
1113 
1114     /**
1115      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1116      *
1117      * Parses the specified HTML source code, appending the resulting content at the specified target location.
1118      * @param source the HTML code extract to parse
1119      * @throws IOException in case of error
1120      * @throws SAXException in case of error
1121      */
1122     public void parseHtmlSnippet(final String source) throws SAXException, IOException {
1123         final WebClient webClient = getPage().getWebClient();
1124         webClient.getPageCreator().getHtmlParser().parseFragment(webClient, this, this, source, false);
1125     }
1126 
1127     /**
1128      * Removes this node from all relationships with other nodes.
1129      */
1130     public void remove() {
1131         // same as detach for the moment
1132         detach();
1133     }
1134 
1135     /**
1136      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1137      *
1138      * Detach this node from all relationships with other nodes.
1139      * This is the first step of a move.
1140      */
1141     protected void detach() {
1142         final DomNode exParent = parent_;
1143 
1144         basicRemove();
1145 
1146         fireRemoval(exParent);
1147     }
1148 
1149     /**
1150      * Cuts off all relationships this node has with siblings and parents.
1151      */
1152     protected void basicRemove() {
1153         if (parent_ != null && parent_.firstChild_ == this) {
1154             parent_.firstChild_ = nextSibling_;
1155         }
1156         else if (previousSibling_ != null && previousSibling_.nextSibling_ == this) {
1157             previousSibling_.nextSibling_ = nextSibling_;
1158         }
1159         if (nextSibling_ != null && nextSibling_.previousSibling_ == this) {
1160             nextSibling_.previousSibling_ = previousSibling_;
1161         }
1162         if (parent_ != null && this == parent_.getLastChild()) {
1163             parent_.firstChild_.previousSibling_ = previousSibling_;
1164         }
1165 
1166         nextSibling_ = null;
1167         previousSibling_ = null;
1168         parent_ = null;
1169         attachedToPage_ = false;
1170         for (final DomNode descendant : getDescendants()) {
1171             descendant.attachedToPage_ = false;
1172         }
1173     }
1174 
1175     private void fireRemoval(final DomNode exParent) {
1176         final SgmlPage page = getPage();
1177         if (page instanceof HtmlPage) {
1178             // some actions executed on removal need an intact parent relationship (e.g. for the
1179             // DocumentPositionComparator) so we have to restore it temporarily
1180             parent_ = exParent;
1181             ((HtmlPage) page).notifyNodeRemoved(this);
1182             parent_ = null;
1183         }
1184 
1185         if (exParent != null && (page == null || page.isDomChangeListenerInUse())) {
1186             fireNodeDeleted(exParent, this);
1187             // ask ex-parent to fire event (because we don't have parent now)
1188             exParent.fireNodeDeleted(exParent, this);
1189         }
1190     }
1191 
1192     /**
1193      * {@inheritDoc}
1194      */
1195     @Override
1196     public Node replaceChild(final Node newChild, final Node oldChild) {
1197         if (oldChild.getParentNode() != this) {
1198             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1199         }
1200         ((DomNode) oldChild).replace((DomNode) newChild);
1201         return oldChild;
1202     }
1203 
1204     /**
1205      * Replaces this node with another node. If the specified node is this node, this
1206      * method is a no-op.
1207      * @param newNode the node to replace this one
1208      */
1209     public void replace(final DomNode newNode) {
1210         if (newNode != this) {
1211             final DomNode exParent = parent_;
1212             final DomNode exNextSibling = nextSibling_;
1213 
1214             remove();
1215 
1216             exParent.insertBefore(newNode, exNextSibling);
1217         }
1218     }
1219 
1220     /**
1221      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1222      *
1223      * Quietly removes this node and moves its children to the specified destination. "Quietly" means
1224      * that no node events are fired. This method is not appropriate for most use cases. It should
1225      * only be used in specific cases for HTML parsing hackery.
1226      *
1227      * @param destination the node to which this node's children should be moved before this node is removed
1228      */
1229     public void quietlyRemoveAndMoveChildrenTo(final DomNode destination) {
1230         if (destination.getPage() != getPage()) {
1231             throw new RuntimeException("Cannot perform quiet move on nodes from different pages.");
1232         }
1233         for (final DomNode child : getChildren()) {
1234             if (child != destination) {
1235                 child.basicRemove();
1236                 destination.basicAppend(child);
1237             }
1238         }
1239         basicRemove();
1240     }
1241 
1242     /**
1243      * Check for insertion errors for a new child node. This is overridden by derived
1244      * classes to enforce which types of children are allowed.
1245      *
1246      * @param newChild the new child node that is being inserted below this node
1247      * @throws DOMException HIERARCHY_REQUEST_ERR: Raised if this node is of a type that does
1248      *         not allow children of the type of the newChild node, or if the node to insert is one of
1249      *         this node's ancestors or this node itself, or if this node is of type Document and the
1250      *         DOM application attempts to insert a second DocumentType or Element node.
1251      *         WRONG_DOCUMENT_ERR: Raised if newChild was created from a different document than the
1252      *         one that created this node.
1253      */
1254     protected void checkChildHierarchy(final Node newChild) throws DOMException {
1255         Node parentNode = this;
1256         while (parentNode != null) {
1257             if (parentNode == newChild) {
1258                 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Child node is already a parent.");
1259             }
1260             parentNode = parentNode.getParentNode();
1261         }
1262         final Document thisDocument = getOwnerDocument();
1263         final Document childDocument = newChild.getOwnerDocument();
1264         if (childDocument != thisDocument && childDocument != null) {
1265             throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Child node " + newChild.getNodeName()
1266                 + " is not in the same Document as this " + getNodeName() + ".");
1267         }
1268     }
1269 
1270     /**
1271      * Lifecycle method invoked whenever a node is added to a page. Intended to
1272      * be overridden by nodes which need to perform custom logic when they are
1273      * added to a page. This method is recursive, so if you override it, please
1274      * be sure to call <code>super.onAddedToPage()</code>.
1275      */
1276     protected void onAddedToPage() {
1277         if (firstChild_ != null) {
1278             for (final DomNode child : getChildren()) {
1279                 child.onAddedToPage();
1280             }
1281         }
1282     }
1283 
1284     /**
1285      * Lifecycle method invoked after a node and all its children have been added to a page, during
1286      * parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic
1287      * after they and all their child nodes have been processed by the HTML parser. This method is
1288      * not recursive, and the default implementation is empty, so there is no need to call
1289      * <code>super.onAllChildrenAddedToPage()</code> if you implement this method.
1290      * @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no
1291      */
1292     public void onAllChildrenAddedToPage(final boolean postponed) {
1293         // Empty by default.
1294     }
1295 
1296     /**
1297      * Lifecycle method invoked whenever a node is added to a document fragment. Intended to
1298      * be overridden by nodes which need to perform custom logic when they are
1299      * added to a fragment. This method is recursive, so if you override it, please
1300      * be sure to call <code>super.onAddedToDocumentFragment()</code>.
1301      */
1302     protected void onAddedToDocumentFragment() {
1303         if (firstChild_ != null) {
1304             for (final DomNode child : getChildren()) {
1305                 child.onAddedToDocumentFragment();
1306             }
1307         }
1308     }
1309 
1310     /**
1311      * @return an {@link Iterable} over the children of this node
1312      */
1313     public final Iterable<DomNode> getChildren() {
1314         return () -> new ChildIterator(firstChild_);
1315     }
1316 
1317     /**
1318      * An iterator over all children of this node.
1319      */
1320     protected static class ChildIterator implements Iterator<DomNode> {
1321 
1322         private DomNode nextNode_;
1323         private DomNode currentNode_;
1324 
1325         public ChildIterator(final DomNode nextNode) {
1326             nextNode_ = nextNode;
1327         }
1328 
1329         /** {@inheritDoc} */
1330         @Override
1331         public boolean hasNext() {
1332             return nextNode_ != null;
1333         }
1334 
1335         /** {@inheritDoc} */
1336         @Override
1337         public DomNode next() {
1338             if (nextNode_ != null) {
1339                 currentNode_ = nextNode_;
1340                 nextNode_ = nextNode_.nextSibling_;
1341                 return currentNode_;
1342             }
1343             throw new NoSuchElementException();
1344         }
1345 
1346         /** {@inheritDoc} */
1347         @Override
1348         public void remove() {
1349             if (currentNode_ == null) {
1350                 throw new IllegalStateException();
1351             }
1352             currentNode_.remove();
1353         }
1354     }
1355 
1356     /**
1357      * Returns an {@link Iterable} that will recursively iterate over all of this node's descendants,
1358      * including {@link DomText} elements, {@link DomComment} elements, etc. If you want to iterate
1359      * only over {@link HtmlElement} descendants, please use {@link #getHtmlElementDescendants()}.
1360      * @return an {@link Iterable} that will recursively iterate over all of this node's descendants
1361      */
1362     public final Iterable<DomNode> getDescendants() {
1363         return () -> new DescendantDomNodesIterator();
1364     }
1365 
1366     /**
1367      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1368      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1369      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1370      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1371      *         descendants
1372      * @see #getDomElementDescendants()
1373      */
1374     public final Iterable<HtmlElement> getHtmlElementDescendants() {
1375         return () -> new DescendantHtmlElementsIterator();
1376     }
1377 
1378     /**
1379      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1380      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1381      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1382      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1383      *         descendants
1384      * @see #getHtmlElementDescendants()
1385      */
1386     public final Iterable<DomElement> getDomElementDescendants() {
1387         return () -> new DescendantDomElementsIterator();
1388     }
1389 
1390     /**
1391      * Iterates over all descendants of a specific type, in document order.
1392      * @param <T> the type of nodes over which to iterate
1393      *
1394      * @deprecated as of version 4.7.0; use {@link DescendantDomNodesIterator},
1395      *     {@link DescendantDomElementsIterator}, or {@link DescendantHtmlElementsIterator} instead.
1396      */
1397     @Deprecated
1398     protected class DescendantElementsIterator<T extends DomNode> implements Iterator<T> {
1399 
1400         private DomNode currentNode_;
1401         private DomNode nextNode_;
1402         private final Class<T> type_;
1403 
1404         /**
1405          * Creates a new instance which iterates over the specified node type.
1406          * @param type the type of nodes over which to iterate
1407          */
1408         public DescendantElementsIterator(final Class<T> type) {
1409             type_ = type;
1410             nextNode_ = getFirstChildElement(DomNode.this);
1411         }
1412 
1413         /** {@inheritDoc} */
1414         @Override
1415         public boolean hasNext() {
1416             return nextNode_ != null;
1417         }
1418 
1419         /** {@inheritDoc} */
1420         @Override
1421         public T next() {
1422             return nextNode();
1423         }
1424 
1425         /** {@inheritDoc} */
1426         @Override
1427         public void remove() {
1428             if (currentNode_ == null) {
1429                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1430             }
1431             final DomNode current = currentNode_;
1432             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1433                 next();
1434             }
1435             current.remove();
1436         }
1437 
1438         /**
1439          * @return the next node, if there is one
1440          */
1441         @SuppressWarnings("unchecked")
1442         public T nextNode() {
1443             currentNode_ = nextNode_;
1444             setNextElement();
1445             return (T) currentNode_;
1446         }
1447 
1448         private void setNextElement() {
1449             DomNode next = getFirstChildElement(nextNode_);
1450             if (next == null) {
1451                 next = getNextDomSibling(nextNode_);
1452             }
1453             if (next == null) {
1454                 next = getNextElementUpwards(nextNode_);
1455             }
1456             nextNode_ = next;
1457         }
1458 
1459         private DomNode getNextElementUpwards(final DomNode startingNode) {
1460             if (startingNode == DomNode.this) {
1461                 return null;
1462             }
1463 
1464             DomNode parent = startingNode.getParentNode();
1465             while (parent != null && parent != DomNode.this) {
1466                 DomNode next = parent.getNextSibling();
1467                 while (next != null && !isAccepted(next)) {
1468                     next = next.getNextSibling();
1469                 }
1470                 if (next != null) {
1471                     return next;
1472                 }
1473                 parent = parent.getParentNode();
1474             }
1475             return null;
1476         }
1477 
1478         private DomNode getFirstChildElement(final DomNode parent) {
1479             DomNode node = parent.getFirstChild();
1480             while (node != null && !isAccepted(node)) {
1481                 node = node.getNextSibling();
1482             }
1483             return node;
1484         }
1485 
1486         /**
1487          * Indicates if the node is accepted. If not it won't be explored at all.
1488          * @param node the node to test
1489          * @return {@code true} if accepted
1490          */
1491         protected boolean isAccepted(final DomNode node) {
1492             return type_.isAssignableFrom(node.getClass());
1493         }
1494 
1495         private DomNode getNextDomSibling(final DomNode element) {
1496             DomNode node = element.getNextSibling();
1497             while (node != null && !isAccepted(node)) {
1498                 node = node.getNextSibling();
1499             }
1500             return node;
1501         }
1502     }
1503 
1504     /**
1505      * Iterates over all descendants DomNodes, in document order.
1506      */
1507     protected final class DescendantDomNodesIterator implements Iterator<DomNode> {
1508         private DomNode currentNode_;
1509         private DomNode nextNode_;
1510 
1511         /**
1512          * Creates a new instance which iterates over the specified node type.
1513          */
1514         public DescendantDomNodesIterator() {
1515             nextNode_ = getFirstChildElement(DomNode.this);
1516         }
1517 
1518         /** {@inheritDoc} */
1519         @Override
1520         public boolean hasNext() {
1521             return nextNode_ != null;
1522         }
1523 
1524         /** {@inheritDoc} */
1525         @Override
1526         public DomNode next() {
1527             return nextNode();
1528         }
1529 
1530         /** {@inheritDoc} */
1531         @Override
1532         public void remove() {
1533             if (currentNode_ == null) {
1534                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1535             }
1536             final DomNode current = currentNode_;
1537             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1538                 next();
1539             }
1540             current.remove();
1541         }
1542 
1543         /**
1544          * @return the next node, if there is one
1545          */
1546         @SuppressWarnings("unchecked")
1547         public DomNode nextNode() {
1548             currentNode_ = nextNode_;
1549 
1550             DomNode next = getFirstChildElement(nextNode_);
1551             if (next == null) {
1552                 next = getNextDomSibling(nextNode_);
1553             }
1554             if (next == null) {
1555                 next = getNextElementUpwards(nextNode_);
1556             }
1557             nextNode_ = next;
1558 
1559             return currentNode_;
1560         }
1561 
1562         private DomNode getNextElementUpwards(final DomNode startingNode) {
1563             if (startingNode == DomNode.this) {
1564                 return null;
1565             }
1566 
1567             DomNode parent = startingNode.getParentNode();
1568             while (parent != null && parent != DomNode.this) {
1569                 DomNode next = parent.getNextSibling();
1570                 while (next != null && !isAccepted(next)) {
1571                     next = next.getNextSibling();
1572                 }
1573                 if (next != null) {
1574                     return next;
1575                 }
1576                 parent = parent.getParentNode();
1577             }
1578             return null;
1579         }
1580 
1581         private DomNode getFirstChildElement(final DomNode parent) {
1582             DomNode node = parent.getFirstChild();
1583             while (node != null && !isAccepted(node)) {
1584                 node = node.getNextSibling();
1585             }
1586             return node;
1587         }
1588 
1589         /**
1590          * Indicates if the node is accepted. If not it won't be explored at all.
1591          * @param node the node to test
1592          * @return {@code true} if accepted
1593          */
1594         private boolean isAccepted(final DomNode node) {
1595             return DomNode.class.isAssignableFrom(node.getClass());
1596         }
1597 
1598         private DomNode getNextDomSibling(final DomNode element) {
1599             DomNode node = element.getNextSibling();
1600             while (node != null && !isAccepted(node)) {
1601                 node = node.getNextSibling();
1602             }
1603             return node;
1604         }
1605     }
1606 
1607     /**
1608      * Iterates over all descendants DomTypes, in document order.
1609      */
1610     protected final class DescendantDomElementsIterator implements Iterator<DomElement> {
1611         private DomNode currentNode_;
1612         private DomNode nextNode_;
1613 
1614         /**
1615          * Creates a new instance which iterates over the specified node type.
1616          */
1617         public DescendantDomElementsIterator() {
1618             nextNode_ = getFirstChildElement(DomNode.this);
1619         }
1620 
1621         /** {@inheritDoc} */
1622         @Override
1623         public boolean hasNext() {
1624             return nextNode_ != null;
1625         }
1626 
1627         /** {@inheritDoc} */
1628         @Override
1629         public DomElement next() {
1630             return nextNode();
1631         }
1632 
1633         /** {@inheritDoc} */
1634         @Override
1635         public void remove() {
1636             if (currentNode_ == null) {
1637                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1638             }
1639             final DomNode current = currentNode_;
1640             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1641                 next();
1642             }
1643             current.remove();
1644         }
1645 
1646         /**
1647          * @return the next node, if there is one
1648          */
1649         @SuppressWarnings("unchecked")
1650         public DomElement nextNode() {
1651             currentNode_ = nextNode_;
1652 
1653             DomNode next = getFirstChildElement(nextNode_);
1654             if (next == null) {
1655                 next = getNextDomSibling(nextNode_);
1656             }
1657             if (next == null) {
1658                 next = getNextElementUpwards(nextNode_);
1659             }
1660             nextNode_ = next;
1661 
1662             return (DomElement) currentNode_;
1663         }
1664 
1665         private DomNode getNextElementUpwards(final DomNode startingNode) {
1666             if (startingNode == DomNode.this) {
1667                 return null;
1668             }
1669 
1670             DomNode parent = startingNode.getParentNode();
1671             while (parent != null && parent != DomNode.this) {
1672                 DomNode next = parent.getNextSibling();
1673                 while (next != null && !isAccepted(next)) {
1674                     next = next.getNextSibling();
1675                 }
1676                 if (next != null) {
1677                     return next;
1678                 }
1679                 parent = parent.getParentNode();
1680             }
1681             return null;
1682         }
1683 
1684         private DomNode getFirstChildElement(final DomNode parent) {
1685             DomNode node = parent.getFirstChild();
1686             while (node != null && !isAccepted(node)) {
1687                 node = node.getNextSibling();
1688             }
1689             return node;
1690         }
1691 
1692         /**
1693          * Indicates if the node is accepted. If not it won't be explored at all.
1694          * @param node the node to test
1695          * @return {@code true} if accepted
1696          */
1697         private boolean isAccepted(final DomNode node) {
1698             return DomElement.class.isAssignableFrom(node.getClass());
1699         }
1700 
1701         private DomNode getNextDomSibling(final DomNode element) {
1702             DomNode node = element.getNextSibling();
1703             while (node != null && !isAccepted(node)) {
1704                 node = node.getNextSibling();
1705             }
1706             return node;
1707         }
1708     }
1709 
1710     /**
1711      * Iterates over all descendants HtmlElements, in document order.
1712      */
1713     protected final class DescendantHtmlElementsIterator implements Iterator<HtmlElement> {
1714         private DomNode currentNode_;
1715         private DomNode nextNode_;
1716 
1717         /**
1718          * Creates a new instance which iterates over the specified node type.
1719          */
1720         public DescendantHtmlElementsIterator() {
1721             nextNode_ = getFirstChildElement(DomNode.this);
1722         }
1723 
1724         /** {@inheritDoc} */
1725         @Override
1726         public boolean hasNext() {
1727             return nextNode_ != null;
1728         }
1729 
1730         /** {@inheritDoc} */
1731         @Override
1732         public HtmlElement next() {
1733             return nextNode();
1734         }
1735 
1736         /** {@inheritDoc} */
1737         @Override
1738         public void remove() {
1739             if (currentNode_ == null) {
1740                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1741             }
1742             final DomNode current = currentNode_;
1743             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1744                 next();
1745             }
1746             current.remove();
1747         }
1748 
1749         /**
1750          * @return the next node, if there is one
1751          */
1752         @SuppressWarnings("unchecked")
1753         public HtmlElement nextNode() {
1754             currentNode_ = nextNode_;
1755 
1756             DomNode next = getFirstChildElement(nextNode_);
1757             if (next == null) {
1758                 next = getNextDomSibling(nextNode_);
1759             }
1760             if (next == null) {
1761                 next = getNextElementUpwards(nextNode_);
1762             }
1763             nextNode_ = next;
1764 
1765             return (HtmlElement) currentNode_;
1766         }
1767 
1768         private DomNode getNextElementUpwards(final DomNode startingNode) {
1769             if (startingNode == DomNode.this) {
1770                 return null;
1771             }
1772 
1773             DomNode parent = startingNode.getParentNode();
1774             while (parent != null && parent != DomNode.this) {
1775                 DomNode next = parent.getNextSibling();
1776                 while (next != null && !isAccepted(next)) {
1777                     next = next.getNextSibling();
1778                 }
1779                 if (next != null) {
1780                     return next;
1781                 }
1782                 parent = parent.getParentNode();
1783             }
1784             return null;
1785         }
1786 
1787         private DomNode getFirstChildElement(final DomNode parent) {
1788             DomNode node = parent.getFirstChild();
1789             while (node != null && !isAccepted(node)) {
1790                 node = node.getNextSibling();
1791             }
1792             return node;
1793         }
1794 
1795         /**
1796          * Indicates if the node is accepted. If not it won't be explored at all.
1797          * @param node the node to test
1798          * @return {@code true} if accepted
1799          */
1800         private boolean isAccepted(final DomNode node) {
1801             return HtmlElement.class.isAssignableFrom(node.getClass());
1802         }
1803 
1804         private DomNode getNextDomSibling(final DomNode element) {
1805             DomNode node = element.getNextSibling();
1806             while (node != null && !isAccepted(node)) {
1807                 node = node.getNextSibling();
1808             }
1809             return node;
1810         }
1811     }
1812 
1813     /**
1814      * Returns this node's ready state (IE only).
1815      * @return this node's ready state
1816      */
1817     public String getReadyState() {
1818         return readyState_;
1819     }
1820 
1821     /**
1822      * Sets this node's ready state (IE only).
1823      * @param state this node's ready state
1824      */
1825     public void setReadyState(final String state) {
1826         readyState_ = state;
1827     }
1828 
1829     /**
1830      * Evaluates the specified XPath expression from this node, returning the matching elements.
1831      * <br>
1832      * Note: This implies that the ',' point to this node but the general axis like '//' are still
1833      * looking at the whole document. E.g. if you like to get all child h1 nodes from the current one
1834      * you have to use './/h1' instead of '//h1' because the latter matches all h1 nodes of the#
1835      * whole document.
1836      *
1837      * @param <T> the expected type
1838      * @param xpathExpr the XPath expression to evaluate
1839      * @return the elements which match the specified XPath expression
1840      * @see #getFirstByXPath(String)
1841      * @see #getCanonicalXPath()
1842      */
1843     public <T> List<T> getByXPath(final String xpathExpr) {
1844         return XPathHelper.getByXPath(this, xpathExpr, null);
1845     }
1846 
1847     /**
1848      * Evaluates the specified XPath expression from this node, returning the matching elements.
1849      *
1850      * @param xpathExpr the XPath expression to evaluate
1851      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1852      * @return the elements which match the specified XPath expression
1853      * @see #getFirstByXPath(String)
1854      * @see #getCanonicalXPath()
1855      */
1856     public List<?> getByXPath(final String xpathExpr, final PrefixResolver resolver) {
1857         return XPathHelper.getByXPath(this, xpathExpr, resolver);
1858     }
1859 
1860     /**
1861      * Evaluates the specified XPath expression from this node, returning the first matching element,
1862      * or {@code null} if no node matches the specified XPath expression.
1863      *
1864      * @param xpathExpr the XPath expression
1865      * @param <X> the expression type
1866      * @return the first element matching the specified XPath expression
1867      * @see #getByXPath(String)
1868      * @see #getCanonicalXPath()
1869      */
1870     public <X> X getFirstByXPath(final String xpathExpr) {
1871         return getFirstByXPath(xpathExpr, null);
1872     }
1873 
1874     /**
1875      * Evaluates the specified XPath expression from this node, returning the first matching element,
1876      * or {@code null} if no node matches the specified XPath expression.
1877      *
1878      * @param xpathExpr the XPath expression
1879      * @param <X> the expression type
1880      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1881      * @return the first element matching the specified XPath expression
1882      * @see #getByXPath(String)
1883      * @see #getCanonicalXPath()
1884      */
1885     @SuppressWarnings("unchecked")
1886     public <X> X getFirstByXPath(final String xpathExpr, final PrefixResolver resolver) {
1887         final List<?> results = getByXPath(xpathExpr, resolver);
1888         if (results.isEmpty()) {
1889             return null;
1890         }
1891         return (X) results.get(0);
1892     }
1893 
1894     /**
1895      * <p>Returns the canonical XPath expression which identifies this node, for instance
1896      * <code>"/html/body/table[3]/tbody/tr[5]/td[2]/span/a[3]"</code>.</p>
1897      *
1898      * <p><span style="color:red">WARNING:</span> This sort of automated XPath expression
1899      * is often quite bad at identifying a node, as it is highly sensitive to changes in
1900      * the DOM tree.</p>
1901      *
1902      * @return the canonical XPath expression which identifies this node
1903      * @see #getByXPath(String)
1904      */
1905     public String getCanonicalXPath() {
1906         throw new RuntimeException("Method getCanonicalXPath() not implemented for nodes of type " + getNodeType());
1907     }
1908 
1909     /**
1910      * Notifies the registered {@link IncorrectnessListener} of something that is not fully correct.
1911      * @param message the notification to send to the registered {@link IncorrectnessListener}
1912      */
1913     protected void notifyIncorrectness(final String message) {
1914         final WebClient client = getPage().getEnclosingWindow().getWebClient();
1915         final IncorrectnessListener incorrectnessListener = client.getIncorrectnessListener();
1916         incorrectnessListener.notify(message, this);
1917     }
1918 
1919     /**
1920      * Adds a {@link DomChangeListener} to the listener list. The listener is registered for
1921      * all descendants of this node.
1922      *
1923      * @param listener the DOM structure change listener to be added
1924      * @see #removeDomChangeListener(DomChangeListener)
1925      */
1926     public void addDomChangeListener(final DomChangeListener listener) {
1927         WebAssert.notNull("listener", listener);
1928 
1929         synchronized (this) {
1930             if (domListeners_ == null) {
1931                 domListeners_ = new ArrayList<>();
1932             }
1933             domListeners_.add(listener);
1934 
1935             final SgmlPage page = getPage();
1936             if (page != null) {
1937                 page.domChangeListenerAdded();
1938             }
1939         }
1940     }
1941 
1942     /**
1943      * Removes a {@link DomChangeListener} from the listener list. The listener is deregistered for
1944      * all descendants of this node.
1945      *
1946      * @param listener the DOM structure change listener to be removed
1947      * @see #addDomChangeListener(DomChangeListener)
1948      */
1949     public void removeDomChangeListener(final DomChangeListener listener) {
1950         WebAssert.notNull("listener", listener);
1951 
1952         synchronized (this) {
1953             if (domListeners_ != null) {
1954                 domListeners_.remove(listener);
1955             }
1956         }
1957     }
1958 
1959     /**
1960      * Support for reporting DOM changes. This method can be called when a node has been added, and it
1961      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
1962      *
1963      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeAdded(DomNode, DomNode)}.</p>
1964      *
1965      * @param parentNode the parent of the node that was changed
1966      * @param addedNode the node that has been added
1967      */
1968     protected void fireNodeAdded(final DomNode parentNode, final DomNode addedNode) {
1969         DomChangeEvent event = null;
1970 
1971         DomNode toInform = this;
1972         while (toInform != null) {
1973             if (toInform.domListeners_ != null) {
1974                 final List<DomChangeListener> listeners;
1975                 synchronized (toInform) {
1976                     listeners = new ArrayList<>(toInform.domListeners_);
1977                 }
1978 
1979                 if (event == null) {
1980                     event = new DomChangeEvent(parentNode, addedNode);
1981                 }
1982                 for (final DomChangeListener domChangeListener : listeners) {
1983                     domChangeListener.nodeAdded(event);
1984                 }
1985             }
1986 
1987             toInform = toInform.getParentNode();
1988         }
1989     }
1990 
1991     /**
1992      * Adds a {@link CharacterDataChangeListener} to the listener list. The listener is registered for
1993      * all descendants of this node.
1994      *
1995      * @param listener the character data change listener to be added
1996      * @see #removeCharacterDataChangeListener(CharacterDataChangeListener)
1997      */
1998     public void addCharacterDataChangeListener(final CharacterDataChangeListener listener) {
1999         WebAssert.notNull("listener", listener);
2000 
2001         synchronized (this) {
2002             if (characterDataListeners_ == null) {
2003                 characterDataListeners_ = new ArrayList<>();
2004             }
2005             characterDataListeners_.add(listener);
2006 
2007             final SgmlPage page = getPage();
2008             if (page != null) {
2009                 page.characterDataChangeListenerAdded();
2010             }
2011         }
2012     }
2013 
2014     /**
2015      * Removes a {@link CharacterDataChangeListener} from the listener list. The listener is deregistered for
2016      * all descendants of this node.
2017      *
2018      * @param listener the Character Data change listener to be removed
2019      * @see #addCharacterDataChangeListener(CharacterDataChangeListener)
2020      */
2021     public void removeCharacterDataChangeListener(final CharacterDataChangeListener listener) {
2022         WebAssert.notNull("listener", listener);
2023 
2024         synchronized (this) {
2025             if (characterDataListeners_ != null) {
2026                 characterDataListeners_.remove(listener);
2027             }
2028         }
2029     }
2030 
2031     /**
2032      * Support for reporting Character Data changes.
2033      *
2034      * <p>Note that this method recursively calls this node's parent's {@link #fireCharacterDataChanged}.</p>
2035      *
2036      * @param characterData the character data which is changed
2037      * @param oldValue the old value
2038      */
2039     protected void fireCharacterDataChanged(final DomCharacterData characterData, final String oldValue) {
2040         CharacterDataChangeEvent event = null;
2041 
2042         DomNode toInform = this;
2043         while (toInform != null) {
2044             if (toInform.characterDataListeners_ != null) {
2045                 final List<CharacterDataChangeListener> listeners;
2046                 synchronized (toInform) {
2047                     listeners = new ArrayList<>(toInform.characterDataListeners_);
2048                 }
2049 
2050                 if (event == null) {
2051                     event = new CharacterDataChangeEvent(characterData, oldValue);
2052                 }
2053                 for (final CharacterDataChangeListener domChangeListener : listeners) {
2054                     domChangeListener.characterDataChanged(event);
2055                 }
2056             }
2057 
2058             toInform = toInform.getParentNode();
2059         }
2060     }
2061 
2062     /**
2063      * Support for reporting DOM changes. This method can be called when a node has been deleted, and it
2064      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
2065      *
2066      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeDeleted(DomNode, DomNode)}.</p>
2067      *
2068      * @param parentNode the parent of the node that was changed
2069      * @param deletedNode the node that has been deleted
2070      */
2071     protected void fireNodeDeleted(final DomNode parentNode, final DomNode deletedNode) {
2072         DomChangeEvent event = null;
2073 
2074         DomNode toInform = this;
2075         while (toInform != null) {
2076             if (toInform.domListeners_ != null) {
2077                 final List<DomChangeListener> listeners;
2078                 synchronized (toInform) {
2079                     listeners = new ArrayList<>(toInform.domListeners_);
2080                 }
2081 
2082                 if (event == null) {
2083                     event = new DomChangeEvent(parentNode, deletedNode);
2084                 }
2085                 for (final DomChangeListener domChangeListener : listeners) {
2086                     domChangeListener.nodeDeleted(event);
2087                 }
2088             }
2089 
2090             toInform = toInform.getParentNode();
2091         }
2092     }
2093 
2094     /**
2095      * Retrieves all element nodes from descendants of the starting element node that match any selector
2096      * within the supplied selector strings.
2097      * @param selectors one or more CSS selectors separated by commas
2098      * @return list of all found nodes
2099      */
2100     public DomNodeList<DomNode> querySelectorAll(final String selectors) {
2101         try {
2102             final WebClient webClient = getPage().getWebClient();
2103             final SelectorList selectorList = getSelectorList(selectors, webClient);
2104 
2105             final List<DomNode> elements = new ArrayList<>();
2106             if (selectorList != null) {
2107                 for (final DomElement child : getDomElementDescendants()) {
2108                     for (final Selector selector : selectorList) {
2109                         if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, child, null, true, true)) {
2110                             elements.add(child);
2111                             break;
2112                         }
2113                     }
2114                 }
2115             }
2116             return new StaticDomNodeList(elements);
2117         }
2118         catch (final IOException e) {
2119             throw new CSSException("Error parsing CSS selectors from '" + selectors + "': " + e.getMessage(), e);
2120         }
2121     }
2122 
2123     /**
2124      * Returns the {@link SelectorList}.
2125      * @param selectors the selectors
2126      * @param webClient the {@link WebClient}
2127      * @return the {@link SelectorList}
2128      * @throws IOException if an error occurs
2129      */
2130     protected SelectorList getSelectorList(final String selectors, final WebClient webClient)
2131             throws IOException {
2132 
2133         // get us a CSS3Parser from the pool so the chance of reusing it are high
2134         try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
2135             final CSSOMParser parser = new CSSOMParser(pooledParser);
2136             final CheckErrorHandler errorHandler = new CheckErrorHandler();
2137             parser.setErrorHandler(errorHandler);
2138 
2139             final SelectorList selectorList = parser.parseSelectors(selectors);
2140             // in case of error parseSelectors returns null
2141             if (errorHandler.error() != null) {
2142                 throw new CSSException("Invalid selectors: '" + selectors + "'", errorHandler.error());
2143             }
2144 
2145             if (selectorList != null) {
2146                 CssStyleSheet.validateSelectors(selectorList, this);
2147 
2148             }
2149             return selectorList;
2150         }
2151     }
2152 
2153     /**
2154      * Returns the first element within the document that matches the specified group of selectors.
2155      * @param selectors one or more CSS selectors separated by commas
2156      * @param <N> the node type
2157      * @return null if no matches are found; otherwise, it returns the first matching element
2158      */
2159     @SuppressWarnings("unchecked")
2160     public <N extends DomNode> N querySelector(final String selectors) {
2161         final DomNodeList<DomNode> list = querySelectorAll(selectors);
2162         if (!list.isEmpty()) {
2163             return (N) list.get(0);
2164         }
2165         return null;
2166     }
2167 
2168     /**
2169      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2170      *
2171      * Indicates if this node is currently attached to the page.
2172      * @return {@code true} if the page is one ancestor of the node.
2173      */
2174     public boolean isAttachedToPage() {
2175         return attachedToPage_;
2176     }
2177 
2178     /**
2179      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2180      *
2181      * Lifecycle method to support special processing for js method importNode.
2182      * @param doc the import target document
2183      * @see org.htmlunit.javascript.host.dom.Document#importNode(
2184      * org.htmlunit.javascript.host.dom.Node, boolean)
2185      * @see HtmlScript#processImportNode(org.htmlunit.javascript.host.dom.Document)
2186      */
2187     public void processImportNode(final org.htmlunit.javascript.host.dom.Document doc) {
2188         page_ = (SgmlPage) doc.getDomNodeOrDie();
2189     }
2190 
2191     /**
2192      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2193      *
2194      * Helper for a common call sequence.
2195      * @param feature the feature to check
2196      * @return {@code true} if the currently emulated browser has this feature.
2197      */
2198     public boolean hasFeature(final BrowserVersionFeatures feature) {
2199         return getPage().getWebClient().getBrowserVersion().hasFeature(feature);
2200     }
2201 
2202     private static final class CheckErrorHandler implements CSSErrorHandler {
2203         private CSSParseException error_;
2204 
2205         CSSParseException error() {
2206             return error_;
2207         }
2208 
2209         @Override
2210         public void warning(final CSSParseException exception) throws CSSException {
2211             // ignore
2212         }
2213 
2214         @Override
2215         public void fatalError(final CSSParseException exception) throws CSSException {
2216             error_ = exception;
2217         }
2218 
2219         @Override
2220         public void error(final CSSParseException exception) throws CSSException {
2221             error_ = exception;
2222         }
2223     }
2224 
2225     /**
2226      * Indicates if the provided event can be applied to this node.
2227      * Overwrite this.
2228      * @param event the event
2229      * @return {@code false} if the event can't be applied
2230      */
2231     public boolean handles(final Event event) {
2232         return true;
2233     }
2234 
2235     /**
2236      * Returns the previous sibling element node of this element.
2237      * null if this element has no element sibling nodes that come before this one in the document tree.
2238      * @return the previous sibling element node of this element.
2239      *         null if this element has no element sibling nodes that come before this one in the document tree
2240      */
2241     public DomElement getPreviousElementSibling() {
2242         DomNode node = getPreviousSibling();
2243         while (node != null && !(node instanceof DomElement)) {
2244             node = node.getPreviousSibling();
2245         }
2246         return (DomElement) node;
2247     }
2248 
2249     /**
2250      * Returns the next sibling element node of this element.
2251      * null if this element has no element sibling nodes that come after this one in the document tree.
2252      * @return the next sibling element node of this element.
2253      *         null if this element has no element sibling nodes that come after this one in the document tree
2254      */
2255     public DomElement getNextElementSibling() {
2256         DomNode node = getNextSibling();
2257         while (node != null && !(node instanceof DomElement)) {
2258             node = node.getNextSibling();
2259         }
2260         return (DomElement) node;
2261     }
2262 
2263     /**
2264      * @param selectorString the selector to test
2265      * @return the selected {@link DomElement} or null.
2266      */
2267     public DomElement closest(final String selectorString) {
2268         try {
2269             final WebClient webClient = getPage().getWebClient();
2270             final SelectorList selectorList = getSelectorList(selectorString, webClient);
2271 
2272             DomNode current = this;
2273             if (selectorList != null) {
2274                 do {
2275                     for (final Selector selector : selectorList) {
2276                         final DomElement elem = (DomElement) current;
2277                         if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, elem, null, true, true)) {
2278                             return elem;
2279                         }
2280                     }
2281 
2282                     do {
2283                         current = current.getParentNode();
2284                     }
2285                     while (current != null && !(current instanceof DomElement));
2286                 }
2287                 while (current != null);
2288             }
2289             return null;
2290         }
2291         catch (final IOException e) {
2292             throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage(), e);
2293         }
2294     }
2295 
2296     /**
2297      * An unmodifiable empty {@link NamedNodeMap} implementation.
2298      */
2299     private static final class ReadOnlyEmptyNamedNodeMapImpl implements NamedNodeMap, Serializable {
2300 
2301         /**
2302          * {@inheritDoc}
2303          */
2304         @Override
2305         public int getLength() {
2306             return 0;
2307         }
2308 
2309         /**
2310          * {@inheritDoc}
2311          */
2312         @Override
2313         public DomAttr getNamedItem(final String name) {
2314             return null;
2315         }
2316 
2317         /**
2318          * {@inheritDoc}
2319          */
2320         @Override
2321         public Node getNamedItemNS(final String namespaceURI, final String localName) {
2322             return null;
2323         }
2324 
2325         /**
2326          * {@inheritDoc}
2327          */
2328         @Override
2329         public Node item(final int index) {
2330             return null;
2331         }
2332 
2333         /**
2334          * {@inheritDoc}
2335          */
2336         @Override
2337         public Node removeNamedItem(final String name) throws DOMException {
2338             return null;
2339         }
2340 
2341         /**
2342          * {@inheritDoc}
2343          */
2344         @Override
2345         public Node removeNamedItemNS(final String namespaceURI, final String localName) {
2346             return null;
2347         }
2348 
2349         /**
2350          * {@inheritDoc}
2351          */
2352         @Override
2353         public DomAttr setNamedItem(final Node node) {
2354             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItem");
2355         }
2356 
2357         /**
2358          * {@inheritDoc}
2359          */
2360         @Override
2361         public Node setNamedItemNS(final Node node) throws DOMException {
2362             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItemNS");
2363         }
2364     }
2365 }