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.EVENT_ONCLICK_USES_POINTEREVENT;
18  
19  import java.io.IOException;
20  import java.io.PrintWriter;
21  import java.io.Serializable;
22  import java.io.StringWriter;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.LinkedHashMap;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.NoSuchElementException;
33  import java.util.Set;
34  import java.util.SortedSet;
35  import java.util.TreeSet;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.w3c.css.sac.CSSException;
40  import org.w3c.css.sac.Selector;
41  import org.w3c.css.sac.SelectorList;
42  import org.w3c.dom.Attr;
43  import org.w3c.dom.DOMException;
44  import org.w3c.dom.Element;
45  import org.w3c.dom.NamedNodeMap;
46  import org.w3c.dom.Node;
47  import org.w3c.dom.TypeInfo;
48  
49  import com.gargoylesoftware.htmlunit.BrowserVersion;
50  import com.gargoylesoftware.htmlunit.Page;
51  import com.gargoylesoftware.htmlunit.ScriptResult;
52  import com.gargoylesoftware.htmlunit.SgmlPage;
53  import com.gargoylesoftware.htmlunit.WebClient;
54  import com.gargoylesoftware.htmlunit.css.SelectorSpecificity;
55  import com.gargoylesoftware.htmlunit.css.StyleElement;
56  import com.gargoylesoftware.htmlunit.javascript.AbstractJavaScriptEngine;
57  import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
58  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
59  import com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet;
60  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
61  import com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget;
62  import com.gargoylesoftware.htmlunit.javascript.host.event.MouseEvent;
63  import com.gargoylesoftware.htmlunit.javascript.host.event.PointerEvent;
64  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
65  import com.gargoylesoftware.htmlunit.util.StringUtils;
66  
67  import net.sourceforge.htmlunit.corejs.javascript.Context;
68  import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
69  import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
70  
71  /**
72   * @author Ahmed Ashour
73   * @author Marc Guillemot
74   * @author <a href="mailto:tom.anderson@univ.oxon.org">Tom Anderson</a>
75   * @author Ronald Brill
76   */
77  public class DomElement extends DomNamespaceNode implements Element {
78  
79      private static final Log LOG = LogFactory.getLog(DomElement.class);
80  
81      /** Constant meaning that the specified attribute was not defined. */
82      public static final String ATTRIBUTE_NOT_DEFINED = new String("");
83  
84      /** Constant meaning that the specified attribute was found but its value was empty. */
85      public static final String ATTRIBUTE_VALUE_EMPTY = new String();
86  
87      /** The map holding the attributes, keyed by name. */
88      private NamedAttrNodeMapImpl attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive());
89  
90      /** The map holding the namespaces, keyed by URI. */
91      private Map<String, String> namespaces_ = new HashMap<>();
92  
93      /** Cache for the styles. */
94      private String styleString_ = new String();
95      private Map<String, StyleElement> styleMap_;
96  
97      /**
98       * Whether the Mouse is currently over this element or not.
99       */
100     private boolean mouseOver_;
101 
102     /**
103      * Creates an instance of a DOM element that can have a namespace.
104      *
105      * @param namespaceURI the URI that identifies an XML namespace
106      * @param qualifiedName the qualified name of the element type to instantiate
107      * @param page the page that contains this element
108      * @param attributes a map ready initialized with the attributes for this element, or
109      * {@code null}. The map will be stored as is, not copied.
110      */
111     public DomElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
112             final Map<String, DomAttr> attributes) {
113         super(namespaceURI, qualifiedName, page);
114         if (attributes != null && !attributes.isEmpty()) {
115             attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive(), attributes);
116             for (final DomAttr entry : attributes_.values()) {
117                 entry.setParentNode(this);
118                 final String attrNamespaceURI = entry.getNamespaceURI();
119                 if (attrNamespaceURI != null) {
120                     namespaces_.put(attrNamespaceURI, entry.getPrefix());
121                 }
122             }
123         }
124     }
125 
126     /**
127      * {@inheritDoc}
128      */
129     @Override
130     public String getNodeName() {
131         return getQualifiedName();
132     }
133 
134     /**
135      * {@inheritDoc}
136      */
137     @Override
138     public final short getNodeType() {
139         return ELEMENT_NODE;
140     }
141 
142     /**
143      * Returns the tag name of this element.
144      * @return the tag name of this element
145      */
146     @Override
147     public final String getTagName() {
148         return getNodeName();
149     }
150 
151     /**
152      * {@inheritDoc}
153      */
154     @Override
155     public final boolean hasAttributes() {
156         return !attributes_.isEmpty();
157     }
158 
159     /**
160      * Returns whether the attribute specified by name has a value.
161      *
162      * @param attributeName the name of the attribute
163      * @return true if an attribute with the given name is specified on this element or has a
164      * default value, false otherwise.
165      */
166     @Override
167     public boolean hasAttribute(final String attributeName) {
168         return attributes_.containsKey(attributeName);
169     }
170 
171     /**
172      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
173      *
174      * Replaces the value of the named style attribute. If there is no style attribute with the
175      * specified name, a new one is added. If the specified value is an empty (or all whitespace)
176      * string, this method actually removes the named style attribute.
177      * @param name the attribute name (delimiter-separated, not camel-cased)
178      * @param value the attribute value
179      * @param priority the new priority of the property; <code>"important"</code>or the empty string if none.
180      */
181     public void replaceStyleAttribute(final String name, final String value, final String priority) {
182         if (org.apache.commons.lang3.StringUtils.isBlank(value)) {
183             removeStyleAttribute(name);
184             return;
185         }
186 
187         final Map<String, StyleElement> styleMap = getStyleMap();
188         final StyleElement old = styleMap.get(name);
189         final StyleElement element;
190         if (old == null) {
191             element = new StyleElement(name, value, priority, SelectorSpecificity.FROM_STYLE_ATTRIBUTE);
192         }
193         else {
194             element = new StyleElement(name, value, priority,
195                     SelectorSpecificity.FROM_STYLE_ATTRIBUTE, old.getIndex());
196         }
197         styleMap.put(name, element);
198         writeStyleToElement(styleMap);
199     }
200 
201     /**
202      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
203      *
204      * Removes the specified style attribute, returning the value of the removed attribute.
205      * @param name the attribute name (delimiter-separated, not camel-cased)
206      * @return the removed value
207      */
208     public String removeStyleAttribute(final String name) {
209         final Map<String, StyleElement> styleMap = getStyleMap();
210         final StyleElement value = styleMap.get(name);
211         if (value == null) {
212             return "";
213         }
214         styleMap.remove(name);
215         writeStyleToElement(styleMap);
216         return value.getValue();
217     }
218 
219     /**
220      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
221      *
222      * Determines the StyleElement for the given name.
223      *
224      * @param name the name of the requested StyleElement
225      * @return the StyleElement or null if not found
226      */
227     public StyleElement getStyleElement(final String name) {
228         final Map<String, StyleElement> map = getStyleMap();
229         if (map != null) {
230             return map.get(name);
231         }
232         return null;
233     }
234 
235     /**
236      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
237      *
238      * Determines the StyleElement for the given name.
239      * This ignores the case of the name.
240      *
241      * @param name the name of the requested StyleElement
242      * @return the StyleElement or null if not found
243      */
244     public StyleElement getStyleElementCaseInSensitive(final String name) {
245         final Map<String, StyleElement> map = getStyleMap();
246         for (final Map.Entry<String, StyleElement> entry : map.entrySet()) {
247             if (entry.getKey().equalsIgnoreCase(name)) {
248                 return entry.getValue();
249             }
250         }
251         return null;
252     }
253 
254     /**
255      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
256      *
257      * Returns a sorted map containing style elements, keyed on style element name. We use a
258      * {@link LinkedHashMap} map so that results are deterministic and are thus testable.
259      *
260      * @return a sorted map containing style elements, keyed on style element name
261      */
262     public Map<String, StyleElement> getStyleMap() {
263         final String styleAttribute = getAttribute("style");
264         if (styleString_ == styleAttribute) {
265             return styleMap_;
266         }
267 
268         final Map<String, StyleElement> styleMap = new LinkedHashMap<>();
269         if (DomElement.ATTRIBUTE_NOT_DEFINED == styleAttribute || DomElement.ATTRIBUTE_VALUE_EMPTY == styleAttribute) {
270             styleMap_ = styleMap;
271             styleString_ = styleAttribute;
272             return styleMap_;
273         }
274 
275         // TODO this should be done by using cssparser also
276         for (final String token : org.apache.commons.lang3.StringUtils.split(styleAttribute, ';')) {
277             final int index = token.indexOf(":");
278             if (index != -1) {
279                 final String key = token.substring(0, index).trim().toLowerCase(Locale.ROOT);
280                 String value = token.substring(index + 1).trim();
281                 String priority = "";
282                 if (org.apache.commons.lang3.StringUtils.endsWithIgnoreCase(value, "!important")) {
283                     priority = StyleElement.PRIORITY_IMPORTANT;
284                     value = value.substring(0, value.length() - 10);
285                     value = value.trim();
286                 }
287                 final StyleElement element = new StyleElement(key, value, priority,
288                                                     SelectorSpecificity.FROM_STYLE_ATTRIBUTE);
289                 styleMap.put(key, element);
290             }
291         }
292 
293         styleMap_ = styleMap;
294         styleString_ = styleAttribute;
295         return styleMap_;
296     }
297 
298     /**
299      * Prints the content between "&lt;" and "&gt;" (or "/&gt;") in the output of the tag name
300      * and its attributes in XML format.
301      * @param printWriter the writer to print in
302      */
303     protected void printOpeningTagContentAsXml(final PrintWriter printWriter) {
304         printWriter.print(getTagName());
305         for (final Map.Entry<String, DomAttr> entry : attributes_.entrySet()) {
306             printWriter.print(" ");
307             printWriter.print(entry.getKey());
308             printWriter.print("=\"");
309             printWriter.print(StringUtils.escapeXmlAttributeValue(entry.getValue().getNodeValue()));
310             printWriter.print("\"");
311         }
312     }
313 
314     /**
315      * Recursively write the XML data for the node tree starting at <code>node</code>.
316      *
317      * @param indent white space to indent child nodes
318      * @param printWriter writer where child nodes are written
319      */
320     @Override
321     protected void printXml(final String indent, final PrintWriter printWriter) {
322         final boolean hasChildren = getFirstChild() != null;
323         printWriter.print(indent + "<");
324         printOpeningTagContentAsXml(printWriter);
325 
326         if (hasChildren || isEmptyXmlTagExpanded()) {
327             printWriter.print(">\r\n");
328             printChildrenAsXml(indent, printWriter);
329             printWriter.print(indent);
330             printWriter.print("</");
331             printWriter.print(getTagName());
332             printWriter.print(">\r\n");
333         }
334         else {
335             printWriter.print("/>\r\n");
336         }
337     }
338 
339     /**
340      * Indicates if a node without children should be written in expanded form as XML
341      * (i.e. with closing tag rather than with "/&gt;")
342      * @return {@code false} by default
343      */
344     protected boolean isEmptyXmlTagExpanded() {
345         return false;
346     }
347 
348     /**
349      * Returns the qualified name (prefix:local) for the specified namespace and local name,
350      * or {@code null} if the specified namespace URI does not exist.
351      *
352      * @param namespaceURI the URI that identifies an XML namespace
353      * @param localName the name within the namespace
354      * @return the qualified name for the specified namespace and local name
355      */
356     String getQualifiedName(final String namespaceURI, final String localName) {
357         final String qualifiedName;
358         if (namespaceURI == null) {
359             qualifiedName = localName;
360         }
361         else {
362             final String prefix = namespaces_.get(namespaceURI);
363             if (prefix == null) {
364                 qualifiedName = null;
365             }
366             else {
367                 qualifiedName = prefix + ':' + localName;
368             }
369         }
370         return qualifiedName;
371     }
372 
373     /**
374      * Returns the value of the attribute specified by name or an empty string. If the
375      * result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED}
376      * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the
377      * attribute was specified but it was empty.
378      *
379      * @param attributeName the name of the attribute
380      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
381      */
382     @Override
383     public String getAttribute(final String attributeName) {
384         final DomAttr attr = attributes_.get(attributeName);
385         if (attr != null) {
386             return attr.getNodeValue();
387         }
388         return ATTRIBUTE_NOT_DEFINED;
389     }
390 
391     /**
392      * Removes an attribute specified by name from this element.
393      * @param attributeName the attribute attributeName
394      */
395     @Override
396     public void removeAttribute(final String attributeName) {
397         attributes_.remove(attributeName);
398     }
399 
400     /**
401      * Removes an attribute specified by namespace and local name from this element.
402      * @param namespaceURI the URI that identifies an XML namespace
403      * @param localName the name within the namespace
404      */
405     @Override
406     public final void removeAttributeNS(final String namespaceURI, final String localName) {
407         final String qualifiedName = getQualifiedName(namespaceURI, localName);
408         if (qualifiedName != null) {
409             removeAttribute(qualifiedName);
410         }
411     }
412 
413     /**
414      * {@inheritDoc}
415      * Not yet implemented.
416      */
417     @Override
418     public final Attr removeAttributeNode(final Attr attribute) {
419         throw new UnsupportedOperationException("DomElement.removeAttributeNode is not yet implemented.");
420     }
421 
422     /**
423      * Returns whether the attribute specified by namespace and local name has a value.
424      *
425      * @param namespaceURI the URI that identifies an XML namespace
426      * @param localName the name within the namespace
427      * @return true if an attribute with the given name is specified on this element or has a
428      * default value, false otherwise.
429      */
430     @Override
431     public final boolean hasAttributeNS(final String namespaceURI, final String localName) {
432         final String qualifiedName = getQualifiedName(namespaceURI, localName);
433         if (qualifiedName != null) {
434             return attributes_.get(qualifiedName) != null;
435         }
436         return false;
437     }
438 
439     /**
440      * Returns the map holding the attributes, keyed by name.
441      * @return the attributes map
442      */
443     public final Map<String, DomAttr> getAttributesMap() {
444         return attributes_;
445     }
446 
447     /**
448      * {@inheritDoc}
449      */
450     @Override
451     public NamedNodeMap getAttributes() {
452         return attributes_;
453     }
454 
455     /**
456      * Sets the value of the attribute specified by name.
457      *
458      * @param attributeName the name of the attribute
459      * @param attributeValue the value of the attribute
460      */
461     @Override
462     public void setAttribute(final String attributeName, final String attributeValue) {
463         setAttributeNS(null, attributeName, attributeValue);
464     }
465 
466     /**
467      * Sets the value of the attribute specified by namespace and qualified name.
468      *
469      * @param namespaceURI the URI that identifies an XML namespace
470      * @param qualifiedName the qualified name (prefix:local) of the attribute
471      * @param attributeValue the value of the attribute
472      */
473     @Override
474     public void setAttributeNS(final String namespaceURI, final String qualifiedName,
475             final String attributeValue) {
476         setAttributeNS(namespaceURI, qualifiedName, attributeValue, true, true);
477     }
478 
479     /**
480      * Sets the value of the attribute specified by namespace and qualified name.
481      *
482      * @param namespaceURI the URI that identifies an XML namespace
483      * @param qualifiedName the qualified name (prefix:local) of the attribute
484      * @param attributeValue the value of the attribute
485      * @param notifyAttributeChangeListeners to notify the associated {@link HtmlAttributeChangeListener}s
486      * @param notifyMutationObservers to notify {@code MutationObserver}s or not
487      */
488     protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
489             final String attributeValue, final boolean notifyAttributeChangeListeners,
490             final boolean notifyMutationObservers) {
491         final String value = attributeValue;
492         final DomAttr newAttr = new DomAttr(getPage(), namespaceURI, qualifiedName, value, true);
493         newAttr.setParentNode(this);
494         attributes_.put(qualifiedName, newAttr);
495 
496         if (namespaceURI != null) {
497             namespaces_.put(namespaceURI, newAttr.getPrefix());
498         }
499     }
500 
501     /**
502      * Indicates if the attribute names are case sensitive.
503      * @return {@code true}
504      */
505     protected boolean isAttributeCaseSensitive() {
506         return true;
507     }
508 
509     /**
510      * Returns the value of the attribute specified by namespace and local name or an empty
511      * string. If the result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED}
512      * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the
513      * attribute was specified but it was empty.
514      *
515      * @param namespaceURI the URI that identifies an XML namespace
516      * @param localName the name within the namespace
517      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
518      */
519     @Override
520     public final String getAttributeNS(final String namespaceURI, final String localName) {
521         final String qualifiedName = getQualifiedName(namespaceURI, localName);
522         if (qualifiedName != null) {
523             return getAttribute(qualifiedName);
524         }
525         return ATTRIBUTE_NOT_DEFINED;
526     }
527 
528     /**
529      * {@inheritDoc}
530      */
531     @Override
532     public DomAttr getAttributeNode(final String name) {
533         return attributes_.get(name);
534     }
535 
536     /**
537      * {@inheritDoc}
538      */
539     @Override
540     public DomAttr getAttributeNodeNS(final String namespaceURI, final String localName) {
541         final String qualifiedName = getQualifiedName(namespaceURI, localName);
542         if (qualifiedName != null) {
543             return attributes_.get(qualifiedName);
544         }
545         return null;
546     }
547 
548     /**
549      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
550      *
551      * @param styleMap the styles
552      */
553     public void writeStyleToElement(final Map<String, StyleElement> styleMap) {
554         final StringBuilder builder = new StringBuilder();
555         final SortedSet<StyleElement> sortedValues = new TreeSet<>(styleMap.values());
556         for (final StyleElement e : sortedValues) {
557             if (builder.length() != 0) {
558                 builder.append(" ");
559             }
560             builder.append(e.getName());
561             builder.append(": ");
562             builder.append(e.getValue());
563 
564             final String prio = e.getPriority();
565             if (org.apache.commons.lang3.StringUtils.isNotBlank(prio)) {
566                 builder.append(" !");
567                 builder.append(prio);
568             }
569             builder.append(";");
570         }
571         final String value = builder.toString();
572         setAttribute("style", value);
573     }
574 
575     /**
576      * {@inheritDoc}
577      */
578     @Override
579     public DomNodeList<HtmlElement> getElementsByTagName(final String tagName) {
580         return getElementsByTagNameImpl(tagName);
581     }
582 
583     /**
584      * This should be {@link #getElementsByTagName(String)}, but is separate because of the type erasure in Java.
585      * @param name The name of the tag to match on
586      * @return A list of matching elements.
587      */
588     <E extends HtmlElement> DomNodeList<E> getElementsByTagNameImpl(final String tagName) {
589         return new AbstractDomNodeList<E>(this) {
590             @Override
591             @SuppressWarnings("unchecked")
592             protected List<E> provideElements() {
593                 final List<E> res = new LinkedList<>();
594                 for (final HtmlElement elem : getDomNode().getHtmlElementDescendants()) {
595                     if (elem.getLocalName().equalsIgnoreCase(tagName)) {
596                         res.add((E) elem);
597                     }
598                 }
599                 return res;
600             }
601         };
602     }
603 
604     /**
605      * {@inheritDoc}
606      * Not yet implemented.
607      */
608     @Override
609     public DomNodeList<HtmlElement> getElementsByTagNameNS(final String namespace, final String localName) {
610         throw new UnsupportedOperationException("DomElement.getElementsByTagNameNS is not yet implemented.");
611     }
612 
613     /**
614      * {@inheritDoc}
615      * Not yet implemented.
616      */
617     @Override
618     public TypeInfo getSchemaTypeInfo() {
619         throw new UnsupportedOperationException("DomElement.getSchemaTypeInfo is not yet implemented.");
620     }
621 
622     /**
623      * {@inheritDoc}
624      * Not yet implemented.
625      */
626     @Override
627     public void setIdAttribute(final String name, final boolean isId) {
628         throw new UnsupportedOperationException("DomElement.setIdAttribute is not yet implemented.");
629     }
630 
631     /**
632      * {@inheritDoc}
633      * Not yet implemented.
634      */
635     @Override
636     public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) {
637         throw new UnsupportedOperationException("DomElement.setIdAttributeNS is not yet implemented.");
638     }
639 
640     /**
641      * {@inheritDoc}
642      */
643     @Override
644     public Attr setAttributeNode(final Attr attribute) {
645         attributes_.setNamedItem(attribute);
646         return null;
647     }
648 
649     /**
650      * {@inheritDoc}
651      * Not yet implemented.
652      */
653     @Override
654     public Attr setAttributeNodeNS(final Attr attribute) {
655         throw new UnsupportedOperationException("DomElement.setAttributeNodeNS is not yet implemented.");
656     }
657 
658     /**
659      * {@inheritDoc}
660      * Not yet implemented.
661      */
662     @Override
663     public final void setIdAttributeNode(final Attr idAttr, final boolean isId) {
664         throw new UnsupportedOperationException("DomElement.setIdAttributeNode is not yet implemented.");
665     }
666 
667     /**
668      * {@inheritDoc}
669      */
670     @Override
671     public DomNode cloneNode(final boolean deep) {
672         final DomElement clone = (DomElement) super.cloneNode(deep);
673         clone.attributes_ = new NamedAttrNodeMapImpl(clone, isAttributeCaseSensitive());
674         clone.attributes_.putAll(attributes_);
675         return clone;
676     }
677 
678     /**
679      * @return the identifier of this element
680      */
681     public final String getId() {
682         return getAttribute("id");
683     }
684 
685     /**
686      * Sets the identifier this element.
687      *
688      * @param newId the new identifier of this element
689      */
690     public final void setId(final String newId) {
691         setAttribute("id", newId);
692     }
693 
694     /**
695      * Returns the first child element node of this element. null if this element has no child elements.
696      * @return the first child element node of this element. null if this element has no child elements
697      */
698     public DomElement getFirstElementChild() {
699         final Iterator<DomElement> i = getChildElements().iterator();
700         if (i.hasNext()) {
701             return i.next();
702         }
703         return null;
704     }
705 
706     /**
707      * Returns the last child element node of this element. null if this element has no child elements.
708      * @return the last child element node of this element. null if this element has no child elements
709      */
710     public DomElement getLastElementChild() {
711         DomElement lastChild = null;
712         final Iterator<DomElement> i = getChildElements().iterator();
713         while (i.hasNext()) {
714             lastChild = i.next();
715         }
716         return lastChild;
717     }
718 
719     /**
720      * Returns the current number of element nodes that are children of this element.
721      * @return the current number of element nodes that are children of this element.
722      */
723     public int getChildElementCount() {
724         int counter = 0;
725         for (final Iterator<DomElement> i = getChildElements().iterator(); i.hasNext(); i.next()) {
726             counter++;
727         }
728         return counter;
729     }
730 
731     /**
732      * @return an Iterable over the DomElement children of this object, i.e. excluding the non-element nodes
733      */
734     public final Iterable<DomElement> getChildElements() {
735         return new ChildElementsIterable(this);
736     }
737 
738     /**
739      * An Iterable over the DomElement children.
740      */
741     private static class ChildElementsIterable implements Iterable<DomElement> {
742         private final Iterator<DomElement> iterator_;
743 
744         /** Constructor.
745          * @param domNode the parent
746          */
747         protected ChildElementsIterable(final DomNode domNode) {
748             iterator_ = new ChildElementsIterator(domNode);
749         }
750 
751         @Override
752         public Iterator<DomElement> iterator() {
753             return iterator_;
754         }
755     }
756 
757     /**
758      * An iterator over the DomElement children.
759      */
760     protected static class ChildElementsIterator implements Iterator<DomElement> {
761 
762         private DomElement nextElement_;
763 
764         /** Constructor.
765          * @param domNode the parent
766          */
767         protected ChildElementsIterator(final DomNode domNode) {
768             final DomNode child = domNode.getFirstChild();
769             if (child != null) {
770                 if (child instanceof DomElement) {
771                     nextElement_ = (DomElement) child;
772                 }
773                 else {
774                     setNextElement(child);
775                 }
776             }
777         }
778 
779         /** @return is there a next one ? */
780         @Override
781         public boolean hasNext() {
782             return nextElement_ != null;
783         }
784 
785         /** @return the next one */
786         @Override
787         public DomElement next() {
788             return nextElement();
789         }
790 
791         /** Removes the current one. */
792         @Override
793         public void remove() {
794             if (nextElement_ == null) {
795                 throw new IllegalStateException();
796             }
797             final DomNode sibling = nextElement_.getPreviousSibling();
798             if (sibling != null) {
799                 sibling.remove();
800             }
801         }
802 
803         /** @return the next element */
804         public DomElement nextElement() {
805             if (nextElement_ != null) {
806                 final DomElement result = nextElement_;
807                 setNextElement(nextElement_);
808                 return result;
809             }
810             throw new NoSuchElementException();
811         }
812 
813         private void setNextElement(final DomNode node) {
814             DomNode next = node.getNextSibling();
815             while (next != null && !(next instanceof DomElement)) {
816                 next = next.getNextSibling();
817             }
818             nextElement_ = (DomElement) next;
819         }
820     }
821 
822     /**
823      * Returns a string representation of this element.
824      * @return a string representation of this element
825      */
826     @Override
827     public String toString() {
828         final StringWriter writer = new StringWriter();
829         final PrintWriter printWriter = new PrintWriter(writer);
830 
831         printWriter.print(getClass().getSimpleName());
832         printWriter.print("[<");
833         printOpeningTagContentAsXml(printWriter);
834         printWriter.print(">]");
835         printWriter.flush();
836         return writer.toString();
837     }
838 
839     /**
840      * Simulates clicking on this element, returning the page in the window that has the focus
841      * after the element has been clicked. Note that the returned page may or may not be the same
842      * as the original page, depending on the type of element being clicked, the presence of JavaScript
843      * action listeners, etc.
844      *
845      * @param <P> the page type
846      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
847      * @exception IOException if an IO error occurs
848      */
849     @SuppressWarnings("unchecked")
850     public <P extends Page> P click() throws IOException {
851         return (P) click(false, false, false);
852     }
853 
854     /**
855      * Simulates clicking on this element, returning the page in the window that has the focus
856      * after the element has been clicked. Note that the returned page may or may not be the same
857      * as the original page, depending on the type of element being clicked, the presence of JavaScript
858      * action listeners, etc.
859      *
860      * @param shiftKey {@code true} if SHIFT is pressed during the click
861      * @param ctrlKey {@code true} if CTRL is pressed during the click
862      * @param altKey {@code true} if ALT is pressed during the click
863      * @param <P> the page type
864      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
865      * @exception IOException if an IO error occurs
866      */
867     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
868         throws IOException {
869 
870         return click(shiftKey, ctrlKey, altKey, true);
871     }
872 
873     /**
874      * Simulates clicking on this element, returning the page in the window that has the focus
875      * after the element has been clicked. Note that the returned page may or may not be the same
876      * as the original page, depending on the type of element being clicked, the presence of JavaScript
877      * action listeners, etc.
878      *
879      * @param shiftKey {@code true} if SHIFT is pressed during the click
880      * @param ctrlKey {@code true} if CTRL is pressed during the click
881      * @param altKey {@code true} if ALT is pressed during the click
882      * @param triggerMouseEvents if true trigger the mouse events also
883      * @param <P> the page type
884      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
885      * @exception IOException if an IO error occurs
886      */
887     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
888             final boolean triggerMouseEvents) throws IOException {
889         return click(shiftKey, ctrlKey, altKey, triggerMouseEvents, false, false);
890     }
891 
892     /**
893      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
894      *
895      * Simulates clicking on this element, returning the page in the window that has the focus
896      * after the element has been clicked. Note that the returned page may or may not be the same
897      * as the original page, depending on the type of element being clicked, the presence of JavaScript
898      * action listeners, etc.
899      *
900      * @param shiftKey {@code true} if SHIFT is pressed during the click
901      * @param ctrlKey {@code true} if CTRL is pressed during the click
902      * @param altKey {@code true} if ALT is pressed during the click
903      * @param triggerMouseEvents if true trigger the mouse events also
904      * @param ignoreVisibility whether to ignore visibility or not
905      * @param disableProcessLabelAfterBubbling ignore label processing
906      * @param <P> the page type
907      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
908      * @exception IOException if an IO error occurs
909      */
910     @SuppressWarnings("unchecked")
911     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
912             final boolean triggerMouseEvents, final boolean ignoreVisibility,
913             final boolean disableProcessLabelAfterBubbling) throws IOException {
914 
915         // make enclosing window the current one
916         final SgmlPage page = getPage();
917         page.getWebClient().setCurrentWindow(page.getEnclosingWindow());
918 
919         if ((!ignoreVisibility && !isDisplayed())
920                 || !(page instanceof HtmlPage)
921                 || this instanceof DisabledElement && ((DisabledElement) this).isDisabled()) {
922             return (P) page;
923         }
924 
925         synchronized (page) {
926             if (triggerMouseEvents) {
927                 mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
928             }
929 
930             // give focus to current element (if possible) or only remove it from previous one
931             DomElement elementToFocus = null;
932             if (this instanceof SubmittableElement || this instanceof HtmlAnchor
933                     || (this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null)) {
934                 elementToFocus = this;
935             }
936             else if (this instanceof HtmlOption) {
937                 elementToFocus = ((HtmlOption) this).getEnclosingSelect();
938             }
939 
940             ((HtmlPage) page).setFocusedElement(elementToFocus);
941 
942             if (triggerMouseEvents) {
943                 mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
944             }
945 
946             final MouseEvent event;
947             if (getPage().getWebClient().getBrowserVersion().hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) {
948                 event = new PointerEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey,
949                         ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
950             }
951             else {
952                 event = new MouseEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey,
953                         ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
954             }
955 
956             if (disableProcessLabelAfterBubbling) {
957                 event.disableProcessLabelAfterBubbling();
958             }
959             return (P) click(event, ignoreVisibility);
960         }
961     }
962 
963     /**
964      * Returns the event target element. This could be overridden by subclasses to have other targets.
965      * The default implementation returns 'this'.
966      * @return the event target element.
967      */
968     protected DomNode getEventTargetElement() {
969         return this;
970     }
971 
972     /**
973      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
974      *
975      * Simulates clicking on this element, returning the page in the window that has the focus
976      * after the element has been clicked. Note that the returned page may or may not be the same
977      * as the original page, depending on the type of element being clicked, the presence of JavaScript
978      * action listeners, etc.
979      *
980      * @param event the click event used
981      * @param ignoreVisibility whether to ignore visibility or not
982      * @param <P> the page type
983      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
984      * @exception IOException if an IO error occurs
985      */
986     @SuppressWarnings("unchecked")
987     public <P extends Page> P click(final Event event, final boolean ignoreVisibility) throws IOException {
988         final SgmlPage page = getPage();
989 
990         if ((!ignoreVisibility && !isDisplayed())
991                 || (this instanceof DisabledElement && ((DisabledElement) this).isDisabled())) {
992             return (P) page;
993         }
994 
995         // may be different from page when working with "orphaned pages"
996         // (ex: clicking a link in a page that is not active anymore)
997         final Page contentPage = page.getEnclosingWindow().getEnclosedPage();
998 
999         boolean stateUpdated = false;
1000         boolean changed = false;
1001         if (isStateUpdateFirst()) {
1002             changed = doClickStateUpdate(event.isShiftKey(), event.isCtrlKey());
1003             stateUpdated = true;
1004         }
1005 
1006         final AbstractJavaScriptEngine<?> jsEngine = page.getWebClient().getJavaScriptEngine();
1007         jsEngine.holdPosponedActions();
1008         try {
1009             final ScriptResult scriptResult = doClickFireClickEvent(event);
1010             final boolean eventIsAborted = event.isAborted(scriptResult);
1011 
1012             final boolean pageAlreadyChanged = contentPage != page.getEnclosingWindow().getEnclosedPage();
1013             if (!pageAlreadyChanged && !stateUpdated && !eventIsAborted) {
1014                 changed = doClickStateUpdate(event.isShiftKey(), event.isCtrlKey());
1015             }
1016         }
1017         finally {
1018             jsEngine.processPostponedActions();
1019         }
1020 
1021         if (changed) {
1022             doClickFireChangeEvent();
1023         }
1024 
1025         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1026     }
1027 
1028     /**
1029      * This method implements the control state update part of the click action.
1030      *
1031      * <p>The default implementation only calls doClickStateUpdate on parent's DomElement (if any).
1032      * Subclasses requiring different behavior (like {@link HtmlSubmitInput}) will override this method.</p>
1033      * @param shiftKey {@code true} if SHIFT is pressed
1034      * @param ctrlKey {@code true} if CTRL is pressed
1035      *
1036      * @return true if doClickFireEvent method has to be called later on (to signal,
1037      * that the value was changed)
1038      * @throws IOException if an IO error occurs
1039      */
1040     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
1041         if (propagateClickStateUpdateToParent()) {
1042             // needed for instance to perform link doClickAction when a nested element is clicked
1043             // it should probably be changed to do this at the event level but currently
1044             // this wouldn't work with JS disabled as events are propagated in the host object tree.
1045             final DomNode parent = getParentNode();
1046             if (parent instanceof DomElement) {
1047                 return ((DomElement) parent).doClickStateUpdate(false, false);
1048             }
1049         }
1050 
1051         return false;
1052     }
1053 
1054     /**
1055      * @see #doClickStateUpdate(boolean, boolean)
1056      * Usually the click is propagated to the parent. Overwrite if you
1057      * like to disable this.
1058      *
1059      * @return true or false
1060      */
1061     protected boolean propagateClickStateUpdateToParent() {
1062         return true;
1063     }
1064 
1065     /**
1066      * This method implements the control onchange handler call during the click action.
1067      */
1068     protected void doClickFireChangeEvent() {
1069         // nothing to do, in the default case
1070     }
1071 
1072     /**
1073      * This method implements the control onclick handler call during the click action.
1074      * @param event the click event used
1075      * @return the script result
1076      */
1077     protected ScriptResult doClickFireClickEvent(final Event event) {
1078         return fireEvent(event);
1079     }
1080 
1081     /**
1082      * Simulates double-clicking on this element, returning the page in the window that has the focus
1083      * after the element has been clicked. Note that the returned page may or may not be the same
1084      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1085      * action listeners, etc. Note also that {@link #click()} is automatically called first.
1086      *
1087      * @param <P> the page type
1088      * @return the page that occupies this element's window after the element has been double-clicked
1089      * @exception IOException if an IO error occurs
1090      */
1091     @SuppressWarnings("unchecked")
1092     public <P extends Page> P dblClick() throws IOException {
1093         return (P) dblClick(false, false, false);
1094     }
1095 
1096     /**
1097      * Simulates double-clicking on this element, returning the page in the window that has the focus
1098      * after the element has been clicked. Note that the returned page may or may not be the same
1099      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1100      * action listeners, etc. Note also that {@link #click(boolean, boolean, boolean)} is automatically
1101      * called first.
1102      *
1103      * @param shiftKey {@code true} if SHIFT is pressed during the double-click
1104      * @param ctrlKey {@code true} if CTRL is pressed during the double-click
1105      * @param altKey {@code true} if ALT is pressed during the double-click
1106      * @param <P> the page type
1107      * @return the page that occupies this element's window after the element has been double-clicked
1108      * @exception IOException if an IO error occurs
1109      */
1110     @SuppressWarnings("unchecked")
1111     public <P extends Page> P dblClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
1112         throws IOException {
1113         if (this instanceof DisabledElement && ((DisabledElement) this).isDisabled()) {
1114             return (P) getPage();
1115         }
1116 
1117         // call click event first
1118         P clickPage = click(shiftKey, ctrlKey, altKey);
1119         if (clickPage != getPage()) {
1120             if (LOG.isDebugEnabled()) {
1121                 LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1122             }
1123             return clickPage;
1124         }
1125 
1126         // call click event a second time
1127         clickPage = click(shiftKey, ctrlKey, altKey);
1128         if (clickPage != getPage()) {
1129             if (LOG.isDebugEnabled()) {
1130                 LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1131             }
1132             return clickPage;
1133         }
1134 
1135         final Event event;
1136         if (getPage().getWebClient().getBrowserVersion().hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) {
1137             event = new PointerEvent(this, MouseEvent.TYPE_DBL_CLICK, shiftKey, ctrlKey, altKey,
1138                     MouseEvent.BUTTON_LEFT);
1139         }
1140         else {
1141             event = new MouseEvent(this, MouseEvent.TYPE_DBL_CLICK, shiftKey, ctrlKey, altKey,
1142                     MouseEvent.BUTTON_LEFT);
1143         }
1144         final ScriptResult scriptResult = fireEvent(event);
1145         if (scriptResult == null) {
1146             return clickPage;
1147         }
1148         return (P) scriptResult.getNewPage();
1149     }
1150 
1151     /**
1152      * Simulates moving the mouse over this element, returning the page which this element's window contains
1153      * after the mouse move. The returned page may or may not be the same as the original page, depending
1154      * on JavaScript event handlers, etc.
1155      *
1156      * @return the page which this element's window contains after the mouse move
1157      */
1158     public Page mouseOver() {
1159         return mouseOver(false, false, false, MouseEvent.BUTTON_LEFT);
1160     }
1161 
1162     /**
1163      * Simulates moving the mouse over this element, returning the page which this element's window contains
1164      * after the mouse move. The returned page may or may not be the same as the original page, depending
1165      * on JavaScript event handlers, etc.
1166      *
1167      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1168      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1169      * @param altKey {@code true} if ALT is pressed during the mouse move
1170      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1171      *        or {@link MouseEvent#BUTTON_RIGHT}
1172      * @return the page which this element's window contains after the mouse move
1173      */
1174     public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1175         return doMouseEvent(MouseEvent.TYPE_MOUSE_OVER, shiftKey, ctrlKey, altKey, button);
1176     }
1177 
1178     /**
1179      * Simulates moving the mouse over this element, returning the page which this element's window contains
1180      * after the mouse move. The returned page may or may not be the same as the original page, depending
1181      * on JavaScript event handlers, etc.
1182      *
1183      * @return the page which this element's window contains after the mouse move
1184      */
1185     public Page mouseMove() {
1186         return mouseMove(false, false, false, MouseEvent.BUTTON_LEFT);
1187     }
1188 
1189     /**
1190      * Simulates moving the mouse over this element, returning the page which this element's window contains
1191      * after the mouse move. The returned page may or may not be the same as the original page, depending
1192      * on JavaScript event handlers, etc.
1193      *
1194      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1195      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1196      * @param altKey {@code true} if ALT is pressed during the mouse move
1197      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1198      *        or {@link MouseEvent#BUTTON_RIGHT}
1199      * @return the page which this element's window contains after the mouse move
1200      */
1201     public Page mouseMove(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1202         return doMouseEvent(MouseEvent.TYPE_MOUSE_MOVE, shiftKey, ctrlKey, altKey, button);
1203     }
1204 
1205     /**
1206      * Simulates moving the mouse out of this element, returning the page which this element's window contains
1207      * after the mouse move. The returned page may or may not be the same as the original page, depending
1208      * on JavaScript event handlers, etc.
1209      *
1210      * @return the page which this element's window contains after the mouse move
1211      */
1212     public Page mouseOut() {
1213         return mouseOut(false, false, false, MouseEvent.BUTTON_LEFT);
1214     }
1215 
1216     /**
1217      * Simulates moving the mouse out of this element, returning the page which this element's window contains
1218      * after the mouse move. The returned page may or may not be the same as the original page, depending
1219      * on JavaScript event handlers, etc.
1220      *
1221      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1222      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1223      * @param altKey {@code true} if ALT is pressed during the mouse move
1224      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1225      *        or {@link MouseEvent#BUTTON_RIGHT}
1226      * @return the page which this element's window contains after the mouse move
1227      */
1228     public Page mouseOut(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1229         return doMouseEvent(MouseEvent.TYPE_MOUSE_OUT, shiftKey, ctrlKey, altKey, button);
1230     }
1231 
1232     /**
1233      * Simulates clicking the mouse on this element, returning the page which this element's window contains
1234      * after the mouse click. The returned page may or may not be the same as the original page, depending
1235      * on JavaScript event handlers, etc.
1236      *
1237      * @return the page which this element's window contains after the mouse click
1238      */
1239     public Page mouseDown() {
1240         return mouseDown(false, false, false, MouseEvent.BUTTON_LEFT);
1241     }
1242 
1243     /**
1244      * Simulates clicking the mouse on this element, returning the page which this element's window contains
1245      * after the mouse click. The returned page may or may not be the same as the original page, depending
1246      * on JavaScript event handlers, etc.
1247      *
1248      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click
1249      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click
1250      * @param altKey {@code true} if ALT is pressed during the mouse click
1251      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1252      *        or {@link MouseEvent#BUTTON_RIGHT}
1253      * @return the page which this element's window contains after the mouse click
1254      */
1255     public Page mouseDown(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1256         return doMouseEvent(MouseEvent.TYPE_MOUSE_DOWN, shiftKey, ctrlKey, altKey, button);
1257     }
1258 
1259     /**
1260      * Simulates releasing the mouse click on this element, returning the page which this element's window contains
1261      * after the mouse click release. The returned page may or may not be the same as the original page, depending
1262      * on JavaScript event handlers, etc.
1263      *
1264      * @return the page which this element's window contains after the mouse click release
1265      */
1266     public Page mouseUp() {
1267         return mouseUp(false, false, false, MouseEvent.BUTTON_LEFT);
1268     }
1269 
1270     /**
1271      * Simulates releasing the mouse click on this element, returning the page which this element's window contains
1272      * after the mouse click release. The returned page may or may not be the same as the original page, depending
1273      * on JavaScript event handlers, etc.
1274      *
1275      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click release
1276      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click release
1277      * @param altKey {@code true} if ALT is pressed during the mouse click release
1278      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1279      *        or {@link MouseEvent#BUTTON_RIGHT}
1280      * @return the page which this element's window contains after the mouse click release
1281      */
1282     public Page mouseUp(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1283         return doMouseEvent(MouseEvent.TYPE_MOUSE_UP, shiftKey, ctrlKey, altKey, button);
1284     }
1285 
1286     /**
1287      * Simulates right clicking the mouse on this element, returning the page which this element's window
1288      * contains after the mouse click. The returned page may or may not be the same as the original page,
1289      * depending on JavaScript event handlers, etc.
1290      *
1291      * @return the page which this element's window contains after the mouse click
1292      */
1293     public Page rightClick() {
1294         return rightClick(false, false, false);
1295     }
1296 
1297     /**
1298      * Simulates right clicking the mouse on this element, returning the page which this element's window
1299      * contains after the mouse click. The returned page may or may not be the same as the original page,
1300      * depending on JavaScript event handlers, etc.
1301      *
1302      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click
1303      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click
1304      * @param altKey {@code true} if ALT is pressed during the mouse click
1305      * @return the page which this element's window contains after the mouse click
1306      */
1307     public Page rightClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey) {
1308         final Page mouseDownPage = mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1309         if (mouseDownPage != getPage()) {
1310             if (LOG.isDebugEnabled()) {
1311                 LOG.debug("rightClick() is incomplete, as mouseDown() loaded a different page.");
1312             }
1313             return mouseDownPage;
1314         }
1315         final Page mouseUpPage = mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1316         if (mouseUpPage != getPage()) {
1317             if (LOG.isDebugEnabled()) {
1318                 LOG.debug("rightClick() is incomplete, as mouseUp() loaded a different page.");
1319             }
1320             return mouseUpPage;
1321         }
1322         return doMouseEvent(MouseEvent.TYPE_CONTEXT_MENU, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1323     }
1324 
1325     /**
1326      * Simulates the specified mouse event, returning the page which this element's window contains after the event.
1327      * The returned page may or may not be the same as the original page, depending on JavaScript event handlers, etc.
1328      *
1329      * @param eventType the mouse event type to simulate
1330      * @param shiftKey {@code true} if SHIFT is pressed during the mouse event
1331      * @param ctrlKey {@code true} if CTRL is pressed during the mouse event
1332      * @param altKey {@code true} if ALT is pressed during the mouse event
1333      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1334      *        or {@link MouseEvent#BUTTON_RIGHT}
1335      * @return the page which this element's window contains after the event
1336      */
1337     private Page doMouseEvent(final String eventType, final boolean shiftKey, final boolean ctrlKey,
1338         final boolean altKey, final int button) {
1339         final SgmlPage page = getPage();
1340 
1341         final ScriptResult scriptResult;
1342         final Event event;
1343         if (MouseEvent.TYPE_CONTEXT_MENU.equals(eventType)
1344                 && getPage().getWebClient().getBrowserVersion().hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) {
1345             event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button);
1346         }
1347         else {
1348             event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button);
1349         }
1350         scriptResult = fireEvent(event);
1351 
1352         final Page currentPage;
1353         if (scriptResult == null) {
1354             currentPage = page;
1355         }
1356         else {
1357             currentPage = scriptResult.getNewPage();
1358         }
1359 
1360         final boolean mouseOver = !MouseEvent.TYPE_MOUSE_OUT.equals(eventType);
1361         if (mouseOver_ != mouseOver) {
1362             mouseOver_ = mouseOver;
1363 
1364             final SimpleScriptable scriptable = getScriptableObject();
1365             scriptable.getWindow().clearComputedStyles();
1366         }
1367 
1368         return currentPage;
1369     }
1370 
1371     /**
1372      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1373      *
1374      * Shortcut for {@link #fireEvent(Event)}.
1375      * @param eventType the event type (like "load", "click")
1376      * @return the execution result, or {@code null} if nothing is executed
1377      */
1378     public ScriptResult fireEvent(final String eventType) {
1379         return fireEvent(new Event(this, eventType));
1380     }
1381 
1382     /**
1383      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1384      *
1385      * Fires the event on the element. Nothing is done if JavaScript is disabled.
1386      * @param event the event to fire
1387      * @return the execution result, or {@code null} if nothing is executed
1388      */
1389     public ScriptResult fireEvent(final Event event) {
1390         final WebClient client = getPage().getWebClient();
1391         if (!client.getOptions().isJavaScriptEnabled()) {
1392             return null;
1393         }
1394 
1395         if (!handles(event)) {
1396             return null;
1397         }
1398 
1399         if (LOG.isDebugEnabled()) {
1400             LOG.debug("Firing " + event);
1401         }
1402         final EventTarget jsElt = (EventTarget) getScriptableObject();
1403         final ContextAction action = new ContextAction() {
1404             @Override
1405             public Object run(final Context cx) {
1406                 return jsElt.fireEvent(event);
1407             }
1408         };
1409 
1410         final ContextFactory cf = ((JavaScriptEngine) client.getJavaScriptEngine()).getContextFactory();
1411         final ScriptResult result = (ScriptResult) cf.call(action);
1412         if (event.isAborted(result)) {
1413             preventDefault();
1414         }
1415         return result;
1416     }
1417 
1418     /**
1419      * This method is called if the current fired event is canceled by <tt>preventDefault()</tt> in FireFox,
1420      * or by returning {@code false} in Internet Explorer.
1421      *
1422      * The default implementation does nothing.
1423      */
1424     protected void preventDefault() {
1425         // Empty by default; override as needed.
1426     }
1427 
1428     /**
1429      * Sets the focus on this element.
1430      */
1431     public void focus() {
1432         final HtmlPage page = (HtmlPage) getPage();
1433         page.setFocusedElement(this);
1434         final Object o = getScriptableObject();
1435         if (o instanceof HTMLElement) {
1436             ((HTMLElement) o).setActive();
1437         }
1438     }
1439 
1440     /**
1441      * Removes focus from this element.
1442      */
1443     public void blur() {
1444         ((HtmlPage) getPage()).setFocusedElement(null);
1445     }
1446 
1447     /**
1448      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1449      *
1450      * Gets notified that it has lost the focus.
1451      */
1452     public void removeFocus() {
1453         // nothing
1454     }
1455 
1456     /**
1457      * Returns {@code true} if state updates should be done before onclick event handling. This method
1458      * returns {@code false} by default, and is expected to be overridden to return {@code true} by
1459      * derived classes like {@link HtmlCheckBoxInput}.
1460      * @return {@code true} if state updates should be done before onclick event handling
1461      */
1462     protected boolean isStateUpdateFirst() {
1463         return false;
1464     }
1465 
1466     /**
1467      * Returns whether the Mouse is currently over this element or not.
1468      * @return whether the Mouse is currently over this element or not
1469      */
1470     public boolean isMouseOver() {
1471         if (mouseOver_) {
1472             return true;
1473         }
1474         for (DomElement child : getChildElements()) {
1475             if (child.isMouseOver()) {
1476                 return true;
1477             }
1478         }
1479         return false;
1480     }
1481 
1482     /**
1483      * Returns true if the element would be selected by the specified selector string; otherwise, returns false.
1484      * @param selectorString the selector to test
1485      * @return true if the element would be selected by the specified selector string; otherwise, returns false.
1486      */
1487     public boolean matches(final String selectorString) {
1488         try {
1489             final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion();
1490             final SelectorList selectorList = getSelectorList(selectorString, browserVersion);
1491 
1492             if (selectorList != null) {
1493                 for (int i = 0; i < selectorList.getLength(); i++) {
1494                     final Selector selector = selectorList.item(i);
1495                     if (CSSStyleSheet.selects(browserVersion, selector, this, null, true)) {
1496                         return true;
1497                     }
1498                 }
1499             }
1500             return false;
1501         }
1502         catch (final IOException e) {
1503             throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage());
1504         }
1505     }
1506 }
1507 
1508 /**
1509  * The {@link NamedNodeMap} to store the node attributes.
1510  */
1511 class NamedAttrNodeMapImpl implements Map<String, DomAttr>, NamedNodeMap, Serializable {
1512     public static final NamedAttrNodeMapImpl EMPTY_MAP = new NamedAttrNodeMapImpl();
1513 
1514     private final Map<String, DomAttr> map_ = new LinkedHashMap<>();
1515     private final List<String> attrPositions_ = new ArrayList<>();
1516     private final DomElement domNode_;
1517     private final boolean caseSensitive_;
1518 
1519     private NamedAttrNodeMapImpl() {
1520         super();
1521         domNode_ = null;
1522         caseSensitive_ = true;
1523     }
1524 
1525     NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive) {
1526         super();
1527         if (domNode == null) {
1528             throw new IllegalArgumentException("Provided domNode can't be null.");
1529         }
1530         domNode_ = domNode;
1531         caseSensitive_ = caseSensitive;
1532     }
1533 
1534     NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive,
1535             final Map<String, DomAttr> attributes) {
1536         this(domNode, caseSensitive);
1537         putAll(attributes);
1538     }
1539 
1540     /**
1541      * {@inheritDoc}
1542      */
1543     @Override
1544     public int getLength() {
1545         return size();
1546     }
1547 
1548     /**
1549      * {@inheritDoc}
1550      */
1551     @Override
1552     public DomAttr getNamedItem(final String name) {
1553         return get(name);
1554     }
1555 
1556     private String fixName(final String name) {
1557         if (caseSensitive_) {
1558             return name;
1559         }
1560         return name.toLowerCase(Locale.ROOT);
1561     }
1562 
1563     /**
1564      * {@inheritDoc}
1565      */
1566     @Override
1567     public Node getNamedItemNS(final String namespaceURI, final String localName) {
1568         if (domNode_ == null) {
1569             return null;
1570         }
1571         return get(domNode_.getQualifiedName(namespaceURI, fixName(localName)));
1572     }
1573 
1574     /**
1575      * {@inheritDoc}
1576      */
1577     @Override
1578     public Node item(final int index) {
1579         if (index < 0 || index >= attrPositions_.size()) {
1580             return null;
1581         }
1582         return map_.get(attrPositions_.get(index));
1583     }
1584 
1585     /**
1586      * {@inheritDoc}
1587      */
1588     @Override
1589     public Node removeNamedItem(final String name) throws DOMException {
1590         return remove(name);
1591     }
1592 
1593     /**
1594      * {@inheritDoc}
1595      */
1596     @Override
1597     public Node removeNamedItemNS(final String namespaceURI, final String localName) {
1598         if (domNode_ == null) {
1599             return null;
1600         }
1601         return remove(domNode_.getQualifiedName(namespaceURI, fixName(localName)));
1602     }
1603 
1604     /**
1605      * {@inheritDoc}
1606      */
1607     @Override
1608     public DomAttr setNamedItem(final Node node) {
1609         return put(node.getLocalName(), (DomAttr) node);
1610     }
1611 
1612     /**
1613      * {@inheritDoc}
1614      */
1615     @Override
1616     public Node setNamedItemNS(final Node node) throws DOMException {
1617         return put(node.getNodeName(), (DomAttr) node);
1618     }
1619 
1620     /**
1621      * {@inheritDoc}
1622      */
1623     @Override
1624     public DomAttr put(final String key, final DomAttr value) {
1625         final String name = fixName(key);
1626         final DomAttr previous = map_.put(name, value);
1627         if (null == previous) {
1628             attrPositions_.add(name);
1629         }
1630         return previous;
1631     }
1632 
1633     /**
1634      * {@inheritDoc}
1635      */
1636     @Override
1637     public DomAttr remove(final Object key) {
1638         if (key instanceof String) {
1639             final String name = fixName((String) key);
1640             attrPositions_.remove(name);
1641             return map_.remove(name);
1642         }
1643         return null;
1644     }
1645 
1646     /**
1647      * {@inheritDoc}
1648      */
1649     @Override
1650     public void clear() {
1651         attrPositions_.clear();
1652         map_.clear();
1653     }
1654 
1655     /**
1656      * {@inheritDoc}
1657      */
1658     @Override
1659     public void putAll(final Map<? extends String, ? extends DomAttr> t) {
1660         // add one after the other to save the positions
1661         for (final Map.Entry<? extends String, ? extends DomAttr> entry : t.entrySet()) {
1662             put(entry.getKey(), entry.getValue());
1663         }
1664     }
1665 
1666     /**
1667      * {@inheritDoc}
1668      */
1669     @Override
1670     public boolean containsKey(final Object key) {
1671         if (key instanceof String) {
1672             final String name = fixName((String) key);
1673             return map_.containsKey(name);
1674         }
1675         return false;
1676     }
1677 
1678     /**
1679      * {@inheritDoc}
1680      */
1681     @Override
1682     public DomAttr get(final Object key) {
1683         if (key instanceof String) {
1684             final String name = fixName((String) key);
1685             return map_.get(name);
1686         }
1687         return null;
1688     }
1689 
1690     /**
1691      * {@inheritDoc}
1692      */
1693     @Override
1694     public boolean containsValue(final Object value) {
1695         return map_.containsValue(value);
1696     }
1697 
1698     /**
1699      * {@inheritDoc}
1700      */
1701     @Override
1702     public Set<java.util.Map.Entry<String, DomAttr>> entrySet() {
1703         return map_.entrySet();
1704     }
1705 
1706     /**
1707      * {@inheritDoc}
1708      */
1709     @Override
1710     public boolean isEmpty() {
1711         return map_.isEmpty();
1712     }
1713 
1714     /**
1715      * {@inheritDoc}
1716      */
1717     @Override
1718     public Set<String> keySet() {
1719         return map_.keySet();
1720     }
1721 
1722     /**
1723      * {@inheritDoc}
1724      */
1725     @Override
1726     public int size() {
1727         return map_.size();
1728     }
1729 
1730     /**
1731      * {@inheritDoc}
1732      */
1733     @Override
1734     public Collection<DomAttr> values() {
1735         return map_.values();
1736     }
1737 
1738 }