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