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