1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.html;
16
17 import static org.htmlunit.BrowserVersionFeatures.EVENT_CONTEXT_MENU_HAS_DETAIL_1;
18 import static org.htmlunit.BrowserVersionFeatures.EVENT_ONCLICK_USES_POINTEREVENT;
19 import static org.htmlunit.BrowserVersionFeatures.JS_AREA_WITHOUT_HREF_FOCUSABLE;
20
21 import java.io.IOException;
22 import java.io.PrintWriter;
23 import java.io.Serializable;
24 import java.io.StringWriter;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.Iterator;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.NoSuchElementException;
35 import java.util.Set;
36
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39 import org.htmlunit.BrowserVersion;
40 import org.htmlunit.Page;
41 import org.htmlunit.ScriptResult;
42 import org.htmlunit.SgmlPage;
43 import org.htmlunit.WebClient;
44 import org.htmlunit.css.ComputedCssStyleDeclaration;
45 import org.htmlunit.css.CssStyleSheet;
46 import org.htmlunit.css.StyleElement;
47 import org.htmlunit.cssparser.dom.CSSStyleDeclarationImpl;
48 import org.htmlunit.cssparser.dom.Property;
49 import org.htmlunit.cssparser.parser.CSSException;
50 import org.htmlunit.cssparser.parser.selector.Selector;
51 import org.htmlunit.cssparser.parser.selector.SelectorList;
52 import org.htmlunit.cssparser.parser.selector.SelectorSpecificity;
53 import org.htmlunit.cyberneko.util.FastHashMap;
54 import org.htmlunit.html.DefaultElementFactory.OrderedFastHashMapWithLowercaseKeys;
55 import org.htmlunit.javascript.AbstractJavaScriptEngine;
56 import org.htmlunit.javascript.JavaScriptEngine;
57 import org.htmlunit.javascript.host.event.Event;
58 import org.htmlunit.javascript.host.event.EventTarget;
59 import org.htmlunit.javascript.host.event.MouseEvent;
60 import org.htmlunit.javascript.host.event.PointerEvent;
61 import org.htmlunit.util.OrderedFastHashMap;
62 import org.htmlunit.util.StringUtils;
63 import org.w3c.dom.Attr;
64 import org.w3c.dom.DOMException;
65 import org.w3c.dom.Element;
66 import org.w3c.dom.NamedNodeMap;
67 import org.w3c.dom.Node;
68 import org.w3c.dom.TypeInfo;
69 import org.xml.sax.SAXException;
70
71
72
73
74
75
76
77
78
79 public class DomElement extends DomNamespaceNode implements Element {
80
81 private static final Log LOG = LogFactory.getLog(DomElement.class);
82
83
84 public static final String ID_ATTRIBUTE = "id";
85
86
87 public static final String NAME_ATTRIBUTE = "name";
88
89
90 public static final String SRC_ATTRIBUTE = "src";
91
92
93 public static final String VALUE_ATTRIBUTE = "value";
94
95
96 public static final String TYPE_ATTRIBUTE = "type";
97
98
99 public static final String ATTRIBUTE_NOT_DEFINED = new String("");
100
101
102 public static final String ATTRIBUTE_VALUE_EMPTY = new String();
103
104
105 private NamedAttrNodeMapImpl attributes_;
106
107
108 private FastHashMap<String, String> namespaces_;
109
110
111 private String styleString_;
112 private LinkedHashMap<String, StyleElement> styleMap_;
113
114 private static final Comparator<StyleElement> STYLE_ELEMENT_COMPARATOR = new Comparator<StyleElement>() {
115 @Override
116 public int compare(final StyleElement first, final StyleElement second) {
117 return StyleElement.compareToByImportanceAndSpecificity(first, second);
118 }
119 };
120
121
122
123
124 private boolean mouseOver_;
125
126
127
128
129
130
131
132
133
134
135 public DomElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
136 final Map<String, DomAttr> attributes) {
137 super(namespaceURI, qualifiedName, page);
138
139 if (attributes == null) {
140 attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive());
141 }
142 else {
143 attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive(), attributes);
144
145 for (final DomAttr entry : attributes.values()) {
146 entry.setParentNode(this);
147 final String attrNamespaceURI = entry.getNamespaceURI();
148 final String prefix = entry.getPrefix();
149
150 if (attrNamespaceURI != null && prefix != null) {
151 if (namespaces_ == null) {
152 namespaces_ = new FastHashMap<>(1, 0.5f);
153 }
154 namespaces_.put(attrNamespaceURI, prefix);
155 }
156 }
157 }
158 }
159
160
161
162
163 @Override
164 public String getNodeName() {
165 return getQualifiedName();
166 }
167
168
169
170
171 @Override
172 public final short getNodeType() {
173 return ELEMENT_NODE;
174 }
175
176
177
178
179
180 @Override
181 public final String getTagName() {
182 return getNodeName();
183 }
184
185
186
187
188 @Override
189 public final boolean hasAttributes() {
190 return !attributes_.isEmpty();
191 }
192
193
194
195
196
197
198
199
200 @Override
201 public boolean hasAttribute(final String attributeName) {
202 return attributes_.containsKey(attributeName);
203 }
204
205
206
207
208
209
210
211
212
213
214
215 public void replaceStyleAttribute(final String name, final String value, final String priority) {
216 if (org.apache.commons.lang3.StringUtils.isBlank(value)) {
217 removeStyleAttribute(name);
218 return;
219 }
220
221 final Map<String, StyleElement> styleMap = getStyleMap();
222 final StyleElement old = styleMap.get(name);
223 final StyleElement element;
224 if (old == null) {
225 element = new StyleElement(name, value, priority, SelectorSpecificity.FROM_STYLE_ATTRIBUTE);
226 }
227 else {
228 element = new StyleElement(name, value, priority,
229 SelectorSpecificity.FROM_STYLE_ATTRIBUTE, old.getIndex());
230 }
231 styleMap.put(name, element);
232 writeStyleToElement(styleMap);
233 }
234
235
236
237
238
239
240
241
242 public String removeStyleAttribute(final String name) {
243 final Map<String, StyleElement> styleMap = getStyleMap();
244 final StyleElement value = styleMap.get(name);
245 if (value == null) {
246 return "";
247 }
248 styleMap.remove(name);
249 writeStyleToElement(styleMap);
250 return value.getValue();
251 }
252
253
254
255
256
257
258
259
260
261 public StyleElement getStyleElement(final String name) {
262 final Map<String, StyleElement> map = getStyleMap();
263 if (map != null) {
264 return map.get(name);
265 }
266 return null;
267 }
268
269
270
271
272
273
274
275
276
277
278 public StyleElement getStyleElementCaseInSensitive(final String name) {
279 final Map<String, StyleElement> map = getStyleMap();
280 for (final Map.Entry<String, StyleElement> entry : map.entrySet()) {
281 if (entry.getKey().equalsIgnoreCase(name)) {
282 return entry.getValue();
283 }
284 }
285 return null;
286 }
287
288
289
290
291
292
293
294
295
296 public LinkedHashMap<String, StyleElement> getStyleMap() {
297 final String styleAttribute = getAttributeDirect("style");
298 if (styleString_ == styleAttribute) {
299 return styleMap_;
300 }
301
302 final LinkedHashMap<String, StyleElement> styleMap = new LinkedHashMap<>();
303 if (ATTRIBUTE_NOT_DEFINED == styleAttribute || ATTRIBUTE_VALUE_EMPTY == styleAttribute) {
304 styleMap_ = styleMap;
305 styleString_ = styleAttribute;
306 return styleMap_;
307 }
308
309 final CSSStyleDeclarationImpl cssStyle = new CSSStyleDeclarationImpl(null);
310 try {
311
312
313 cssStyle.setCssText(styleAttribute, getPage().getWebClient().getCssErrorHandler());
314 }
315 catch (final Exception e) {
316 if (LOG.isErrorEnabled()) {
317 LOG.error("Error while parsing style value '" + styleAttribute + "'", e);
318 }
319 }
320
321 for (final Property prop : cssStyle.getProperties()) {
322 final String key = prop.getName().toLowerCase(Locale.ROOT);
323 final StyleElement element = new StyleElement(key,
324 prop.getValue().getCssText(),
325 prop.isImportant() ? StyleElement.PRIORITY_IMPORTANT : "",
326 SelectorSpecificity.FROM_STYLE_ATTRIBUTE);
327 styleMap.put(key, element);
328 }
329
330 styleMap_ = styleMap;
331 styleString_ = styleAttribute;
332
333 return styleMap_;
334 }
335
336
337
338
339
340
341 protected void printOpeningTagContentAsXml(final PrintWriter printWriter) {
342 printWriter.print(getTagName());
343 for (final Map.Entry<String, DomAttr> entry : attributes_.entrySet()) {
344 printWriter.print(" ");
345 printWriter.print(entry.getKey());
346 printWriter.print("=\"");
347 printWriter.print(StringUtils.escapeXmlAttributeValue(entry.getValue().getNodeValue()));
348 printWriter.print("\"");
349 }
350 }
351
352
353
354
355
356
357
358 @Override
359 protected void printXml(final String indent, final PrintWriter printWriter) {
360 final boolean hasChildren = getFirstChild() != null;
361 printWriter.print(indent + "<");
362 printOpeningTagContentAsXml(printWriter);
363
364 if (hasChildren || isEmptyXmlTagExpanded()) {
365 printWriter.print(">\r\n");
366 printChildrenAsXml(indent, printWriter);
367 printWriter.print(indent);
368 printWriter.print("</");
369 printWriter.print(getTagName());
370 printWriter.print(">\r\n");
371 }
372 else {
373 printWriter.print("/>\r\n");
374 }
375 }
376
377
378
379
380
381
382 protected boolean isEmptyXmlTagExpanded() {
383 return false;
384 }
385
386
387
388
389
390
391
392
393
394 String getQualifiedName(final String namespaceURI, final String localName) {
395 final String qualifiedName;
396 if (namespaceURI == null) {
397 qualifiedName = localName;
398 }
399 else {
400 final String prefix = namespaces_ == null ? null : namespaces_.get(namespaceURI);
401 if (prefix == null) {
402 qualifiedName = null;
403 }
404 else {
405 qualifiedName = prefix + ':' + localName;
406 }
407 }
408 return qualifiedName;
409 }
410
411
412
413
414
415
416
417
418
419
420 @Override
421 public String getAttribute(final String attributeName) {
422 final DomAttr attr = attributes_.get(attributeName);
423 if (attr != null) {
424 return attr.getNodeValue();
425 }
426 return ATTRIBUTE_NOT_DEFINED;
427 }
428
429
430
431
432
433
434
435 public String getAttributeDirect(final String attributeName) {
436 final DomAttr attr = attributes_.getDirect(attributeName);
437 if (attr != null) {
438 return attr.getNodeValue();
439 }
440 return ATTRIBUTE_NOT_DEFINED;
441 }
442
443
444
445
446
447 @Override
448 public void removeAttribute(final String attributeName) {
449 attributes_.remove(attributeName);
450 }
451
452
453
454
455
456
457 @Override
458 public final void removeAttributeNS(final String namespaceURI, final String localName) {
459 final String qualifiedName = getQualifiedName(namespaceURI, localName);
460 if (qualifiedName != null) {
461 removeAttribute(qualifiedName);
462 }
463 }
464
465
466
467
468
469 @Override
470 public final Attr removeAttributeNode(final Attr attribute) {
471 throw new UnsupportedOperationException("DomElement.removeAttributeNode is not yet implemented.");
472 }
473
474
475
476
477
478
479
480
481
482 @Override
483 public final boolean hasAttributeNS(final String namespaceURI, final String localName) {
484 final String qualifiedName = getQualifiedName(namespaceURI, localName);
485 if (qualifiedName != null) {
486 return attributes_.get(qualifiedName) != null;
487 }
488 return false;
489 }
490
491
492
493
494
495 public final Map<String, DomAttr> getAttributesMap() {
496 return attributes_;
497 }
498
499
500
501
502 @Override
503 public NamedNodeMap getAttributes() {
504 return attributes_;
505 }
506
507
508
509
510
511
512
513 @Override
514 public void setAttribute(final String attributeName, final String attributeValue) {
515 setAttributeNS(null, attributeName, attributeValue);
516 }
517
518
519
520
521
522
523
524
525 @Override
526 public void setAttributeNS(final String namespaceURI, final String qualifiedName,
527 final String attributeValue) {
528 setAttributeNS(namespaceURI, qualifiedName, attributeValue, true, true);
529 }
530
531
532
533
534
535
536
537
538
539
540 protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
541 final String attributeValue, final boolean notifyAttributeChangeListeners,
542 final boolean notifyMutationObservers) {
543 final DomAttr newAttr = new DomAttr(getPage(), namespaceURI, qualifiedName, attributeValue, true);
544 newAttr.setParentNode(this);
545 attributes_.put(qualifiedName, newAttr);
546
547 if (namespaceURI != null) {
548 if (namespaces_ == null) {
549 namespaces_ = new FastHashMap<>(1, 0.5f);
550 }
551 namespaces_.put(namespaceURI, newAttr.getPrefix());
552 }
553 }
554
555
556
557
558
559 protected boolean isAttributeCaseSensitive() {
560 return true;
561 }
562
563
564
565
566
567
568
569
570
571
572
573 @Override
574 public final String getAttributeNS(final String namespaceURI, final String localName) {
575 final String qualifiedName = getQualifiedName(namespaceURI, localName);
576 if (qualifiedName != null) {
577 return getAttribute(qualifiedName);
578 }
579 return ATTRIBUTE_NOT_DEFINED;
580 }
581
582
583
584
585 @Override
586 public DomAttr getAttributeNode(final String name) {
587 return attributes_.get(name);
588 }
589
590
591
592
593 @Override
594 public DomAttr getAttributeNodeNS(final String namespaceURI, final String localName) {
595 final String qualifiedName = getQualifiedName(namespaceURI, localName);
596 if (qualifiedName != null) {
597 return attributes_.get(qualifiedName);
598 }
599 return null;
600 }
601
602
603
604
605
606
607 public void writeStyleToElement(final Map<String, StyleElement> styleMap) {
608 if (styleMap.isEmpty()) {
609 setAttribute("style", "");
610 return;
611 }
612
613 final StringBuilder builder = new StringBuilder();
614 final List<StyleElement> styleElements = new ArrayList<>(styleMap.values());
615 Collections.sort(styleElements, STYLE_ELEMENT_COMPARATOR);
616 for (final StyleElement e : styleElements) {
617 if (builder.length() != 0) {
618 builder.append(' ');
619 }
620 builder.append(e.getName())
621 .append(": ")
622 .append(e.getValue());
623
624 final String prio = e.getPriority();
625 if (org.apache.commons.lang3.StringUtils.isNotBlank(prio)) {
626 builder.append(" !").append(prio);
627 }
628 builder.append(';');
629 }
630 setAttribute("style", builder.toString());
631 }
632
633
634
635
636 @Override
637 public DomNodeList<HtmlElement> getElementsByTagName(final String tagName) {
638 return getElementsByTagNameImpl(tagName);
639 }
640
641
642
643
644
645
646 <E extends HtmlElement> DomNodeList<E> getElementsByTagNameImpl(final String tagName) {
647 return new AbstractDomNodeList<E>(this) {
648 @Override
649 @SuppressWarnings("unchecked")
650 protected List<E> provideElements() {
651 final List<E> res = new ArrayList<>();
652 for (final HtmlElement elem : getDomNode().getHtmlElementDescendants()) {
653 if (elem.getLocalName().equalsIgnoreCase(tagName)) {
654 res.add((E) elem);
655 }
656 }
657 return res;
658 }
659 };
660 }
661
662
663
664
665
666
667
668
669 public <E extends HtmlElement> List<E> getStaticElementsByTagName(final String tagName) {
670 final List<E> res = new ArrayList<>();
671 for (final Iterator<HtmlElement> iterator = this.new DescendantHtmlElementsIterator(); iterator.hasNext();) {
672 final HtmlElement elem = iterator.next();
673 if (elem.getLocalName().equalsIgnoreCase(tagName)) {
674 final String prefix = elem.getPrefix();
675 if (prefix == null || prefix.isEmpty()) {
676 res.add((E) elem);
677 }
678 }
679 }
680 return res;
681 }
682
683
684
685
686
687 @Override
688 public DomNodeList<HtmlElement> getElementsByTagNameNS(final String namespace, final String localName) {
689 throw new UnsupportedOperationException("DomElement.getElementsByTagNameNS is not yet implemented.");
690 }
691
692
693
694
695
696 @Override
697 public TypeInfo getSchemaTypeInfo() {
698 throw new UnsupportedOperationException("DomElement.getSchemaTypeInfo is not yet implemented.");
699 }
700
701
702
703
704
705 @Override
706 public void setIdAttribute(final String name, final boolean isId) {
707 throw new UnsupportedOperationException("DomElement.setIdAttribute is not yet implemented.");
708 }
709
710
711
712
713
714 @Override
715 public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) {
716 throw new UnsupportedOperationException("DomElement.setIdAttributeNS is not yet implemented.");
717 }
718
719
720
721
722 @Override
723 public Attr setAttributeNode(final Attr attribute) {
724 attributes_.setNamedItem(attribute);
725 return null;
726 }
727
728
729
730
731
732 @Override
733 public Attr setAttributeNodeNS(final Attr attribute) {
734 throw new UnsupportedOperationException("DomElement.setAttributeNodeNS is not yet implemented.");
735 }
736
737
738
739
740
741 @Override
742 public final void setIdAttributeNode(final Attr idAttr, final boolean isId) {
743 throw new UnsupportedOperationException("DomElement.setIdAttributeNode is not yet implemented.");
744 }
745
746
747
748
749 @Override
750 public DomNode cloneNode(final boolean deep) {
751 final DomElement clone = (DomElement) super.cloneNode(deep);
752 clone.attributes_ = new NamedAttrNodeMapImpl(clone, isAttributeCaseSensitive());
753 clone.attributes_.putAll(attributes_);
754 return clone;
755 }
756
757
758
759
760 public final String getId() {
761 return getAttributeDirect(ID_ATTRIBUTE);
762 }
763
764
765
766
767
768
769 public final void setId(final String newId) {
770 setAttribute(ID_ATTRIBUTE, newId);
771 }
772
773
774
775
776
777 public DomElement getFirstElementChild() {
778 final Iterator<DomElement> i = getChildElements().iterator();
779 if (i.hasNext()) {
780 return i.next();
781 }
782 return null;
783 }
784
785
786
787
788
789 public DomElement getLastElementChild() {
790 DomElement lastChild = null;
791 for (final DomElement domElement : getChildElements()) {
792 lastChild = domElement;
793 }
794 return lastChild;
795 }
796
797
798
799
800
801 public int getChildElementCount() {
802 int counter = 0;
803
804 final Iterator<DomElement> iterator = getChildElements().iterator();
805 while (iterator.hasNext()) {
806 iterator.next();
807 counter++;
808 }
809 return counter;
810 }
811
812
813
814
815 public final Iterable<DomElement> getChildElements() {
816 return new ChildElementsIterable(this);
817 }
818
819
820
821
822 private static class ChildElementsIterable implements Iterable<DomElement> {
823 private final Iterator<DomElement> iterator_;
824
825
826
827
828
829 protected ChildElementsIterable(final DomNode domNode) {
830 iterator_ = new ChildElementsIterator(domNode);
831 }
832
833 @Override
834 public Iterator<DomElement> iterator() {
835 return iterator_;
836 }
837 }
838
839
840
841
842 protected static class ChildElementsIterator implements Iterator<DomElement> {
843
844 private DomElement nextElement_;
845
846
847
848
849
850 protected ChildElementsIterator(final DomNode domNode) {
851 final DomNode child = domNode.getFirstChild();
852 if (child != null) {
853 if (child instanceof DomElement) {
854 nextElement_ = (DomElement) child;
855 }
856 else {
857 setNextElement(child);
858 }
859 }
860 }
861
862
863
864
865 @Override
866 public boolean hasNext() {
867 return nextElement_ != null;
868 }
869
870
871
872
873 @Override
874 public DomElement next() {
875 if (nextElement_ != null) {
876 final DomElement result = nextElement_;
877 setNextElement(nextElement_);
878 return result;
879 }
880 throw new NoSuchElementException();
881 }
882
883
884 @Override
885 public void remove() {
886 if (nextElement_ == null) {
887 throw new IllegalStateException();
888 }
889 final DomNode sibling = nextElement_.getPreviousSibling();
890 if (sibling != null) {
891 sibling.remove();
892 }
893 }
894
895 private void setNextElement(final DomNode node) {
896 DomNode next = node.getNextSibling();
897 while (next != null && !(next instanceof DomElement)) {
898 next = next.getNextSibling();
899 }
900 nextElement_ = (DomElement) next;
901 }
902 }
903
904
905
906
907
908 @Override
909 public String toString() {
910 final StringWriter writer = new StringWriter();
911 final PrintWriter printWriter = new PrintWriter(writer);
912
913 printWriter.print(getClass().getSimpleName());
914 printWriter.print("[<");
915 printOpeningTagContentAsXml(printWriter);
916 printWriter.print(">]");
917 printWriter.flush();
918 return writer.toString();
919 }
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935 public <P extends Page> P click() throws IOException {
936 return click(false, false, false);
937 }
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956 public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
957 throws IOException {
958
959 return click(shiftKey, ctrlKey, altKey, true);
960 }
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980 public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
981 final boolean triggerMouseEvents) throws IOException {
982 return click(shiftKey, ctrlKey, altKey, triggerMouseEvents, true, false, false);
983 }
984
985
986
987
988 protected boolean isDisabledElementAndDisabled() {
989 return this instanceof DisabledElement && ((DisabledElement) this).isDisabled();
990 }
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011 @SuppressWarnings("unchecked")
1012 public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
1013 final boolean triggerMouseEvents, final boolean handleFocus, final boolean ignoreVisibility,
1014 final boolean disableProcessLabelAfterBubbling) throws IOException {
1015
1016
1017 final SgmlPage page = getPage();
1018 final WebClient webClient = page.getWebClient();
1019 webClient.setCurrentWindow(page.getEnclosingWindow());
1020
1021 if (!ignoreVisibility) {
1022 if (!(page instanceof HtmlPage)) {
1023 return (P) page;
1024 }
1025
1026 if (!isDisplayed()) {
1027 if (LOG.isWarnEnabled()) {
1028 LOG.warn("Calling click() ignored because the target element '" + this
1029 + "' is not displayed.");
1030 }
1031 return (P) page;
1032 }
1033
1034 if (isDisabledElementAndDisabled()) {
1035 if (LOG.isWarnEnabled()) {
1036 LOG.warn("Calling click() ignored because the target element '" + this + "' is disabled.");
1037 }
1038 return (P) page;
1039 }
1040 }
1041
1042 synchronized (page) {
1043 if (triggerMouseEvents) {
1044 mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
1045 }
1046
1047 final AbstractJavaScriptEngine<?> jsEngine = webClient.getJavaScriptEngine();
1048 if (webClient.isJavaScriptEnabled()) {
1049 jsEngine.holdPosponedActions();
1050 }
1051 try {
1052 if (handleFocus) {
1053
1054 DomElement elementToFocus = null;
1055 if (this instanceof SubmittableElement
1056 || this instanceof HtmlAnchor
1057 && ATTRIBUTE_NOT_DEFINED != ((HtmlAnchor) this).getHrefAttribute()
1058 || this instanceof HtmlArea
1059 && (ATTRIBUTE_NOT_DEFINED != ((HtmlArea) this).getHrefAttribute()
1060 || webClient.getBrowserVersion().hasFeature(JS_AREA_WITHOUT_HREF_FOCUSABLE))
1061 || this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null) {
1062 elementToFocus = this;
1063 }
1064 else if (this instanceof HtmlOption) {
1065 elementToFocus = ((HtmlOption) this).getEnclosingSelect();
1066 }
1067
1068 if (elementToFocus == null) {
1069 ((HtmlPage) page).setFocusedElement(null);
1070 }
1071 else {
1072 elementToFocus.focus();
1073 }
1074 }
1075
1076 if (triggerMouseEvents) {
1077 mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
1078 }
1079
1080 MouseEvent event = null;
1081 if (webClient.isJavaScriptEnabled()) {
1082 final BrowserVersion browser = webClient.getBrowserVersion();
1083 if (browser.hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) {
1084 event = new PointerEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey,
1085 ctrlKey, altKey, MouseEvent.BUTTON_LEFT, 1);
1086 }
1087 else {
1088 event = new MouseEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey,
1089 ctrlKey, altKey, MouseEvent.BUTTON_LEFT, 1);
1090 }
1091
1092 if (disableProcessLabelAfterBubbling) {
1093 event.disableProcessLabelAfterBubbling();
1094 }
1095 }
1096 click(event, shiftKey, ctrlKey, altKey, ignoreVisibility);
1097 }
1098 finally {
1099 if (webClient.isJavaScriptEnabled()) {
1100 jsEngine.processPostponedActions();
1101 }
1102 }
1103
1104 return (P) webClient.getCurrentWindow().getEnclosedPage();
1105 }
1106 }
1107
1108
1109
1110
1111
1112
1113 protected DomNode getEventTargetElement() {
1114 return this;
1115 }
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134 @SuppressWarnings("unchecked")
1135 public <P extends Page> P click(final Event event,
1136 final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
1137 final boolean ignoreVisibility) throws IOException {
1138 final SgmlPage page = getPage();
1139
1140 if ((!ignoreVisibility && !isDisplayed()) || isDisabledElementAndDisabled()) {
1141 return (P) page;
1142 }
1143
1144 final WebClient webClient = page.getWebClient();
1145 if (!webClient.isJavaScriptEnabled()) {
1146 doClickStateUpdate(shiftKey, ctrlKey);
1147
1148 webClient.loadDownloadedResponses();
1149 return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1150 }
1151
1152
1153
1154 final Page contentPage = page.getEnclosingWindow().getEnclosedPage();
1155
1156 boolean stateUpdated = false;
1157 boolean changed = false;
1158 if (isStateUpdateFirst()) {
1159 changed = doClickStateUpdate(shiftKey, ctrlKey);
1160 stateUpdated = true;
1161 }
1162
1163 final ScriptResult scriptResult = doClickFireClickEvent(event);
1164 final boolean eventIsAborted = event.isAborted(scriptResult);
1165
1166 final boolean pageAlreadyChanged = contentPage != page.getEnclosingWindow().getEnclosedPage();
1167 if (!pageAlreadyChanged && !stateUpdated && !eventIsAborted) {
1168 changed = doClickStateUpdate(shiftKey, ctrlKey);
1169 }
1170
1171 if (changed) {
1172 doClickFireChangeEvent();
1173 }
1174
1175 webClient.loadDownloadedResponses();
1176 return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1177 }
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191 protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
1192 if (propagateClickStateUpdateToParent()) {
1193
1194
1195
1196 final DomNode parent = getParentNode();
1197 if (parent instanceof DomElement) {
1198 return ((DomElement) parent).doClickStateUpdate(false, false);
1199 }
1200 }
1201
1202 return false;
1203 }
1204
1205
1206
1207
1208
1209
1210 protected boolean propagateClickStateUpdateToParent() {
1211 return true;
1212 }
1213
1214
1215
1216
1217 protected void doClickFireChangeEvent() {
1218
1219 }
1220
1221
1222
1223
1224
1225
1226 protected ScriptResult doClickFireClickEvent(final Event event) {
1227 return fireEvent(event);
1228 }
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240 public <P extends Page> P dblClick() throws IOException {
1241 return dblClick(false, false, false);
1242 }
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258 @SuppressWarnings("unchecked")
1259 public <P extends Page> P dblClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
1260 throws IOException {
1261 if (isDisabledElementAndDisabled()) {
1262 return (P) getPage();
1263 }
1264
1265
1266 P clickPage = click(shiftKey, ctrlKey, altKey);
1267 if (clickPage != getPage()) {
1268 LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1269 return clickPage;
1270 }
1271
1272
1273 clickPage = click(shiftKey, ctrlKey, altKey);
1274 if (clickPage != getPage()) {
1275 LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1276 return clickPage;
1277 }
1278
1279 final Event event;
1280 event = new MouseEvent(this, MouseEvent.TYPE_DBL_CLICK, shiftKey, ctrlKey, altKey,
1281 MouseEvent.BUTTON_LEFT, 2);
1282
1283 final ScriptResult scriptResult = fireEvent(event);
1284 if (scriptResult == null) {
1285 return clickPage;
1286 }
1287 return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1288 }
1289
1290
1291
1292
1293
1294
1295
1296
1297 public Page mouseOver() {
1298 return mouseOver(false, false, false, MouseEvent.BUTTON_LEFT);
1299 }
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313 public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1314 return doMouseEvent(MouseEvent.TYPE_MOUSE_OVER, shiftKey, ctrlKey, altKey, button);
1315 }
1316
1317
1318
1319
1320
1321
1322
1323
1324 public Page mouseMove() {
1325 return mouseMove(false, false, false, MouseEvent.BUTTON_LEFT);
1326 }
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340 public Page mouseMove(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1341 return doMouseEvent(MouseEvent.TYPE_MOUSE_MOVE, shiftKey, ctrlKey, altKey, button);
1342 }
1343
1344
1345
1346
1347
1348
1349
1350
1351 public Page mouseOut() {
1352 return mouseOut(false, false, false, MouseEvent.BUTTON_LEFT);
1353 }
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367 public Page mouseOut(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1368 return doMouseEvent(MouseEvent.TYPE_MOUSE_OUT, shiftKey, ctrlKey, altKey, button);
1369 }
1370
1371
1372
1373
1374
1375
1376
1377
1378 public Page mouseDown() {
1379 return mouseDown(false, false, false, MouseEvent.BUTTON_LEFT);
1380 }
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394 public Page mouseDown(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1395 return doMouseEvent(MouseEvent.TYPE_MOUSE_DOWN, shiftKey, ctrlKey, altKey, button);
1396 }
1397
1398
1399
1400
1401
1402
1403
1404
1405 public Page mouseUp() {
1406 return mouseUp(false, false, false, MouseEvent.BUTTON_LEFT);
1407 }
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421 public Page mouseUp(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1422 return doMouseEvent(MouseEvent.TYPE_MOUSE_UP, shiftKey, ctrlKey, altKey, button);
1423 }
1424
1425
1426
1427
1428
1429
1430
1431
1432 public Page rightClick() {
1433 return rightClick(false, false, false);
1434 }
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446 public Page rightClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey) {
1447 final Page mouseDownPage = mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1448 if (mouseDownPage != getPage()) {
1449 LOG.debug("rightClick() is incomplete, as mouseDown() loaded a different page.");
1450 return mouseDownPage;
1451 }
1452
1453 final Page mouseUpPage = mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1454 if (mouseUpPage != getPage()) {
1455 LOG.debug("rightClick() is incomplete, as mouseUp() loaded a different page.");
1456 return mouseUpPage;
1457 }
1458
1459 return doMouseEvent(MouseEvent.TYPE_CONTEXT_MENU, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1460 }
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474 private Page doMouseEvent(final String eventType, final boolean shiftKey, final boolean ctrlKey,
1475 final boolean altKey, final int button) {
1476 final SgmlPage page = getPage();
1477 final WebClient webClient = getPage().getWebClient();
1478 if (!webClient.isJavaScriptEnabled()) {
1479 return page;
1480 }
1481
1482 final ScriptResult scriptResult;
1483 final Event event;
1484 if (MouseEvent.TYPE_CONTEXT_MENU.equals(eventType)) {
1485 final BrowserVersion browserVersion = webClient.getBrowserVersion();
1486 if (browserVersion.hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) {
1487 if (browserVersion.hasFeature(EVENT_CONTEXT_MENU_HAS_DETAIL_1)) {
1488 event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 1);
1489 }
1490 else {
1491 event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 0);
1492 }
1493 }
1494 else {
1495 event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 1);
1496 }
1497 }
1498 else if (MouseEvent.TYPE_DBL_CLICK.equals(eventType)) {
1499 event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 2);
1500 }
1501 else {
1502 event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 1);
1503 }
1504 scriptResult = fireEvent(event);
1505
1506 final Page currentPage;
1507 if (scriptResult == null) {
1508 currentPage = page;
1509 }
1510 else {
1511 currentPage = webClient.getCurrentWindow().getEnclosedPage();
1512 }
1513
1514 final boolean mouseOver = !MouseEvent.TYPE_MOUSE_OUT.equals(eventType);
1515 if (mouseOver_ != mouseOver) {
1516 mouseOver_ = mouseOver;
1517
1518 page.clearComputedStyles();
1519 }
1520
1521 return currentPage;
1522 }
1523
1524
1525
1526
1527
1528
1529
1530
1531 public ScriptResult fireEvent(final String eventType) {
1532 if (getPage().getWebClient().isJavaScriptEnabled()) {
1533 return fireEvent(new Event(this, eventType));
1534 }
1535 return null;
1536 }
1537
1538
1539
1540
1541
1542
1543
1544
1545 public ScriptResult fireEvent(final Event event) {
1546 final WebClient client = getPage().getWebClient();
1547 if (!client.isJavaScriptEnabled()) {
1548 return null;
1549 }
1550
1551 if (!handles(event)) {
1552 return null;
1553 }
1554
1555 if (LOG.isDebugEnabled()) {
1556 LOG.debug("Firing " + event);
1557 }
1558
1559 final EventTarget jsElt = getScriptableObject();
1560 final ScriptResult result = ((JavaScriptEngine) client.getJavaScriptEngine())
1561 .callSecured(cx -> jsElt.fireEvent(event), getHtmlPageOrNull());
1562 if (event.isAborted(result)) {
1563 preventDefault();
1564 }
1565 return result;
1566 }
1567
1568
1569
1570
1571
1572
1573 protected void preventDefault() {
1574
1575 }
1576
1577
1578
1579
1580 public void focus() {
1581 if (!(this instanceof SubmittableElement
1582 || this instanceof HtmlAnchor && ATTRIBUTE_NOT_DEFINED != ((HtmlAnchor) this).getHrefAttribute()
1583 || this instanceof HtmlArea
1584 && (ATTRIBUTE_NOT_DEFINED != ((HtmlArea) this).getHrefAttribute()
1585 || getPage().getWebClient().getBrowserVersion().hasFeature(JS_AREA_WITHOUT_HREF_FOCUSABLE))
1586 || this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null)) {
1587 return;
1588 }
1589
1590 if (!isDisplayed() || isDisabledElementAndDisabled()) {
1591 return;
1592 }
1593
1594 final HtmlPage page = (HtmlPage) getPage();
1595 page.setFocusedElement(this);
1596 }
1597
1598
1599
1600
1601 public void blur() {
1602 final HtmlPage page = (HtmlPage) getPage();
1603 if (page.getFocusedElement() != this) {
1604 return;
1605 }
1606
1607 page.setFocusedElement(null);
1608 }
1609
1610
1611
1612
1613
1614
1615 public void removeFocus() {
1616
1617 }
1618
1619
1620
1621
1622
1623
1624
1625 protected boolean isStateUpdateFirst() {
1626 return false;
1627 }
1628
1629
1630
1631
1632
1633 public boolean isMouseOver() {
1634 if (mouseOver_) {
1635 return true;
1636 }
1637 for (final DomElement child : getChildElements()) {
1638 if (child.isMouseOver()) {
1639 return true;
1640 }
1641 }
1642 return false;
1643 }
1644
1645
1646
1647
1648
1649
1650 public boolean matches(final String selectorString) {
1651 try {
1652 final WebClient webClient = getPage().getWebClient();
1653 final SelectorList selectorList = getSelectorList(selectorString, webClient);
1654
1655 if (selectorList != null) {
1656 for (final Selector selector : selectorList) {
1657 if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, this, null, true, true)) {
1658 return true;
1659 }
1660 }
1661 }
1662 return false;
1663 }
1664 catch (final IOException e) {
1665 throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage(), e);
1666 }
1667 }
1668
1669
1670
1671
1672 @Override
1673 public void setNodeValue(final String value) {
1674
1675 }
1676
1677
1678
1679
1680
1681
1682
1683
1684 public void setDefaults(final ComputedCssStyleDeclaration style) {
1685
1686 }
1687
1688
1689
1690
1691
1692
1693
1694 public void setInnerHtml(final String source) throws SAXException, IOException {
1695 removeAllChildren();
1696 getPage().clearComputedStylesUpToRoot(this);
1697
1698 if (source != null) {
1699 parseHtmlSnippet(source);
1700 }
1701 }
1702 }
1703
1704
1705
1706
1707 class NamedAttrNodeMapImpl implements Map<String, DomAttr>, NamedNodeMap, Serializable {
1708 private final OrderedFastHashMap<String, DomAttr> map_;
1709 private final DomElement domNode_;
1710 private final boolean caseSensitive_;
1711
1712 NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive) {
1713 super();
1714 if (domNode == null) {
1715 throw new IllegalArgumentException("Provided domNode can't be null.");
1716 }
1717 domNode_ = domNode;
1718 caseSensitive_ = caseSensitive;
1719 map_ = new OrderedFastHashMap<>(0);
1720 }
1721
1722 NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive,
1723 final Map<String, DomAttr> attributes) {
1724 super();
1725 if (domNode == null) {
1726 throw new IllegalArgumentException("Provided domNode can't be null.");
1727 }
1728 domNode_ = domNode;
1729 caseSensitive_ = caseSensitive;
1730
1731 if (attributes instanceof OrderedFastHashMapWithLowercaseKeys) {
1732
1733
1734 map_ = (OrderedFastHashMap) attributes;
1735 }
1736 else if (caseSensitive && attributes instanceof OrderedFastHashMap) {
1737
1738
1739 map_ = (OrderedFastHashMap) attributes;
1740 }
1741 else {
1742
1743 map_ = new OrderedFastHashMap<>(attributes.size());
1744
1745 putAll(attributes);
1746 }
1747 }
1748
1749
1750
1751
1752 @Override
1753 public int getLength() {
1754 return size();
1755 }
1756
1757
1758
1759
1760 @Override
1761 public DomAttr getNamedItem(final String name) {
1762 return get(name);
1763 }
1764
1765 private String fixName(final String name) {
1766 if (caseSensitive_) {
1767 return name;
1768 }
1769 return StringUtils.toRootLowerCase(name);
1770 }
1771
1772
1773
1774
1775 @Override
1776 public Node getNamedItemNS(final String namespaceURI, final String localName) {
1777 if (domNode_ == null) {
1778 return null;
1779 }
1780 return get(domNode_.getQualifiedName(namespaceURI, fixName(localName)));
1781 }
1782
1783
1784
1785
1786 @Override
1787 public Node item(final int index) {
1788 if (index < 0 || index >= map_.size()) {
1789 return null;
1790 }
1791 return map_.getValue(index);
1792 }
1793
1794
1795
1796
1797 @Override
1798 public Node removeNamedItem(final String name) throws DOMException {
1799 return remove(name);
1800 }
1801
1802
1803
1804
1805 @Override
1806 public Node removeNamedItemNS(final String namespaceURI, final String localName) {
1807 if (domNode_ == null) {
1808 return null;
1809 }
1810 return remove(domNode_.getQualifiedName(namespaceURI, fixName(localName)));
1811 }
1812
1813
1814
1815
1816 @Override
1817 public DomAttr setNamedItem(final Node node) {
1818 return put(node.getLocalName(), (DomAttr) node);
1819 }
1820
1821
1822
1823
1824 @Override
1825 public Node setNamedItemNS(final Node node) throws DOMException {
1826 return put(node.getNodeName(), (DomAttr) node);
1827 }
1828
1829
1830
1831
1832 @Override
1833 public DomAttr put(final String key, final DomAttr value) {
1834 final String name = fixName(key);
1835 return map_.put(name, value);
1836 }
1837
1838
1839
1840
1841 @Override
1842 public DomAttr remove(final Object key) {
1843 if (key instanceof String) {
1844 final String name = fixName((String) key);
1845 return map_.remove(name);
1846 }
1847 return null;
1848 }
1849
1850
1851
1852
1853 @Override
1854 public void clear() {
1855 map_.clear();
1856 }
1857
1858
1859
1860
1861 @Override
1862 public void putAll(final Map<? extends String, ? extends DomAttr> t) {
1863
1864 for (final Map.Entry<? extends String, ? extends DomAttr> entry : t.entrySet()) {
1865 put(entry.getKey(), entry.getValue());
1866 }
1867 }
1868
1869
1870
1871
1872 @Override
1873 public boolean containsKey(final Object key) {
1874 if (key instanceof String) {
1875 final String name = fixName((String) key);
1876 return map_.containsKey(name);
1877 }
1878 return false;
1879 }
1880
1881
1882
1883
1884 @Override
1885 public DomAttr get(final Object key) {
1886 if (key instanceof String) {
1887 final String name = fixName((String) key);
1888 return map_.get(name);
1889 }
1890 return null;
1891 }
1892
1893
1894
1895
1896
1897 protected DomAttr getDirect(final String key) {
1898 return map_.get(key);
1899 }
1900
1901
1902
1903
1904 @Override
1905 public boolean containsValue(final Object value) {
1906 return map_.containsValue(value);
1907 }
1908
1909
1910
1911
1912 @Override
1913 public Set<Map.Entry<String, DomAttr>> entrySet() {
1914 return map_.entrySet();
1915 }
1916
1917
1918
1919
1920 @Override
1921 public boolean isEmpty() {
1922 return map_.isEmpty();
1923 }
1924
1925
1926
1927
1928 @Override
1929 public Set<String> keySet() {
1930 return map_.keySet();
1931 }
1932
1933
1934
1935
1936 @Override
1937 public int size() {
1938 return map_.size();
1939 }
1940
1941
1942
1943
1944 @Override
1945 public Collection<DomAttr> values() {
1946 return map_.values();
1947 }
1948 }