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