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="https://developer.mozilla.org/en-US/docs/Web/CSS/visibility">MDN CSS visibility</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                     // the hidden attribute and the open state of a dialog are handled
705                     // by the computed display style (an explicit display style overrides both)
706                     final ComputedCssStyleDeclaration style = window.getComputedStyle(elem, null);
707                     if (DisplayStyle.NONE.value().equals(style.getDisplay())) {
708                         return false;
709                     }
710                     styles.add(style);
711                 }
712             }
713 
714             // visibility: iterate bottom to top, because children can override
715             // the visibility used by parent nodes
716             for (int i = styles.size() - 1; i >= 0; i--) {
717                 final ComputedCssStyleDeclaration style = styles.get(i);
718                 final String visibility = style.getStyleAttribute(StyleAttributes.Definition.VISIBILITY, true);
719                 if (visibility.length() > 5) {
720                     if ("visible".equals(visibility)) {
721                         return true;
722                     }
723                     if ("hidden".equals(visibility) || "collapse".equals(visibility)) {
724                         return false;
725                     }
726                 }
727             }
728         }
729         return true;
730     }
731 
732     /**
733      * Returns {@code true} if nodes of this type can ever be displayed, {@code false} otherwise. Examples of nodes
734      * that can never be displayed are <code>&lt;head&gt;</code>,
735      * <code>&lt;meta&gt;</code>, <code>&lt;script&gt;</code>, etc.
736      * @return {@code true} if nodes of this type can ever be displayed, {@code false} otherwise
737      * @see #isDisplayed()
738      */
739     public boolean mayBeDisplayed() {
740         return true;
741     }
742 
743     /**
744      * Returns a normalized textual representation of this element that represents
745      * what would be visible to the user if this page was shown in a web browser.
746      * Whitespace is normalized like in the browser and block tags are separated by '\n'.
747      *
748      * @return a normalized textual representation of this element
749      */
750     public String asNormalizedText() {
751         final HtmlSerializerNormalizedText ser = new HtmlSerializerNormalizedText();
752         return ser.asText(this);
753     }
754 
755     /**
756      * Returns a textual representation of this element in the same way as
757      * the selenium/WebDriver WebElement#getText() property does.<br>
758      * see <a href="https://w3c.github.io/webdriver/#get-element-text">get-element-text</a> and
759      * <a href="https://w3c.github.io/webdriver/#dfn-bot-dom-getvisibletext">dfn-bot-dom-getvisibletext</a>
760      * Note: this is different from {@link #asNormalizedText()}
761      *
762      * @return a textual representation of this element that represents what would
763      *         be visible to the user if this page was shown in a web browser
764      */
765     public String getVisibleText() {
766         final HtmlSerializerVisibleText ser = new HtmlSerializerVisibleText();
767         return ser.asText(this);
768     }
769 
770     /**
771      * Returns a string representation as XML document from this element and all it's children (recursively).<br>
772      * The charset used in the xml header is the current page encoding; but the result is still a string.
773      * You have to make sure to use the correct (in fact the same) encoding if you write this to a file.<br>
774      * This serializes the current state of the DomTree - this implies that the content of noscript tags
775      * usually serialized as string because the content is converted during parsing (if js was enabled at that time).
776      * @return the XML string
777      */
778     public String asXml() {
779         Charset charsetName = null;
780         final HtmlPage htmlPage = getHtmlPageOrNull();
781         if (htmlPage != null) {
782             charsetName = htmlPage.getCharset();
783         }
784 
785         final StringWriter stringWriter = new StringWriter();
786         try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
787             boolean tag = false;
788             if (charsetName != null && this instanceof HtmlHtml) {
789                 printWriter.print("<?xml version=\"1.0\" encoding=\"");
790                 printWriter.print(charsetName);
791                 printWriter.print("\"?>");
792                 tag = true;
793             }
794             printXml("", tag, printWriter);
795             return stringWriter.toString().trim();
796         }
797     }
798 
799     /**
800      * Recursively writes the XML data for the node tree starting at <code>node</code>.
801      *
802      * @param indent white space to indent child nodes
803      * @param indentBefore if true start a new line before outputting
804      * @param printWriter writer where child nodes are written
805      * @return true if the last thing printed was a tag
806      */
807     protected boolean printXml(final String indent, final boolean indentBefore, final PrintWriter printWriter) {
808         if (indentBefore) {
809             printWriter.print("\r\n");
810             printWriter.print(indent);
811         }
812         printWriter.print(this);
813         return printChildrenAsXml(indent, false, printWriter);
814     }
815 
816     /**
817      * Recursively writes the XML data for the node tree starting at <code>node</code>.
818      *
819      * @param indent white space to indent child nodes
820      * @param tagBefore true if the last thing printed was a tag
821      * @param printWriter writer where child nodes are written
822      * @return true if the last thing printed was a tag
823      */
824     protected boolean printChildrenAsXml(final String indent, final boolean tagBefore, final PrintWriter printWriter) {
825         DomNode child = getFirstChild();
826         boolean tag = tagBefore;
827         while (child != null) {
828             tag = child.printXml(indent + "  ", tag, printWriter);
829             child = child.getNextSibling();
830         }
831         return tag;
832     }
833 
834     /**
835      * {@inheritDoc}
836      */
837     @Override
838     public String getNodeValue() {
839         return null;
840     }
841 
842     /**
843      * {@inheritDoc}
844      */
845     @Override
846     public DomNode cloneNode(final boolean deep) {
847         final DomNode newnode;
848         try {
849             newnode = (DomNode) clone();
850         }
851         catch (final CloneNotSupportedException e) {
852             throw new IllegalStateException("Clone not supported for node [" + this + "]", e);
853         }
854 
855         newnode.parent_ = null;
856         newnode.nextSibling_ = null;
857         newnode.previousSibling_ = null;
858         newnode.scriptObject_ = null;
859         newnode.firstChild_ = null;
860         newnode.attachedToPage_ = false;
861 
862         // make sure isBodyParsed() returns true
863         newnode.startLineNumber_ = -1;
864         newnode.endLineNumber_ = -1;
865 
866         // if deep, clone the children too.
867         if (deep) {
868             for (DomNode child = firstChild_; child != null; child = child.nextSibling_) {
869                 newnode.appendChild(child.cloneNode(true));
870             }
871         }
872 
873         return newnode;
874     }
875 
876     /**
877      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
878      *
879      * <p>Returns the JavaScript object that corresponds to this node, lazily initializing a new one if necessary.</p>
880      *
881      * <p>The logic of when and where the JavaScript object is created needs a cleanup: functions using
882      * a DOM node's JavaScript object should not have to check if they should create it first.</p>
883      *
884      * @param <T> the object type
885      * @return the JavaScript object that corresponds to this node
886      */
887     @SuppressWarnings("unchecked")
888     public <T extends HtmlUnitScriptable> T getScriptableObject() {
889         if (scriptObject_ == null) {
890             final SgmlPage page = getPage();
891             if (this == page) {
892                 final StringBuilder msg = new StringBuilder("No script object associated with the Page.");
893                 // because this is a strange case we like to provide as much info as possible
894                 msg.append(" class: '")
895                     .append(page.getClass().getName())
896                     .append('\'');
897                 try {
898                     msg.append(" url: '")
899                         .append(page.getUrl()).append("' content: ")
900                         .append(page.getWebResponse().getContentAsString());
901                 }
902                 catch (final Exception e) {
903                     // ok bad luck with detail
904                     msg.append(" no details: '").append(e).append('\'');
905                 }
906                 throw new IllegalStateException(msg.toString());
907             }
908             scriptObject_ = page.getScriptableObject().makeScriptableFor(this);
909         }
910         return (T) scriptObject_;
911     }
912 
913     /**
914      * {@inheritDoc}
915      */
916     @Override
917     public DomNode appendChild(final Node node) {
918         if (node == this) {
919             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add not to itself " + this);
920         }
921         final DomNode domNode = (DomNode) node;
922         if (domNode.isAncestorOf(this)) {
923             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add (grand)parent to itself " + this);
924         }
925 
926         if (domNode instanceof DomDocumentFragment fragment) {
927             for (final DomNode child : fragment.getChildren()) {
928                 appendChild(child);
929             }
930         }
931         else {
932             // clean up the new node, in case it is being moved
933             if (domNode.getParentNode() != null) {
934                 domNode.detach();
935             }
936 
937             basicAppend(domNode);
938 
939             fireAddition(domNode);
940         }
941 
942         return domNode;
943     }
944 
945     /**
946      * Appends the specified node to the end of this node's children, assuming the specified
947      * node is clean (doesn't have preexisting relationships to other nodes).
948      *
949      * @param node the node to append to this node's children
950      */
951     private void basicAppend(final DomNode node) {
952         // try to make the node setup as complete as possible
953         // before the node is reachable
954         node.setPage(getPage());
955         node.parent_ = this;
956 
957         if (firstChild_ == null) {
958             firstChild_ = node;
959         }
960         else {
961             final DomNode last = getLastChild();
962             node.previousSibling_ = last;
963             node.nextSibling_ = null; // safety first
964 
965             last.nextSibling_ = node;
966         }
967         firstChild_.previousSibling_ = node;
968     }
969 
970     /**
971      * {@inheritDoc}
972      */
973     @Override
974     public Node insertBefore(final Node newChild, final Node refChild) {
975         if (newChild instanceof DomDocumentFragment fragment) {
976             for (final DomNode child : fragment.getChildren()) {
977                 insertBefore(child, refChild);
978             }
979             return newChild;
980         }
981 
982         if (refChild == null) {
983             appendChild(newChild);
984             return newChild;
985         }
986 
987         if (refChild.getParentNode() != this) {
988             throw new DOMException(DOMException.NOT_FOUND_ERR, "Reference node is not a child of this node.");
989         }
990 
991         ((DomNode) refChild).insertBefore((DomNode) newChild);
992         return newChild;
993     }
994 
995     /**
996      * Inserts the specified node as a new child node before this node into the child relationship this node is a
997      * part of. If the specified node is this node, this method is a no-op.
998      *
999      * @param newNode the new node to insert
1000      */
1001     public void insertBefore(final DomNode newNode) {
1002         if (previousSibling_ == null) {
1003             throw new IllegalStateException("Previous sibling for " + this + " is null.");
1004         }
1005 
1006         if (newNode == this) {
1007             return;
1008         }
1009 
1010         if (newNode instanceof DomDocumentFragment) {
1011             for (final DomNode child : newNode.getChildren()) {
1012                 insertBefore(child);
1013             }
1014             return;
1015         }
1016 
1017         // clean up the new node, in case it is being moved
1018         if (newNode.getParentNode() != null) {
1019             newNode.detach();
1020         }
1021 
1022         basicInsertBefore(newNode);
1023 
1024         fireAddition(newNode);
1025     }
1026 
1027     /**
1028      * Inserts the specified node into this node's parent's children right before this node, assuming the specified
1029      * node is clean (doesn't have preexisting relationships to other nodes).
1030      *
1031      * @param node the node to insert before this node
1032      */
1033     private void basicInsertBefore(final DomNode node) {
1034         // try to make the node setup as complete as possible
1035         // before the node is reachable
1036         node.setPage(page_);
1037         node.parent_ = parent_;
1038         node.previousSibling_ = previousSibling_;
1039         node.nextSibling_ = this;
1040 
1041         if (parent_.firstChild_ == this) {
1042             parent_.firstChild_ = node;
1043         }
1044         else {
1045             previousSibling_.nextSibling_ = node;
1046         }
1047         previousSibling_ = node;
1048     }
1049 
1050     private void fireAddition(final DomNode domNode) {
1051         final boolean wasAlreadyAttached = domNode.isAttachedToPage();
1052         domNode.attachedToPage_ = isAttachedToPage();
1053 
1054         final SgmlPage page = getPage();
1055         if (domNode.attachedToPage_) {
1056             // trigger events
1057             if (null != page && page.isHtmlPage()) {
1058                 ((HtmlPage) page).notifyNodeAdded(domNode);
1059             }
1060 
1061             // a node that is already "complete" (ie not being parsed) and not yet attached
1062             if (!domNode.isBodyParsed() && !wasAlreadyAttached) {
1063                 if (domNode.getFirstChild() != null) {
1064                     for (final Iterator<DomNode> iterator =
1065                             domNode.new DescendantDomNodesIterator(); iterator.hasNext();) {
1066                         final DomNode child = iterator.next();
1067                         child.attachedToPage_ = true;
1068                         child.onAllChildrenAddedToPage(true);
1069                     }
1070                 }
1071                 domNode.onAllChildrenAddedToPage(true);
1072             }
1073         }
1074 
1075         if (this instanceof DomDocumentFragment) {
1076             onAddedToDocumentFragment();
1077         }
1078 
1079         if (page == null || page.isDomChangeListenerInUse()) {
1080             fireNodeAdded(this, domNode);
1081         }
1082     }
1083 
1084     /**
1085      * Indicates if the current node is being parsed. This means that the opening tag has already been
1086      * parsed but not the body and end tag.
1087      */
1088     private boolean isBodyParsed() {
1089         return getStartLineNumber() != -1 && getEndLineNumber() == -1;
1090     }
1091 
1092     /**
1093      * Recursively sets the new page on the node and its children.
1094      * @param newPage the new owning page
1095      */
1096     private void setPage(final SgmlPage newPage) {
1097         if (page_ == newPage) {
1098             return; // nothing to do
1099         }
1100 
1101         page_ = newPage;
1102         for (final DomNode node : getChildren()) {
1103             node.setPage(newPage);
1104         }
1105     }
1106 
1107     /**
1108      * {@inheritDoc}
1109      */
1110     @Override
1111     public Node removeChild(final Node child) {
1112         if (child.getParentNode() != this) {
1113             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1114         }
1115         ((DomNode) child).remove();
1116         return child;
1117     }
1118 
1119     /**
1120      * Removes all of this node's children.
1121      */
1122     public void removeAllChildren() {
1123         while (getFirstChild() != null) {
1124             getFirstChild().remove();
1125         }
1126     }
1127 
1128     /**
1129      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1130      *
1131      * Parses the specified HTML source code, appending the resulting content at the specified target location.
1132      * @param source the HTML code extract to parse
1133      * @throws IOException in case of error
1134      * @throws SAXException in case of error
1135      */
1136     public void parseHtmlSnippet(final String source) throws SAXException, IOException {
1137         final WebClient webClient = getPage().getWebClient();
1138         webClient.getPageCreator().getHtmlParser().parseFragment(webClient, this, this, source, false);
1139     }
1140 
1141     /**
1142      * Removes this node from all relationships with other nodes.
1143      */
1144     public void remove() {
1145         // same as detach for the moment
1146         detach();
1147     }
1148 
1149     /**
1150      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1151      *
1152      * Detach this node from all relationships with other nodes.
1153      * This is the first step of a move.
1154      */
1155     protected void detach() {
1156         final DomNode exParent = parent_;
1157 
1158         basicRemove();
1159 
1160         fireRemoval(exParent);
1161     }
1162 
1163     /**
1164      * Cuts off all relationships this node has with siblings and parents.
1165      */
1166     protected void basicRemove() {
1167         basicDetach();
1168 
1169         nextSibling_ = null;
1170         previousSibling_ = null;
1171         parent_ = null;
1172         attachedToPage_ = false;
1173         for (final DomNode descendant : getDescendants()) {
1174             descendant.attachedToPage_ = false;
1175         }
1176     }
1177 
1178     /**
1179      * Cuts off all relationships this node has with siblings and parents.
1180      */
1181     private void basicDetach() {
1182         if (parent_ != null && parent_.firstChild_ == this) {
1183             parent_.firstChild_ = nextSibling_;
1184         }
1185         else if (previousSibling_ != null && previousSibling_.nextSibling_ == this) {
1186             previousSibling_.nextSibling_ = nextSibling_;
1187         }
1188         if (nextSibling_ != null && nextSibling_.previousSibling_ == this) {
1189             nextSibling_.previousSibling_ = previousSibling_;
1190         }
1191         if (parent_ != null && parent_.getLastChild() == this) {
1192             parent_.firstChild_.previousSibling_ = previousSibling_;
1193         }
1194     }
1195 
1196     private void fireRemoval(final DomNode exParent) {
1197         final SgmlPage page = getPage();
1198         if (page instanceof HtmlPage htmlPage) {
1199             // some actions executed on removal need an intact parent relationship (e.g. for the
1200             // DocumentPositionComparator) so we have to restore it temporarily
1201             parent_ = exParent;
1202             htmlPage.notifyNodeRemoved(this);
1203             parent_ = null;
1204         }
1205 
1206         if (exParent != null && (page == null || page.isDomChangeListenerInUse())) {
1207             fireNodeDeleted(exParent, this);
1208             // ask ex-parent to fire event (because we don't have parent now)
1209             exParent.fireNodeDeleted(exParent, this);
1210         }
1211     }
1212 
1213     /**
1214      * {@inheritDoc}
1215      */
1216     @Override
1217     public Node replaceChild(final Node newChild, final Node oldChild) {
1218         if (oldChild.getParentNode() != this) {
1219             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1220         }
1221         ((DomNode) oldChild).replace((DomNode) newChild);
1222         return oldChild;
1223     }
1224 
1225     /**
1226      * Replaces this node with another node. If the specified node is this node, this
1227      * method is a no-op.
1228      * @param newNode the node to replace this one
1229      */
1230     public void replace(final DomNode newNode) {
1231         if (newNode != this) {
1232             final DomNode exParent = parent_;
1233             final DomNode exNextSibling = nextSibling_;
1234 
1235             remove();
1236 
1237             exParent.insertBefore(newNode, exNextSibling);
1238         }
1239     }
1240 
1241     /**
1242      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1243      *
1244      * Quietly removes this node and moves its children to the specified destination. "Quietly" means
1245      * that no node events are fired. This method is not appropriate for most use cases. It should
1246      * only be used in specific cases for HTML parsing hackery.
1247      *
1248      * @param destination the node to which this node's children should be moved before this node is removed
1249      */
1250     public void quietlyRemoveAndMoveChildrenTo(final DomNode destination) {
1251         if (destination.getPage() != getPage()) {
1252             throw new RuntimeException("Cannot perform quiet move on nodes from different pages.");
1253         }
1254         for (final DomNode child : getChildren()) {
1255             if (child != destination) {
1256                 child.basicRemove();
1257                 destination.basicAppend(child);
1258             }
1259         }
1260         basicRemove();
1261     }
1262 
1263     /**
1264      * Check for insertion errors for a new child node. This is overridden by derived
1265      * classes to enforce which types of children are allowed.
1266      *
1267      * @param newChild the new child node that is being inserted below this node
1268      * @throws DOMException HIERARCHY_REQUEST_ERR: Raised if this node is of a type that does
1269      *         not allow children of the type of the newChild node, or if the node to insert is one of
1270      *         this node's ancestors or this node itself, or if this node is of type Document and the
1271      *         DOM application attempts to insert a second DocumentType or Element node.
1272      *         WRONG_DOCUMENT_ERR: Raised if newChild was created from a different document than the
1273      *         one that created this node.
1274      */
1275     protected void checkChildHierarchy(final Node newChild) throws DOMException {
1276         Node parentNode = this;
1277         while (parentNode != null) {
1278             if (parentNode == newChild) {
1279                 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Child node is already a parent.");
1280             }
1281             parentNode = parentNode.getParentNode();
1282         }
1283         final Document thisDocument = getOwnerDocument();
1284         final Document childDocument = newChild.getOwnerDocument();
1285         if (childDocument != thisDocument && childDocument != null) {
1286             throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Child node " + newChild.getNodeName()
1287                 + " is not in the same Document as this " + getNodeName() + ".");
1288         }
1289     }
1290 
1291     /**
1292      * Lifecycle method invoked whenever a node is added to a page. Intended to
1293      * be overridden by nodes which need to perform custom logic when they are
1294      * added to a page. This method is recursive, so if you override it, please
1295      * be sure to call <code>super.onAddedToPage()</code>.
1296      */
1297     protected void onAddedToPage() {
1298         if (firstChild_ != null) {
1299             for (final DomNode child : getChildren()) {
1300                 child.onAddedToPage();
1301             }
1302         }
1303     }
1304 
1305     /**
1306      * Lifecycle method invoked after a node and all its children have been added to a page, during
1307      * parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic
1308      * after they and all their child nodes have been processed by the HTML parser. This method is
1309      * not recursive, and the default implementation is empty, so there is no need to call
1310      * <code>super.onAllChildrenAddedToPage()</code> if you implement this method.
1311      * @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no
1312      */
1313     public void onAllChildrenAddedToPage(final boolean postponed) {
1314         // Empty by default.
1315     }
1316 
1317     /**
1318      * Lifecycle method invoked whenever a node is added to a document fragment. Intended to
1319      * be overridden by nodes which need to perform custom logic when they are
1320      * added to a fragment. This method is recursive, so if you override it, please
1321      * be sure to call <code>super.onAddedToDocumentFragment()</code>.
1322      */
1323     protected void onAddedToDocumentFragment() {
1324         if (firstChild_ != null) {
1325             for (final DomNode child : getChildren()) {
1326                 child.onAddedToDocumentFragment();
1327             }
1328         }
1329     }
1330 
1331     /**
1332      * Add a DOM node as a child to this node before the referenced node.
1333      * If the referenced node is null, append to the end.
1334      * @param movedDomNode the node to move
1335      * @param referenceDomNode the node to move before
1336      * @throws DOMException in case of problems
1337      */
1338     public void moveBefore(final DomNode movedDomNode, final DomNode referenceDomNode) {
1339         if (movedDomNode == referenceDomNode) {
1340             return;
1341         }
1342 
1343         if (movedDomNode instanceof DomDocumentFragment fragment) {
1344             for (final DomNode child : fragment.getChildren()) {
1345                 moveBefore(child, referenceDomNode);
1346             }
1347             return;
1348         }
1349 
1350         // If moving to the same position (node is already right before referenceNode), no operation needed
1351         if (referenceDomNode != null && movedDomNode.getNextSibling() == referenceDomNode) {
1352             return;
1353         }
1354 
1355         if (movedDomNode.isAncestorOf(this)) {
1356             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1357                     "The new child element contains the parent.");
1358         }
1359 
1360         if (referenceDomNode != null && !this.isAncestorOf(referenceDomNode)) {
1361             throw new DOMException(DOMException.NOT_FOUND_ERR,
1362                     "The node before which the new node is to be inserted is not a child of this node.");
1363         }
1364 
1365         if (referenceDomNode != null && referenceDomNode.isAttachedToPage() && !movedDomNode.isAttachedToPage()) {
1366             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1367                     "State-preserving atomic move cannot be performed on nodes participating in an invalid hierarchy.");
1368         }
1369 
1370         if (referenceDomNode == null) {
1371             appendChild(movedDomNode);
1372             return;
1373         }
1374 
1375         referenceDomNode.moveBefore(movedDomNode);
1376     }
1377 
1378     /**
1379      * Inserts the specified node as a new child node before this node into the child relationship this node is a
1380      * part of. If the specified node is this node, this method is a no-op.
1381      *
1382      * @param movedDomNode the node to move before the current node
1383      */
1384     public void moveBefore(final DomNode movedDomNode) {
1385         if (previousSibling_ == null) {
1386             throw new IllegalStateException("Previous sibling for " + this + " is null.");
1387         }
1388 
1389         if (movedDomNode == this) {
1390             return;
1391         }
1392 
1393         movedDomNode.detach();
1394         basicInsertBefore(movedDomNode);
1395 
1396         fireAddition(movedDomNode);
1397     }
1398 
1399     /**
1400      * Returns an iterable over the children of this node.
1401      *
1402      * @return an {@link Iterable} over the children of this node
1403      */
1404     public final Iterable<DomNode> getChildren() {
1405         return () -> new ChildIterator(firstChild_);
1406     }
1407 
1408     /**
1409      * An iterator over all children of this node.
1410      */
1411     protected static class ChildIterator implements Iterator<DomNode> {
1412 
1413         private DomNode nextNode_;
1414         private DomNode currentNode_;
1415 
1416         public ChildIterator(final DomNode nextNode) {
1417             nextNode_ = nextNode;
1418         }
1419 
1420         /** {@inheritDoc} */
1421         @Override
1422         public boolean hasNext() {
1423             return nextNode_ != null;
1424         }
1425 
1426         /** {@inheritDoc} */
1427         @Override
1428         public DomNode next() {
1429             if (nextNode_ != null) {
1430                 currentNode_ = nextNode_;
1431                 nextNode_ = nextNode_.nextSibling_;
1432                 return currentNode_;
1433             }
1434             throw new NoSuchElementException();
1435         }
1436 
1437         /** {@inheritDoc} */
1438         @Override
1439         public void remove() {
1440             if (currentNode_ == null) {
1441                 throw new IllegalStateException();
1442             }
1443             currentNode_.remove();
1444         }
1445     }
1446 
1447     /**
1448      * Returns an {@link Iterable} that will recursively iterate over all of this node's descendants,
1449      * including {@link DomText} elements, {@link DomComment} elements, etc. If you want to iterate
1450      * only over {@link HtmlElement} descendants, please use {@link #getHtmlElementDescendants()}.
1451      * @return an {@link Iterable} that will recursively iterate over all of this node's descendants
1452      */
1453     public final Iterable<DomNode> getDescendants() {
1454         return () -> new DescendantDomNodesIterator();
1455     }
1456 
1457     /**
1458      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1459      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1460      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1461      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1462      *         descendants
1463      * @see #getDomElementDescendants()
1464      */
1465     public final Iterable<HtmlElement> getHtmlElementDescendants() {
1466         return () -> new DescendantHtmlElementsIterator();
1467     }
1468 
1469     /**
1470      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1471      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1472      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1473      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1474      *         descendants
1475      * @see #getHtmlElementDescendants()
1476      */
1477     public final Iterable<DomElement> getDomElementDescendants() {
1478         return () -> new DescendantDomElementsIterator();
1479     }
1480 
1481     /**
1482      * Iterates over all descendants DomNodes, in document order.
1483      */
1484     protected final class DescendantDomNodesIterator implements Iterator<DomNode> {
1485         private DomNode currentNode_;
1486         private DomNode nextNode_;
1487 
1488         /**
1489          * Creates a new instance which iterates over the specified node type.
1490          */
1491         public DescendantDomNodesIterator() {
1492             nextNode_ = DomNode.this.getFirstChild();
1493         }
1494 
1495         /** {@inheritDoc} */
1496         @Override
1497         public boolean hasNext() {
1498             return nextNode_ != null;
1499         }
1500 
1501         /** {@inheritDoc} */
1502         @Override
1503         public DomNode next() {
1504             return nextNode();
1505         }
1506 
1507         /** {@inheritDoc} */
1508         @Override
1509         public void remove() {
1510             if (currentNode_ == null) {
1511                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1512             }
1513             final DomNode current = currentNode_;
1514             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1515                 next();
1516             }
1517             current.remove();
1518         }
1519 
1520         /**
1521          * Returns the next node in the iteration.
1522          *
1523          * @return the next node, or {@code null} if there are no more nodes
1524          */
1525         public DomNode nextNode() {
1526             currentNode_ = nextNode_;
1527 
1528             DomNode next = nextNode_.getFirstChild();
1529             if (next == null) {
1530                 next = nextNode_.getNextSibling();
1531             }
1532             if (next == null) {
1533                 next = getNextElementUpwards(nextNode_);
1534             }
1535             nextNode_ = next;
1536 
1537             return currentNode_;
1538         }
1539 
1540         private DomNode getNextElementUpwards(final DomNode startingNode) {
1541             if (startingNode == DomNode.this) {
1542                 return null;
1543             }
1544 
1545             DomNode parent = startingNode.getParentNode();
1546             while (parent != null && parent != DomNode.this) {
1547                 final DomNode next = parent.getNextSibling();
1548                 if (next != null) {
1549                     return next;
1550                 }
1551                 parent = parent.getParentNode();
1552             }
1553             return null;
1554         }
1555     }
1556 
1557     /**
1558      * Iterates over all descendants DomElements, in document order.
1559      *
1560      * @param <T> the element type
1561      */
1562     protected abstract class AbstractDescendantIterator<T extends DomNode> implements Iterator<T> {
1563         private DomNode currentNode_;
1564         private DomNode nextNode_;
1565 
1566         /**
1567          * Creates a new instance which iterates over the specified node type.
1568          */
1569         protected AbstractDescendantIterator() {
1570             nextNode_ = getFirstChildElement(DomNode.this);
1571         }
1572 
1573         /** {@inheritDoc} */
1574         @Override
1575         public boolean hasNext() {
1576             return nextNode_ != null;
1577         }
1578 
1579         /** {@inheritDoc} */
1580         @Override
1581         public T next() {
1582             return nextNode();
1583         }
1584 
1585         /** {@inheritDoc} */
1586         @Override
1587         public void remove() {
1588             if (currentNode_ == null) {
1589                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1590             }
1591             final DomNode current = currentNode_;
1592             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1593                 next();
1594             }
1595             current.remove();
1596         }
1597 
1598         /**
1599          * Returns the next node in the iteration.
1600          *
1601          * @return the next node, or {@code null} if there are no more nodes
1602          */
1603         @SuppressWarnings("unchecked")
1604         public T nextNode() {
1605             currentNode_ = nextNode_;
1606 
1607             DomNode next = getFirstChildElement(nextNode_);
1608             if (next == null) {
1609                 next = getNextDomSibling(nextNode_);
1610             }
1611             if (next == null) {
1612                 next = getNextElementUpwards(nextNode_);
1613             }
1614             nextNode_ = next;
1615 
1616             return (T) currentNode_;
1617         }
1618 
1619         private DomNode getNextElementUpwards(final DomNode startingNode) {
1620             if (startingNode == DomNode.this) {
1621                 return null;
1622             }
1623 
1624             DomNode parent = startingNode.getParentNode();
1625             while (parent != null && parent != DomNode.this) {
1626                 DomNode next = parent.getNextSibling();
1627                 while (next != null && !isAccepted(next)) {
1628                     next = next.getNextSibling();
1629                 }
1630                 if (next != null) {
1631                     return next;
1632                 }
1633                 parent = parent.getParentNode();
1634             }
1635             return null;
1636         }
1637 
1638         private DomNode getFirstChildElement(final DomNode parent) {
1639             DomNode node = parent.getFirstChild();
1640             while (node != null && !isAccepted(node)) {
1641                 node = node.getNextSibling();
1642             }
1643             return node;
1644         }
1645 
1646         /**
1647          * Indicates if the node is accepted. If not it won't be explored at all.
1648          *
1649          * @param node the node to test
1650          * @return {@code true} if accepted
1651          */
1652         protected abstract boolean isAccepted(DomNode node);
1653 
1654         private DomNode getNextDomSibling(final DomNode element) {
1655             DomNode node = element.getNextSibling();
1656             while (node != null && !isAccepted(node)) {
1657                 node = node.getNextSibling();
1658             }
1659             return node;
1660         }
1661     }
1662 
1663     /**
1664      * Iterates over all descendants DomTypes, in document order.
1665      */
1666     protected final class DescendantDomElementsIterator extends AbstractDescendantIterator<DomElement> {
1667         /**
1668          * {@inheritDoc}
1669          */
1670         @Override
1671         protected boolean isAccepted(final DomNode node) {
1672             return node instanceof DomElement;
1673         }
1674     }
1675 
1676     /**
1677      * Iterates over all descendants DomTypes, in document order.
1678      */
1679     protected final class DescendantHtmlElementsIterator extends AbstractDescendantIterator<HtmlElement> {
1680         /**
1681          * {@inheritDoc}
1682          */
1683         @Override
1684         protected boolean isAccepted(final DomNode node) {
1685             return node instanceof HtmlElement;
1686         }
1687     }
1688 
1689     /**
1690      * Returns this node's ready state (IE only).
1691      * @return this node's ready state
1692      */
1693     public String getReadyState() {
1694         return readyState_;
1695     }
1696 
1697     /**
1698      * Sets this node's ready state (IE only).
1699      * @param state this node's ready state
1700      */
1701     public void setReadyState(final String state) {
1702         readyState_ = state;
1703     }
1704 
1705     /**
1706      * Evaluates the specified XPath expression from this node, returning the matching elements.
1707      * <br>
1708      * Note: This implies that the ',' point to this node but the general axis like '//' are still
1709      * looking at the whole document. E.g. if you like to get all child h1 nodes from the current one
1710      * you have to use './/h1' instead of '//h1' because the latter matches all h1 nodes of the#
1711      * whole document.
1712      *
1713      * @param <T> the expected type
1714      * @param xpathExpr the XPath expression to evaluate
1715      * @return the elements which match the specified XPath expression
1716      * @see #getFirstByXPath(String)
1717      * @see #getCanonicalXPath()
1718      */
1719     public <T> List<T> getByXPath(final String xpathExpr) {
1720         return XPathHelper.getByXPath(this, xpathExpr, null);
1721     }
1722 
1723     /**
1724      * Evaluates the specified XPath expression from this node, returning the matching elements.
1725      *
1726      * @param xpathExpr the XPath expression to evaluate
1727      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1728      * @return the elements which match the specified XPath expression
1729      * @see #getFirstByXPath(String)
1730      * @see #getCanonicalXPath()
1731      */
1732     public List<?> getByXPath(final String xpathExpr, final PrefixResolver resolver) {
1733         return XPathHelper.getByXPath(this, xpathExpr, resolver);
1734     }
1735 
1736     /**
1737      * Evaluates the specified XPath expression from this node, returning the first matching element,
1738      * or {@code null} if no node matches the specified XPath expression.
1739      *
1740      * @param xpathExpr the XPath expression
1741      * @param <X> the expression type
1742      * @return the first element matching the specified XPath expression
1743      * @see #getByXPath(String)
1744      * @see #getCanonicalXPath()
1745      */
1746     public <X> X getFirstByXPath(final String xpathExpr) {
1747         return getFirstByXPath(xpathExpr, null);
1748     }
1749 
1750     /**
1751      * Evaluates the specified XPath expression from this node, returning the first matching element,
1752      * or {@code null} if no node matches the specified XPath expression.
1753      *
1754      * @param xpathExpr the XPath expression
1755      * @param <X> the expression type
1756      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1757      * @return the first element matching the specified XPath expression
1758      * @see #getByXPath(String)
1759      * @see #getCanonicalXPath()
1760      */
1761     @SuppressWarnings("unchecked")
1762     public <X> X getFirstByXPath(final String xpathExpr, final PrefixResolver resolver) {
1763         final List<?> results = getByXPath(xpathExpr, resolver);
1764         if (results.isEmpty()) {
1765             return null;
1766         }
1767         return (X) results.get(0);
1768     }
1769 
1770     /**
1771      * <p>Returns the canonical XPath expression which identifies this node, for instance
1772      * <code>"/html/body/table[3]/tbody/tr[5]/td[2]/span/a[3]"</code>.</p>
1773      *
1774      * <p><span style="color:red">WARNING:</span> This sort of automated XPath expression
1775      * is often quite bad at identifying a node, as it is highly sensitive to changes in
1776      * the DOM tree.</p>
1777      *
1778      * @return the canonical XPath expression which identifies this node
1779      * @see #getByXPath(String)
1780      */
1781     public String getCanonicalXPath() {
1782         throw new RuntimeException("Method getCanonicalXPath() not implemented for nodes of type " + getNodeType());
1783     }
1784 
1785     /**
1786      * Notifies the registered {@link IncorrectnessListener} of something that is not fully correct.
1787      * @param message the notification to send to the registered {@link IncorrectnessListener}
1788      */
1789     protected void notifyIncorrectness(final String message) {
1790         final WebClient client = getPage().getEnclosingWindow().getWebClient();
1791         final IncorrectnessListener incorrectnessListener = client.getIncorrectnessListener();
1792         incorrectnessListener.notify(message, this);
1793     }
1794 
1795     /**
1796      * Adds a {@link DomChangeListener} to the listener list. The listener is registered for
1797      * all descendants of this node.
1798      *
1799      * @param listener the DOM structure change listener to be added
1800      * @see #removeDomChangeListener(DomChangeListener)
1801      */
1802     public void addDomChangeListener(final DomChangeListener listener) {
1803         WebAssert.notNull("listener", listener);
1804 
1805         synchronized (this) {
1806             if (domListeners_ == null) {
1807                 domListeners_ = new ArrayList<>();
1808             }
1809             domListeners_.add(listener);
1810 
1811             final SgmlPage page = getPage();
1812             if (page != null) {
1813                 page.domChangeListenerAdded();
1814             }
1815         }
1816     }
1817 
1818     /**
1819      * Removes a {@link DomChangeListener} from the listener list. The listener is deregistered for
1820      * all descendants of this node.
1821      *
1822      * @param listener the DOM structure change listener to be removed
1823      * @see #addDomChangeListener(DomChangeListener)
1824      */
1825     public void removeDomChangeListener(final DomChangeListener listener) {
1826         WebAssert.notNull("listener", listener);
1827 
1828         synchronized (this) {
1829             if (domListeners_ != null) {
1830                 domListeners_.remove(listener);
1831             }
1832         }
1833     }
1834 
1835     /**
1836      * Support for reporting DOM changes. This method can be called when a node has been added, and it
1837      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
1838      *
1839      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeAdded(DomNode, DomNode)}.</p>
1840      *
1841      * @param parentNode the parent of the node that was changed
1842      * @param addedNode the node that has been added
1843      */
1844     protected void fireNodeAdded(final DomNode parentNode, final DomNode addedNode) {
1845         DomChangeEvent event = null;
1846 
1847         DomNode toInform = this;
1848         while (toInform != null) {
1849             if (toInform.domListeners_ != null) {
1850                 final List<DomChangeListener> listeners;
1851                 synchronized (toInform) {
1852                     listeners = new ArrayList<>(toInform.domListeners_);
1853                 }
1854 
1855                 if (event == null) {
1856                     event = new DomChangeEvent(parentNode, addedNode);
1857                 }
1858                 for (final DomChangeListener domChangeListener : listeners) {
1859                     domChangeListener.nodeAdded(event);
1860                 }
1861             }
1862 
1863             toInform = toInform.getParentNode();
1864         }
1865     }
1866 
1867     /**
1868      * Adds a {@link CharacterDataChangeListener} to the listener list. The listener is registered for
1869      * all descendants of this node.
1870      *
1871      * @param listener the character data change listener to be added
1872      * @see #removeCharacterDataChangeListener(CharacterDataChangeListener)
1873      */
1874     public void addCharacterDataChangeListener(final CharacterDataChangeListener listener) {
1875         WebAssert.notNull("listener", listener);
1876 
1877         synchronized (this) {
1878             if (characterDataListeners_ == null) {
1879                 characterDataListeners_ = new ArrayList<>();
1880             }
1881             characterDataListeners_.add(listener);
1882 
1883             final SgmlPage page = getPage();
1884             if (page != null) {
1885                 page.characterDataChangeListenerAdded();
1886             }
1887         }
1888     }
1889 
1890     /**
1891      * Removes a {@link CharacterDataChangeListener} from the listener list. The listener is deregistered for
1892      * all descendants of this node.
1893      *
1894      * @param listener the Character Data change listener to be removed
1895      * @see #addCharacterDataChangeListener(CharacterDataChangeListener)
1896      */
1897     public void removeCharacterDataChangeListener(final CharacterDataChangeListener listener) {
1898         WebAssert.notNull("listener", listener);
1899 
1900         synchronized (this) {
1901             if (characterDataListeners_ != null) {
1902                 characterDataListeners_.remove(listener);
1903             }
1904         }
1905     }
1906 
1907     /**
1908      * Support for reporting Character Data changes.
1909      *
1910      * <p>Note that this method recursively calls this node's parent's {@link #fireCharacterDataChanged}.</p>
1911      *
1912      * @param characterData the character data which is changed
1913      * @param oldValue the old value
1914      */
1915     protected void fireCharacterDataChanged(final DomCharacterData characterData, final String oldValue) {
1916         CharacterDataChangeEvent event = null;
1917 
1918         DomNode toInform = this;
1919         while (toInform != null) {
1920             if (toInform.characterDataListeners_ != null) {
1921                 final List<CharacterDataChangeListener> listeners;
1922                 synchronized (toInform) {
1923                     listeners = new ArrayList<>(toInform.characterDataListeners_);
1924                 }
1925 
1926                 if (event == null) {
1927                     event = new CharacterDataChangeEvent(characterData, oldValue);
1928                 }
1929                 for (final CharacterDataChangeListener domChangeListener : listeners) {
1930                     domChangeListener.characterDataChanged(event);
1931                 }
1932             }
1933 
1934             toInform = toInform.getParentNode();
1935         }
1936     }
1937 
1938     /**
1939      * Support for reporting DOM changes. This method can be called when a node has been deleted, and it
1940      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
1941      *
1942      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeDeleted(DomNode, DomNode)}.</p>
1943      *
1944      * @param parentNode the parent of the node that was changed
1945      * @param deletedNode the node that has been deleted
1946      */
1947     protected void fireNodeDeleted(final DomNode parentNode, final DomNode deletedNode) {
1948         DomChangeEvent event = null;
1949 
1950         DomNode toInform = this;
1951         while (toInform != null) {
1952             if (toInform.domListeners_ != null) {
1953                 final List<DomChangeListener> listeners;
1954                 synchronized (toInform) {
1955                     listeners = new ArrayList<>(toInform.domListeners_);
1956                 }
1957 
1958                 if (event == null) {
1959                     event = new DomChangeEvent(parentNode, deletedNode);
1960                 }
1961                 for (final DomChangeListener domChangeListener : listeners) {
1962                     domChangeListener.nodeDeleted(event);
1963                 }
1964             }
1965 
1966             toInform = toInform.getParentNode();
1967         }
1968     }
1969 
1970     /**
1971      * Retrieves all element nodes from descendants of the starting element node that match any selector
1972      * within the supplied selector strings.
1973      * @param selectors one or more CSS selectors separated by commas
1974      * @param scope the context scope node used for evaluating relative selectors like :scope
1975      * @return list of all found nodes
1976      */
1977     public DomNodeList<DomNode> querySelectorAll(final String selectors, final DomNode scope) {
1978         try {
1979             final WebClient webClient = getPage().getWebClient();
1980             final SelectorList selectorList = getSelectorList(selectors, webClient);
1981 
1982             final List<DomNode> elements = new ArrayList<>();
1983             if (selectorList != null) {
1984                 for (final DomElement child : getDomElementDescendants()) {
1985                     for (final Selector selector : selectorList) {
1986                         if (CssStyleSheet.selects(webClient.getBrowserVersion(), child,
1987                                 selector, null, scope, true, true)) {
1988                             elements.add(child);
1989                             break;
1990                         }
1991                     }
1992                 }
1993             }
1994             return new StaticDomNodeList(elements);
1995         }
1996         catch (final IOException e) {
1997             throw new CSSException("Error parsing CSS selectors from '" + selectors + "': " + e.getMessage(), e);
1998         }
1999     }
2000 
2001     /**
2002      * Returns the {@link SelectorList}.
2003      * @param selectors the selectors
2004      * @param webClient the {@link WebClient}
2005      * @return the {@link SelectorList}
2006      * @throws IOException if an error occurs
2007      */
2008     protected SelectorList getSelectorList(final String selectors, final WebClient webClient)
2009             throws IOException {
2010 
2011         // get us a CSS3Parser from the pool so the chance of reusing it are high
2012         try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
2013             final CSSOMParser parser = new CSSOMParser(pooledParser);
2014             final CheckErrorHandler errorHandler = new CheckErrorHandler();
2015             parser.setErrorHandler(errorHandler);
2016 
2017             final SelectorList selectorList = parser.parseSelectors(selectors);
2018             // in case of error parseSelectors returns null
2019             if (errorHandler.error() != null) {
2020                 throw new CSSException("Invalid selectors: '" + selectors + "'", errorHandler.error());
2021             }
2022 
2023             if (selectorList != null) {
2024                 CssStyleSheet.validateSelectors(selectorList, this);
2025 
2026             }
2027             return selectorList;
2028         }
2029     }
2030 
2031     /**
2032      * Returns the first element within the document that matches the specified group of selectors.
2033      * @param selectors one or more CSS selectors separated by commas
2034      * @param scope the context scope node used for evaluating relative selectors like :scope
2035      * @param <N> the node type
2036      * @return null if no matches are found; otherwise, it returns the first matching element
2037      */
2038     @SuppressWarnings("unchecked")
2039     public <N extends DomNode> N querySelector(final String selectors, final DomNode scope) {
2040         final DomNodeList<DomNode> list = querySelectorAll(selectors, scope);
2041         if (!list.isEmpty()) {
2042             return (N) list.get(0);
2043         }
2044         return null;
2045     }
2046 
2047     /**
2048      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2049      *
2050      * Indicates if this node is currently attached to the page.
2051      * @return {@code true} if the page is one ancestor of the node.
2052      */
2053     public boolean isAttachedToPage() {
2054         return attachedToPage_;
2055     }
2056 
2057     /**
2058      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2059      *
2060      * Lifecycle method to support special processing for js method importNode.
2061      * @param doc the import target document
2062      * @see org.htmlunit.javascript.host.dom.Document#importNode(
2063      * org.htmlunit.javascript.host.dom.Node, boolean)
2064      * @see HtmlScript#processImportNode(org.htmlunit.javascript.host.dom.Document)
2065      */
2066     public void processImportNode(final org.htmlunit.javascript.host.dom.Document doc) {
2067         page_ = (SgmlPage) doc.getDomNodeOrDie();
2068     }
2069 
2070     /**
2071      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2072      *
2073      * Helper for a common call sequence.
2074      * @param feature the feature to check
2075      * @return {@code true} if the currently emulated browser has this feature.
2076      */
2077     public boolean hasFeature(final BrowserVersionFeatures feature) {
2078         return getPage().getWebClient().getBrowserVersion().hasFeature(feature);
2079     }
2080 
2081     private static final class CheckErrorHandler implements CSSErrorHandler {
2082         private CSSParseException error_;
2083 
2084         CSSParseException error() {
2085             return error_;
2086         }
2087 
2088         @Override
2089         public void warning(final CSSParseException exception) throws CSSException {
2090             // ignore
2091         }
2092 
2093         @Override
2094         public void fatalError(final CSSParseException exception) throws CSSException {
2095             error_ = exception;
2096         }
2097 
2098         @Override
2099         public void error(final CSSParseException exception) throws CSSException {
2100             error_ = exception;
2101         }
2102     }
2103 
2104     /**
2105      * Indicates if the provided event can be applied to this node.
2106      * Overwrite this.
2107      * @param event the event
2108      * @return {@code false} if the event can't be applied
2109      */
2110     public boolean handles(final Event event) {
2111         return true;
2112     }
2113 
2114     /**
2115      * Returns the previous sibling element node of this element.
2116      * null if this element has no element sibling nodes that come before this one in the document tree.
2117      * @return the previous sibling element node of this element.
2118      *         null if this element has no element sibling nodes that come before this one in the document tree
2119      */
2120     public DomElement getPreviousElementSibling() {
2121         DomNode node = getPreviousSibling();
2122         for ( ; node != null; node = node.getPreviousSibling()) {
2123             if (node instanceof DomElement elem) {
2124                 return elem;
2125             }
2126         }
2127         return null;
2128     }
2129 
2130     /**
2131      * Returns the next sibling element node of this element.
2132      * null if this element has no element sibling nodes that come after this one in the document tree.
2133      * @return the next sibling element node of this element.
2134      *         null if this element has no element sibling nodes that come after this one in the document tree
2135      */
2136     public DomElement getNextElementSibling() {
2137         DomNode node = getNextSibling();
2138         for ( ; node != null; node = node.getNextSibling()) {
2139             if (node instanceof DomElement elem) {
2140                 return elem;
2141             }
2142         }
2143         return null;
2144     }
2145 
2146     /**
2147      * Returns the closest ancestor element, or this element, that matches the
2148      * specified CSS selector.
2149      *
2150      * @param selectorString the CSS selector to test
2151      * @return the closest matching {@link DomElement}, or {@code null} if no
2152      *         matching element is found
2153      */
2154     public DomElement closest(final String selectorString) {
2155         try {
2156             final WebClient webClient = getPage().getWebClient();
2157             final SelectorList selectorList = getSelectorList(selectorString, webClient);
2158 
2159             if (selectorList != null) {
2160                 // closest() only ever matches elements; if this node isn't one,
2161                 // start the search from the nearest ancestor element instead.
2162                 DomNode current = this;
2163                 for ( ; current != null; current = current.getParentNode()) {
2164                     if (current instanceof DomElement elem) {
2165                         for (final Selector selector : selectorList) {
2166                             if (CssStyleSheet.selects(
2167                                     webClient.getBrowserVersion(), elem, selector, null, this, true, true)) {
2168                                 return elem;
2169                             }
2170                         }
2171                     }
2172                 }
2173             }
2174             return null;
2175         }
2176         catch (final IOException e) {
2177             throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage(), e);
2178         }
2179     }
2180 
2181     /**
2182      * An unmodifiable empty {@link NamedNodeMap} implementation.
2183      */
2184     private static final class ReadOnlyEmptyNamedNodeMapImpl implements NamedNodeMap, Serializable {
2185 
2186         /**
2187          * {@inheritDoc}
2188          */
2189         @Override
2190         public int getLength() {
2191             return 0;
2192         }
2193 
2194         /**
2195          * {@inheritDoc}
2196          */
2197         @Override
2198         public DomAttr getNamedItem(final String name) {
2199             return null;
2200         }
2201 
2202         /**
2203          * {@inheritDoc}
2204          */
2205         @Override
2206         public Node getNamedItemNS(final String namespaceURI, final String localName) {
2207             return null;
2208         }
2209 
2210         /**
2211          * {@inheritDoc}
2212          */
2213         @Override
2214         public Node item(final int index) {
2215             return null;
2216         }
2217 
2218         /**
2219          * {@inheritDoc}
2220          */
2221         @Override
2222         public Node removeNamedItem(final String name) throws DOMException {
2223             return null;
2224         }
2225 
2226         /**
2227          * {@inheritDoc}
2228          */
2229         @Override
2230         public Node removeNamedItemNS(final String namespaceURI, final String localName) {
2231             return null;
2232         }
2233 
2234         /**
2235          * {@inheritDoc}
2236          */
2237         @Override
2238         public DomAttr setNamedItem(final Node node) {
2239             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItem");
2240         }
2241 
2242         /**
2243          * {@inheritDoc}
2244          */
2245         @Override
2246         public Node setNamedItemNS(final Node node) throws DOMException {
2247             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItemNS");
2248         }
2249     }
2250 }