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