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_FOCUS_ON_LOAD;
18 import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.ObjectInputStream;
23 import java.io.ObjectOutputStream;
24 import java.io.Serializable;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.nio.charset.Charset;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.LinkedHashSet;
38 import java.util.List;
39 import java.util.Locale;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.WeakHashMap;
43 import java.util.concurrent.ConcurrentHashMap;
44
45 import org.apache.commons.lang3.StringUtils;
46 import org.apache.commons.logging.Log;
47 import org.apache.commons.logging.LogFactory;
48 import org.htmlunit.Cache;
49 import org.htmlunit.ElementNotFoundException;
50 import org.htmlunit.FailingHttpStatusCodeException;
51 import org.htmlunit.History;
52 import org.htmlunit.HttpHeader;
53 import org.htmlunit.OnbeforeunloadHandler;
54 import org.htmlunit.Page;
55 import org.htmlunit.ScriptResult;
56 import org.htmlunit.SgmlPage;
57 import org.htmlunit.TopLevelWindow;
58 import org.htmlunit.WebAssert;
59 import org.htmlunit.WebClient;
60 import org.htmlunit.WebClientOptions;
61 import org.htmlunit.WebRequest;
62 import org.htmlunit.WebResponse;
63 import org.htmlunit.WebWindow;
64 import org.htmlunit.corejs.javascript.Function;
65 import org.htmlunit.corejs.javascript.Script;
66 import org.htmlunit.corejs.javascript.Scriptable;
67 import org.htmlunit.css.ComputedCssStyleDeclaration;
68 import org.htmlunit.css.CssStyleSheet;
69 import org.htmlunit.html.impl.SimpleRange;
70 import org.htmlunit.html.parser.HTMLParserDOMBuilder;
71 import org.htmlunit.http.HttpStatus;
72 import org.htmlunit.javascript.AbstractJavaScriptEngine;
73 import org.htmlunit.javascript.HtmlUnitScriptable;
74 import org.htmlunit.javascript.JavaScriptEngine;
75 import org.htmlunit.javascript.PostponedAction;
76 import org.htmlunit.javascript.host.Window;
77 import org.htmlunit.javascript.host.event.BeforeUnloadEvent;
78 import org.htmlunit.javascript.host.event.Event;
79 import org.htmlunit.javascript.host.event.EventTarget;
80 import org.htmlunit.javascript.host.html.HTMLDocument;
81 import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
82 import org.htmlunit.util.MimeType;
83 import org.htmlunit.util.SerializableLock;
84 import org.htmlunit.util.UrlUtils;
85 import org.w3c.dom.Attr;
86 import org.w3c.dom.Comment;
87 import org.w3c.dom.DOMConfiguration;
88 import org.w3c.dom.DOMException;
89 import org.w3c.dom.DOMImplementation;
90 import org.w3c.dom.Document;
91 import org.w3c.dom.DocumentType;
92 import org.w3c.dom.Element;
93 import org.w3c.dom.EntityReference;
94 import org.w3c.dom.ProcessingInstruction;
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142 public class HtmlPage extends SgmlPage {
143
144 private static final Log LOG = LogFactory.getLog(HtmlPage.class);
145
146 private static final Comparator<DomElement> DOCUMENT_POSITION_COMPERATOR = new DocumentPositionComparator();
147
148 private HTMLParserDOMBuilder domBuilder_;
149 private transient Charset originalCharset_;
150 private final Object lock_ = new SerializableLock();
151
152 private Map<String, MappedElementIndexEntry> idMap_ = new ConcurrentHashMap<>();
153 private Map<String, MappedElementIndexEntry> nameMap_ = new ConcurrentHashMap<>();
154
155 private List<BaseFrameElement> frameElements_ = new ArrayList<>();
156 private int parserCount_;
157 private int snippetParserCount_;
158 private int inlineSnippetParserCount_;
159 private Collection<HtmlAttributeChangeListener> attributeListeners_;
160 private List<PostponedAction> afterLoadActions_ = Collections.synchronizedList(new ArrayList<>());
161 private boolean cleaning_;
162 private HtmlBase base_;
163 private URL baseUrl_;
164 private List<AutoCloseable> autoCloseableList_;
165 private ElementFromPointHandler elementFromPointHandler_;
166 private DomElement elementWithFocus_;
167 private List<SimpleRange> selectionRanges_ = new ArrayList<>(3);
168
169 private transient ComputedStylesCache computedStylesCache_;
170
171 private static final HashSet<String> TABBABLE_TAGS =
172 new HashSet<>(Arrays.asList(HtmlAnchor.TAG_NAME, HtmlArea.TAG_NAME,
173 HtmlButton.TAG_NAME, HtmlInput.TAG_NAME, HtmlObject.TAG_NAME,
174 HtmlSelect.TAG_NAME, HtmlTextArea.TAG_NAME));
175 private static final HashSet<String> ACCEPTABLE_TAG_NAMES =
176 new HashSet<>(Arrays.asList(HtmlAnchor.TAG_NAME, HtmlArea.TAG_NAME,
177 HtmlButton.TAG_NAME, HtmlInput.TAG_NAME, HtmlLabel.TAG_NAME,
178 HtmlLegend.TAG_NAME, HtmlTextArea.TAG_NAME));
179
180
181 private static final Set<String> ATTRIBUTES_AFFECTING_PARENT = new HashSet<>(Arrays.asList(
182 "style",
183 "class",
184 "height",
185 "width"));
186
187 static class DocumentPositionComparator implements Comparator<DomElement>, Serializable {
188 @Override
189 public int compare(final DomElement elt1, final DomElement elt2) {
190 final short relation = elt1.compareDocumentPosition(elt2);
191 if (relation == 0) {
192 return 0;
193 }
194 if ((relation & DOCUMENT_POSITION_CONTAINS) != 0 || (relation & DOCUMENT_POSITION_PRECEDING) != 0) {
195 return 1;
196 }
197
198 return -1;
199 }
200 }
201
202
203
204
205
206
207
208
209 public HtmlPage(final WebResponse webResponse, final WebWindow webWindow) {
210 super(webResponse, webWindow);
211 }
212
213
214
215
216 @Override
217 public HtmlPage getPage() {
218 return this;
219 }
220
221
222
223
224 @Override
225 public boolean hasCaseSensitiveTagNames() {
226 return false;
227 }
228
229
230
231
232
233
234
235
236 @Override
237 public void initialize() throws IOException, FailingHttpStatusCodeException {
238 final WebWindow enclosingWindow = getEnclosingWindow();
239 final boolean isAboutBlank = getUrl() == UrlUtils.URL_ABOUT_BLANK;
240 if (isAboutBlank) {
241
242 if (enclosingWindow instanceof FrameWindow
243 && !((FrameWindow) enclosingWindow).getFrameElement().isContentLoaded()) {
244 return;
245 }
246
247
248 if (enclosingWindow instanceof TopLevelWindow) {
249 final TopLevelWindow topWindow = (TopLevelWindow) enclosingWindow;
250 final WebWindow openerWindow = topWindow.getOpener();
251 if (openerWindow != null && openerWindow.getEnclosedPage() != null) {
252 baseUrl_ = openerWindow.getEnclosedPage().getWebResponse().getWebRequest().getUrl();
253 }
254 }
255 }
256
257 if (!isAboutBlank) {
258 setReadyState(READY_STATE_INTERACTIVE);
259 getDocumentElement().setReadyState(READY_STATE_INTERACTIVE);
260 executeEventHandlersIfNeeded(Event.TYPE_READY_STATE_CHANGE);
261 }
262
263 executeDeferredScriptsIfNeeded();
264
265 executeEventHandlersIfNeeded(Event.TYPE_DOM_DOCUMENT_LOADED);
266
267 loadFrames();
268
269
270
271 if (!isAboutBlank) {
272 setReadyState(READY_STATE_COMPLETE);
273 getDocumentElement().setReadyState(READY_STATE_COMPLETE);
274 executeEventHandlersIfNeeded(Event.TYPE_READY_STATE_CHANGE);
275 }
276
277
278 boolean isFrameWindow = enclosingWindow instanceof FrameWindow;
279 boolean isFirstPageInFrameWindow = false;
280 if (isFrameWindow) {
281 isFrameWindow = ((FrameWindow) enclosingWindow).getFrameElement() instanceof HtmlFrame;
282
283 final History hist = enclosingWindow.getHistory();
284 if (hist.getLength() > 0 && UrlUtils.URL_ABOUT_BLANK == hist.getUrl(0)) {
285 isFirstPageInFrameWindow = hist.getLength() <= 2;
286 }
287 else {
288 isFirstPageInFrameWindow = enclosingWindow.getHistory().getLength() < 2;
289 }
290 }
291
292 if (isFrameWindow && !isFirstPageInFrameWindow) {
293 executeEventHandlersIfNeeded(Event.TYPE_LOAD);
294 }
295
296 for (final BaseFrameElement frameElement : new ArrayList<>(frameElements_)) {
297 if (frameElement instanceof HtmlFrame) {
298 final Page page = frameElement.getEnclosedWindow().getEnclosedPage();
299 if (page != null && page.isHtmlPage()) {
300 ((HtmlPage) page).executeEventHandlersIfNeeded(Event.TYPE_LOAD);
301 }
302 }
303 }
304
305 if (!isFrameWindow) {
306 executeEventHandlersIfNeeded(Event.TYPE_LOAD);
307
308 if (!isAboutBlank && enclosingWindow.getWebClient().isJavaScriptEnabled()
309 && hasFeature(EVENT_FOCUS_ON_LOAD)) {
310 final HtmlElement body = getBody();
311 if (body != null) {
312 final Event event = new Event((Window) enclosingWindow.getScriptableObject(), Event.TYPE_FOCUS);
313 body.fireEvent(event);
314 }
315 }
316 }
317
318 try {
319 while (!afterLoadActions_.isEmpty()) {
320 final PostponedAction action = afterLoadActions_.remove(0);
321 action.execute();
322 }
323 }
324 catch (final IOException e) {
325 throw e;
326 }
327 catch (final Exception e) {
328 throw new RuntimeException(e);
329 }
330 executeRefreshIfNeeded();
331 }
332
333
334
335
336
337 void addAfterLoadAction(final PostponedAction action) {
338 afterLoadActions_.add(action);
339 }
340
341
342
343
344 @Override
345 public void cleanUp() {
346
347 if (cleaning_) {
348 return;
349 }
350
351 cleaning_ = true;
352 try {
353 super.cleanUp();
354 executeEventHandlersIfNeeded(Event.TYPE_UNLOAD);
355 deregisterFramesIfNeeded();
356 }
357 finally {
358 cleaning_ = false;
359
360 if (autoCloseableList_ != null) {
361 for (final AutoCloseable closeable : new ArrayList<>(autoCloseableList_)) {
362 try {
363 closeable.close();
364 }
365 catch (final Exception e) {
366 LOG.error("Closing the autoclosable " + closeable + " failed", e);
367 }
368 }
369 }
370 }
371 }
372
373
374
375
376 @Override
377 public HtmlElement getDocumentElement() {
378 return (HtmlElement) super.getDocumentElement();
379 }
380
381
382
383
384 public HtmlBody getBody() {
385 final DomElement doc = getDocumentElement();
386 if (doc != null) {
387 for (final DomNode node : doc.getChildren()) {
388 if (node instanceof HtmlBody) {
389 return (HtmlBody) node;
390 }
391 }
392 }
393 return null;
394 }
395
396
397
398
399
400 public HtmlElement getHead() {
401 final DomElement doc = getDocumentElement();
402 if (doc != null) {
403 for (final DomNode node : doc.getChildren()) {
404 if (node instanceof HtmlHead) {
405 return (HtmlElement) node;
406 }
407 }
408 }
409 return null;
410 }
411
412
413
414
415 @Override
416 public Document getOwnerDocument() {
417 return null;
418 }
419
420
421
422
423
424 @Override
425 public org.w3c.dom.Node importNode(final org.w3c.dom.Node importedNode, final boolean deep) {
426 throw new UnsupportedOperationException("HtmlPage.importNode is not yet implemented.");
427 }
428
429
430
431
432
433 @Override
434 public String getInputEncoding() {
435 throw new UnsupportedOperationException("HtmlPage.getInputEncoding is not yet implemented.");
436 }
437
438
439
440
441 @Override
442 public String getXmlEncoding() {
443 return null;
444 }
445
446
447
448
449 @Override
450 public boolean getXmlStandalone() {
451 return false;
452 }
453
454
455
456
457
458 @Override
459 public void setXmlStandalone(final boolean xmlStandalone) throws DOMException {
460 throw new UnsupportedOperationException("HtmlPage.setXmlStandalone is not yet implemented.");
461 }
462
463
464
465
466 @Override
467 public String getXmlVersion() {
468 return null;
469 }
470
471
472
473
474
475 @Override
476 public void setXmlVersion(final String xmlVersion) throws DOMException {
477 throw new UnsupportedOperationException("HtmlPage.setXmlVersion is not yet implemented.");
478 }
479
480
481
482
483
484 @Override
485 public boolean getStrictErrorChecking() {
486 throw new UnsupportedOperationException("HtmlPage.getStrictErrorChecking is not yet implemented.");
487 }
488
489
490
491
492
493 @Override
494 public void setStrictErrorChecking(final boolean strictErrorChecking) {
495 throw new UnsupportedOperationException("HtmlPage.setStrictErrorChecking is not yet implemented.");
496 }
497
498
499
500
501
502 @Override
503 public String getDocumentURI() {
504 throw new UnsupportedOperationException("HtmlPage.getDocumentURI is not yet implemented.");
505 }
506
507
508
509
510
511 @Override
512 public void setDocumentURI(final String documentURI) {
513 throw new UnsupportedOperationException("HtmlPage.setDocumentURI is not yet implemented.");
514 }
515
516
517
518
519
520 @Override
521 public org.w3c.dom.Node adoptNode(final org.w3c.dom.Node source) throws DOMException {
522 throw new UnsupportedOperationException("HtmlPage.adoptNode is not yet implemented.");
523 }
524
525
526
527
528
529 @Override
530 public DOMConfiguration getDomConfig() {
531 throw new UnsupportedOperationException("HtmlPage.getDomConfig is not yet implemented.");
532 }
533
534
535
536
537
538 @Override
539 public org.w3c.dom.Node renameNode(final org.w3c.dom.Node newNode, final String namespaceURI,
540 final String qualifiedName) throws DOMException {
541 throw new UnsupportedOperationException("HtmlPage.renameNode is not yet implemented.");
542 }
543
544
545
546
547 @Override
548 public Charset getCharset() {
549 if (originalCharset_ == null) {
550 originalCharset_ = getWebResponse().getContentCharset();
551 }
552 return originalCharset_;
553 }
554
555
556
557
558 @Override
559 public String getContentType() {
560 return getWebResponse().getContentType();
561 }
562
563
564
565
566
567 @Override
568 public DOMImplementation getImplementation() {
569 throw new UnsupportedOperationException("HtmlPage.getImplementation is not yet implemented.");
570 }
571
572
573
574
575
576 @Override
577 public DomElement createElement(String tagName) {
578 if (tagName.indexOf(':') == -1) {
579 tagName = org.htmlunit.util.StringUtils.toRootLowerCase(tagName);
580 }
581 return getWebClient().getPageCreator().getHtmlParser().getFactory(tagName)
582 .createElementNS(this, null, tagName, null);
583 }
584
585
586
587
588 @Override
589 public DomElement createElementNS(final String namespaceURI, final String qualifiedName) {
590 return getWebClient().getPageCreator().getHtmlParser()
591 .getElementFactory(this, namespaceURI, qualifiedName, false, true)
592 .createElementNS(this, namespaceURI, qualifiedName, null);
593 }
594
595
596
597
598
599 @Override
600 public Attr createAttributeNS(final String namespaceURI, final String qualifiedName) {
601 throw new UnsupportedOperationException("HtmlPage.createAttributeNS is not yet implemented.");
602 }
603
604
605
606
607
608 @Override
609 public EntityReference createEntityReference(final String id) {
610 throw new UnsupportedOperationException("HtmlPage.createEntityReference is not yet implemented.");
611 }
612
613
614
615
616
617 @Override
618 public ProcessingInstruction createProcessingInstruction(final String namespaceURI, final String qualifiedName) {
619 throw new UnsupportedOperationException("HtmlPage.createProcessingInstruction is not yet implemented.");
620 }
621
622
623
624
625 @Override
626 public DomElement getElementById(final String elementId) {
627 if (elementId != null) {
628 final MappedElementIndexEntry elements = idMap_.get(elementId);
629 if (elements != null) {
630 return elements.first();
631 }
632 }
633 return null;
634 }
635
636
637
638
639
640
641
642
643 public HtmlAnchor getAnchorByName(final String name) throws ElementNotFoundException {
644 return getDocumentElement().getOneHtmlElementByAttribute("a", DomElement.NAME_ATTRIBUTE, name);
645 }
646
647
648
649
650
651
652
653
654 public HtmlAnchor getAnchorByHref(final String href) throws ElementNotFoundException {
655 return getDocumentElement().getOneHtmlElementByAttribute("a", "href", href);
656 }
657
658
659
660
661
662 public List<HtmlAnchor> getAnchors() {
663 return getDocumentElement().getElementsByTagNameImpl("a");
664 }
665
666
667
668
669
670
671
672 public HtmlAnchor getAnchorByText(final String text) throws ElementNotFoundException {
673 WebAssert.notNull("text", text);
674
675 for (final HtmlAnchor anchor : getAnchors()) {
676 if (text.equals(anchor.asNormalizedText())) {
677 return anchor;
678 }
679 }
680 throw new ElementNotFoundException("a", "<text>", text);
681 }
682
683
684
685
686
687
688
689 public HtmlForm getFormByName(final String name) throws ElementNotFoundException {
690 final List<HtmlForm> forms = getDocumentElement()
691 .getElementsByAttribute("form", DomElement.NAME_ATTRIBUTE, name);
692 if (forms.isEmpty()) {
693 throw new ElementNotFoundException("form", DomElement.NAME_ATTRIBUTE, name);
694 }
695 return forms.get(0);
696 }
697
698
699
700
701
702 public List<HtmlForm> getForms() {
703 return getDocumentElement().getElementsByTagNameImpl("form");
704 }
705
706
707
708
709
710
711
712
713
714 public URL getFullyQualifiedUrl(String relativeUrl) throws MalformedURLException {
715
716 boolean incorrectnessNotified = false;
717 while (relativeUrl.startsWith("http:") && !relativeUrl.startsWith("http://")) {
718 if (!incorrectnessNotified) {
719 notifyIncorrectness("Incorrect URL \"" + relativeUrl + "\" has been corrected");
720 incorrectnessNotified = true;
721 }
722 relativeUrl = "http:/" + relativeUrl.substring(5);
723 }
724
725 return WebClient.expandUrl(getBaseURL(), relativeUrl);
726 }
727
728
729
730
731
732
733
734 public String getResolvedTarget(final String elementTarget) {
735 final String resolvedTarget;
736 if (base_ == null) {
737 resolvedTarget = elementTarget;
738 }
739 else if (elementTarget != null && !elementTarget.isEmpty()) {
740 resolvedTarget = elementTarget;
741 }
742 else {
743 resolvedTarget = base_.getTargetAttribute();
744 }
745 return resolvedTarget;
746 }
747
748
749
750
751
752
753
754 public List<String> getTabbableElementIds() {
755 final List<String> list = new ArrayList<>();
756
757 for (final HtmlElement element : getTabbableElements()) {
758 list.add(element.getId());
759 }
760
761 return Collections.unmodifiableList(list);
762 }
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791 public List<HtmlElement> getTabbableElements() {
792 final List<HtmlElement> tabbableElements = new ArrayList<>();
793 for (final HtmlElement element : getHtmlElementDescendants()) {
794 final String tagName = element.getTagName();
795 if (TABBABLE_TAGS.contains(tagName)) {
796 final boolean disabled = element.isDisabledElementAndDisabled();
797 if (!disabled && !HtmlElement.TAB_INDEX_OUT_OF_BOUNDS.equals(element.getTabIndex())) {
798 tabbableElements.add(element);
799 }
800 }
801 }
802 tabbableElements.sort(createTabOrderComparator());
803 return Collections.unmodifiableList(tabbableElements);
804 }
805
806 private static Comparator<HtmlElement> createTabOrderComparator() {
807 return (element1, element2) -> {
808 final Short i1 = element1.getTabIndex();
809 final Short i2 = element2.getTabIndex();
810
811 final short index1;
812 if (i1 == null) {
813 index1 = -1;
814 }
815 else {
816 index1 = i1.shortValue();
817 }
818
819 final short index2;
820 if (i2 == null) {
821 index2 = -1;
822 }
823 else {
824 index2 = i2.shortValue();
825 }
826
827 final int result;
828 if (index1 > 0 && index2 > 0) {
829 result = index1 - index2;
830 }
831 else if (index1 > 0) {
832 result = -1;
833 }
834 else if (index2 > 0) {
835 result = 1;
836 }
837 else if (index1 == index2) {
838 result = 0;
839 }
840 else {
841 result = index2 - index1;
842 }
843
844 return result;
845 };
846 }
847
848
849
850
851
852
853
854
855
856
857
858
859
860 public HtmlElement getHtmlElementByAccessKey(final char accessKey) {
861 final List<HtmlElement> elements = getHtmlElementsByAccessKey(accessKey);
862 if (elements.isEmpty()) {
863 return null;
864 }
865 return elements.get(0);
866 }
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885 public List<HtmlElement> getHtmlElementsByAccessKey(final char accessKey) {
886 final List<HtmlElement> elements = new ArrayList<>();
887
888 final String searchString = Character.toString(accessKey).toLowerCase(Locale.ROOT);
889 for (final HtmlElement element : getHtmlElementDescendants()) {
890 if (ACCEPTABLE_TAG_NAMES.contains(element.getTagName())) {
891 final String accessKeyAttribute = element.getAttributeDirect("accesskey");
892 if (searchString.equalsIgnoreCase(accessKeyAttribute)) {
893 elements.add(element);
894 }
895 }
896 }
897
898 return elements;
899 }
900
901
902
903
904
905
906
907
908
909
910
911 public ScriptResult executeJavaScript(final String sourceCode) {
912 return executeJavaScript(sourceCode, "injected script", 1);
913 }
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935 public ScriptResult executeJavaScript(String sourceCode, final String sourceName, final int startLine) {
936 if (!getWebClient().isJavaScriptEnabled()) {
937 return new ScriptResult(JavaScriptEngine.UNDEFINED);
938 }
939
940 if (StringUtils.startsWithIgnoreCase(sourceCode, JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
941 sourceCode = sourceCode.substring(JavaScriptURLConnection.JAVASCRIPT_PREFIX.length()).trim();
942 if (sourceCode.startsWith("return ")) {
943 sourceCode = sourceCode.substring("return ".length());
944 }
945 }
946
947 final Object result = getWebClient().getJavaScriptEngine()
948 .execute(this, getEnclosingWindow().getScriptableObject(), sourceCode, sourceName, startLine);
949 return new ScriptResult(result);
950 }
951
952
953 enum JavaScriptLoadResult {
954
955 NOOP,
956
957 NO_CONTENT,
958
959 SUCCESS,
960
961 DOWNLOAD_ERROR,
962
963 COMPILATION_ERROR
964 }
965
966
967
968
969
970
971
972
973
974
975
976 JavaScriptLoadResult loadExternalJavaScriptFile(final String srcAttribute, final Charset scriptCharset)
977 throws FailingHttpStatusCodeException {
978
979 final WebClient client = getWebClient();
980 if (StringUtils.isBlank(srcAttribute) || !client.isJavaScriptEnabled()) {
981 return JavaScriptLoadResult.NOOP;
982 }
983
984 final URL scriptURL;
985 try {
986 scriptURL = getFullyQualifiedUrl(srcAttribute);
987 final String protocol = scriptURL.getProtocol();
988 if ("javascript".equals(protocol)) {
989 if (LOG.isInfoEnabled()) {
990 LOG.info("Ignoring script src [" + srcAttribute + "]");
991 }
992 return JavaScriptLoadResult.NOOP;
993 }
994 if (!"http".equals(protocol) && !"https".equals(protocol)
995 && !"data".equals(protocol) && !"file".equals(protocol)) {
996 client.getJavaScriptErrorListener().malformedScriptURL(this, srcAttribute,
997 new MalformedURLException("unknown protocol: '" + protocol + "'"));
998 return JavaScriptLoadResult.NOOP;
999 }
1000 }
1001 catch (final MalformedURLException e) {
1002 client.getJavaScriptErrorListener().malformedScriptURL(this, srcAttribute, e);
1003 return JavaScriptLoadResult.NOOP;
1004 }
1005
1006 final Object script;
1007 try {
1008 script = loadJavaScriptFromUrl(scriptURL, scriptCharset);
1009 }
1010 catch (final IOException e) {
1011 client.getJavaScriptErrorListener().loadScriptError(this, scriptURL, e);
1012 return JavaScriptLoadResult.DOWNLOAD_ERROR;
1013 }
1014 catch (final FailingHttpStatusCodeException e) {
1015 if (e.getStatusCode() == HttpStatus.NO_CONTENT_204) {
1016 return JavaScriptLoadResult.NO_CONTENT;
1017 }
1018 client.getJavaScriptErrorListener().loadScriptError(this, scriptURL, e);
1019 throw e;
1020 }
1021
1022 if (script == null) {
1023 return JavaScriptLoadResult.COMPILATION_ERROR;
1024 }
1025
1026 @SuppressWarnings("unchecked")
1027 final AbstractJavaScriptEngine<Object> engine = (AbstractJavaScriptEngine<Object>) client.getJavaScriptEngine();
1028 engine.execute(this, getEnclosingWindow().getScriptableObject(), script);
1029 return JavaScriptLoadResult.SUCCESS;
1030 }
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044 private Object loadJavaScriptFromUrl(final URL url, final Charset scriptCharset) throws IOException,
1045 FailingHttpStatusCodeException {
1046
1047 final WebRequest referringRequest = getWebResponse().getWebRequest();
1048
1049 final WebClient client = getWebClient();
1050 final WebRequest request = new WebRequest(url);
1051
1052 request.setAdditionalHeaders(new HashMap<>(referringRequest.getAdditionalHeaders()));
1053
1054
1055 request.setAdditionalHeader(HttpHeader.ACCEPT, client.getBrowserVersion().getScriptAcceptHeader());
1056 request.setAdditionalHeader(HttpHeader.SEC_FETCH_SITE, "same-origin");
1057 request.setAdditionalHeader(HttpHeader.SEC_FETCH_MODE, "no-cors");
1058 request.setAdditionalHeader(HttpHeader.SEC_FETCH_DEST, "script");
1059
1060 request.setRefererHeader(referringRequest.getUrl());
1061 request.setCharset(scriptCharset);
1062
1063
1064
1065 if (scriptCharset != null) {
1066 request.setDefaultResponseContentCharset(scriptCharset);
1067 }
1068 else {
1069 request.setDefaultResponseContentCharset(StandardCharsets.UTF_8);
1070 }
1071
1072
1073
1074
1075 final WebResponse response = client.loadWebResponse(request);
1076
1077
1078
1079 final Cache cache = client.getCache();
1080 final Object cachedScript = cache.getCachedObject(request);
1081 if (cachedScript instanceof Script) {
1082 return cachedScript;
1083 }
1084
1085 client.printContentIfNecessary(response);
1086 client.throwFailingHttpStatusCodeExceptionIfNecessary(response);
1087
1088 final int statusCode = response.getStatusCode();
1089 if (statusCode == HttpStatus.NO_CONTENT_204) {
1090 throw new FailingHttpStatusCodeException(response);
1091 }
1092
1093 if (!response.isSuccess()) {
1094 throw new IOException("Unable to download JavaScript from '" + url + "' (status " + statusCode + ").");
1095 }
1096
1097 final String contentType = response.getContentType();
1098 if (contentType != null) {
1099 if (MimeType.isObsoleteJavascriptMimeType(contentType)) {
1100 getWebClient().getIncorrectnessListener().notify(
1101 "Obsolete content type encountered: '" + contentType + "' "
1102 + "for remotely loaded JavaScript element at '" + url + "'.", this);
1103 }
1104 else if (!MimeType.isJavascriptMimeType(contentType)) {
1105 getWebClient().getIncorrectnessListener().notify(
1106 "Expect content type of '" + MimeType.TEXT_JAVASCRIPT + "' "
1107 + "for remotely loaded JavaScript element at '" + url + "', "
1108 + "but got '" + contentType + "'.", this);
1109 }
1110 }
1111
1112 final Charset scriptEncoding = response.getContentCharset();
1113 final String scriptCode = response.getContentAsString(scriptEncoding);
1114 if (null != scriptCode) {
1115 final AbstractJavaScriptEngine<?> javaScriptEngine = client.getJavaScriptEngine();
1116 final Scriptable scope = getEnclosingWindow().getScriptableObject();
1117 final Object script = javaScriptEngine.compile(this, scope, scriptCode, url.toExternalForm(), 1);
1118 if (script != null && cache.cacheIfPossible(request, response, script)) {
1119
1120 return script;
1121 }
1122
1123 response.cleanUp();
1124 return script;
1125 }
1126
1127 response.cleanUp();
1128 return null;
1129 }
1130
1131
1132
1133
1134
1135
1136 public String getTitleText() {
1137 final HtmlTitle titleElement = getTitleElement();
1138 if (titleElement != null) {
1139 return titleElement.asNormalizedText();
1140 }
1141 return "";
1142 }
1143
1144
1145
1146
1147
1148
1149 public void setTitleText(final String message) {
1150 HtmlTitle titleElement = getTitleElement();
1151 if (titleElement == null) {
1152 LOG.debug("No title element, creating one");
1153 final HtmlHead head = (HtmlHead) getFirstChildElement(getDocumentElement(), HtmlHead.class);
1154 if (head == null) {
1155
1156 throw new IllegalStateException("Headelement was not defined for this page");
1157 }
1158 final Map<String, DomAttr> emptyMap = Collections.emptyMap();
1159 titleElement = new HtmlTitle(HtmlTitle.TAG_NAME, this, emptyMap);
1160 if (head.getFirstChild() != null) {
1161 head.getFirstChild().insertBefore(titleElement);
1162 }
1163 else {
1164 head.appendChild(titleElement);
1165 }
1166 }
1167
1168 titleElement.setNodeValue(message);
1169 }
1170
1171
1172
1173
1174
1175
1176
1177 private static DomElement getFirstChildElement(final DomElement startElement, final Class<?> clazz) {
1178 if (startElement == null) {
1179 return null;
1180 }
1181 for (final DomElement element : startElement.getChildElements()) {
1182 if (clazz.isInstance(element)) {
1183 return element;
1184 }
1185 }
1186
1187 return null;
1188 }
1189
1190
1191
1192
1193
1194
1195
1196 private DomElement getFirstChildElementRecursive(final DomElement startElement, final Class<?> clazz) {
1197 if (startElement == null) {
1198 return null;
1199 }
1200 for (final DomElement element : startElement.getChildElements()) {
1201 if (clazz.isInstance(element)) {
1202 return element;
1203 }
1204 final DomElement childFound = getFirstChildElementRecursive(element, clazz);
1205 if (childFound != null) {
1206 return childFound;
1207 }
1208 }
1209
1210 return null;
1211 }
1212
1213
1214
1215
1216
1217
1218 private HtmlTitle getTitleElement() {
1219 return (HtmlTitle) getFirstChildElementRecursive(getDocumentElement(), HtmlTitle.class);
1220 }
1221
1222
1223
1224
1225
1226
1227 private boolean executeEventHandlersIfNeeded(final String eventType) {
1228
1229 if (!getWebClient().isJavaScriptEnabled()) {
1230 return true;
1231 }
1232
1233
1234 final WebWindow window = getEnclosingWindow();
1235 if (window.getScriptableObject() instanceof Window) {
1236 final Event event;
1237 if (Event.TYPE_BEFORE_UNLOAD.equals(eventType)) {
1238 event = new BeforeUnloadEvent(this, eventType);
1239 }
1240 else {
1241 event = new Event(this, eventType);
1242 }
1243
1244
1245
1246 if (LOG.isDebugEnabled()) {
1247 LOG.debug("Firing " + event);
1248 }
1249
1250 final EventTarget jsNode;
1251 if (Event.TYPE_DOM_DOCUMENT_LOADED.equals(eventType)) {
1252 jsNode = getScriptableObject();
1253 }
1254 else if (Event.TYPE_READY_STATE_CHANGE.equals(eventType)) {
1255 jsNode = getDocumentElement().getScriptableObject();
1256 }
1257 else {
1258
1259 jsNode = window.getScriptableObject();
1260 }
1261
1262 ((JavaScriptEngine) getWebClient().getJavaScriptEngine()).callSecured(cx -> jsNode.fireEvent(event), this);
1263
1264 if (!isOnbeforeunloadAccepted(this, event)) {
1265 return false;
1266 }
1267 }
1268
1269
1270 if (window instanceof FrameWindow) {
1271 final FrameWindow fw = (FrameWindow) window;
1272 final BaseFrameElement frame = fw.getFrameElement();
1273
1274
1275 if (Event.TYPE_LOAD.equals(eventType) && frame.getParentNode() instanceof DomDocumentFragment) {
1276 return true;
1277 }
1278
1279 if (frame.hasEventHandlers("on" + eventType)) {
1280 if (LOG.isDebugEnabled()) {
1281 LOG.debug("Executing on" + eventType + " handler for " + frame);
1282 }
1283 if (window.getScriptableObject() instanceof Window) {
1284 final Event event;
1285 if (Event.TYPE_BEFORE_UNLOAD.equals(eventType)) {
1286 event = new BeforeUnloadEvent(frame, eventType);
1287 }
1288 else {
1289 event = new Event(frame, eventType);
1290 }
1291
1292
1293
1294
1295 frame.fireEvent(event);
1296
1297 if (!isOnbeforeunloadAccepted((HtmlPage) frame.getPage(), event)) {
1298 return false;
1299 }
1300 }
1301 }
1302 }
1303
1304 return true;
1305 }
1306
1307
1308
1309
1310
1311
1312 public boolean isOnbeforeunloadAccepted() {
1313 return executeEventHandlersIfNeeded(Event.TYPE_BEFORE_UNLOAD);
1314 }
1315
1316 private boolean isOnbeforeunloadAccepted(final HtmlPage page, final Event event) {
1317 if (event instanceof BeforeUnloadEvent) {
1318 final BeforeUnloadEvent beforeUnloadEvent = (BeforeUnloadEvent) event;
1319 if (beforeUnloadEvent.isBeforeUnloadMessageSet()) {
1320 final OnbeforeunloadHandler handler = getWebClient().getOnbeforeunloadHandler();
1321 if (handler == null) {
1322 LOG.warn("document.onbeforeunload() returned a string in event.returnValue,"
1323 + " but no onbeforeunload handler installed.");
1324 }
1325 else {
1326 final String message = JavaScriptEngine.toString(beforeUnloadEvent.getReturnValue());
1327 return handler.handleEvent(page, message);
1328 }
1329 }
1330 }
1331 return true;
1332 }
1333
1334
1335
1336
1337
1338
1339 private void executeRefreshIfNeeded() throws IOException {
1340
1341
1342
1343 final WebWindow window = getEnclosingWindow();
1344 if (window == null) {
1345 return;
1346 }
1347
1348 final String refreshString = getRefreshStringOrNull();
1349 if (refreshString == null || refreshString.isEmpty()) {
1350 return;
1351 }
1352
1353 final double time;
1354 final URL url;
1355
1356 int index = StringUtils.indexOfAnyBut(refreshString, "0123456789");
1357 final boolean timeOnly = index == -1;
1358
1359 if (timeOnly) {
1360
1361 try {
1362 time = Double.parseDouble(refreshString);
1363 }
1364 catch (final NumberFormatException e) {
1365 if (LOG.isErrorEnabled()) {
1366 LOG.error("Malformed refresh string (no ';' but not a number): " + refreshString, e);
1367 }
1368 return;
1369 }
1370 url = getUrl();
1371 }
1372 else {
1373
1374 try {
1375 time = Double.parseDouble(refreshString.substring(0, index).trim());
1376 }
1377 catch (final NumberFormatException e) {
1378 if (LOG.isErrorEnabled()) {
1379 LOG.error("Malformed refresh string (no valid number before ';') " + refreshString, e);
1380 }
1381 return;
1382 }
1383 index = refreshString.toLowerCase(Locale.ROOT).indexOf("url=", index);
1384 if (index == -1) {
1385 if (LOG.isErrorEnabled()) {
1386 LOG.error("Malformed refresh string (found ';' but no 'url='): " + refreshString);
1387 }
1388 return;
1389 }
1390 final StringBuilder builder = new StringBuilder(refreshString.substring(index + 4));
1391 if (StringUtils.isBlank(builder.toString())) {
1392
1393 url = getUrl();
1394 }
1395 else {
1396 if (builder.charAt(0) == '"' || builder.charAt(0) == 0x27) {
1397 builder.deleteCharAt(0);
1398 }
1399 if (builder.charAt(builder.length() - 1) == '"' || builder.charAt(builder.length() - 1) == 0x27) {
1400 builder.deleteCharAt(builder.length() - 1);
1401 }
1402 final String urlString = builder.toString();
1403 try {
1404 url = getFullyQualifiedUrl(urlString);
1405 }
1406 catch (final MalformedURLException e) {
1407 if (LOG.isErrorEnabled()) {
1408 LOG.error("Malformed URL in refresh string: " + refreshString, e);
1409 }
1410 throw e;
1411 }
1412 }
1413 }
1414
1415 final int timeRounded = (int) time;
1416 checkRecursion();
1417 getWebClient().getRefreshHandler().handleRefresh(this, url, timeRounded);
1418 }
1419
1420 private void checkRecursion() {
1421 final StackTraceElement[] elements = new Exception().getStackTrace();
1422 if (elements.length > 500) {
1423 for (int i = 0; i < 500; i++) {
1424 if (!elements[i].getClassName().startsWith("org.htmlunit.")) {
1425 return;
1426 }
1427 }
1428 final WebResponse webResponse = getWebResponse();
1429 throw new FailingHttpStatusCodeException("Too much redirect for "
1430 + webResponse.getWebRequest().getUrl(), webResponse);
1431 }
1432 }
1433
1434
1435
1436
1437
1438
1439 private String getRefreshStringOrNull() {
1440 final List<HtmlMeta> metaTags = getMetaTags("refresh");
1441 if (!metaTags.isEmpty()) {
1442 return metaTags.get(0).getContentAttribute().trim();
1443 }
1444 return getWebResponse().getResponseHeaderValue("Refresh");
1445 }
1446
1447
1448
1449
1450 private void executeDeferredScriptsIfNeeded() {
1451 if (!getWebClient().isJavaScriptEnabled()) {
1452 return;
1453 }
1454 final DomElement doc = getDocumentElement();
1455 final List<HtmlScript> scripts = new ArrayList<>();
1456
1457
1458 for (final HtmlElement elem : doc.getHtmlElementDescendants()) {
1459 if ("script".equals(elem.getLocalName()) && (elem instanceof HtmlScript)) {
1460 final HtmlScript script = (HtmlScript) elem;
1461 if (script.isDeferred() && ATTRIBUTE_NOT_DEFINED != script.getSrcAttribute()) {
1462 scripts.add(script);
1463 }
1464 }
1465 }
1466 for (final HtmlScript script : scripts) {
1467 ScriptElementSupport.executeScriptIfNeeded(script, true, true);
1468 }
1469 }
1470
1471
1472
1473
1474 public void deregisterFramesIfNeeded() {
1475 for (final BaseFrameElement frameElement : frameElements_) {
1476 final WebWindow window = frameElement.getEnclosedWindow();
1477 getWebClient().deregisterWebWindow(window);
1478 final Page page = window.getEnclosedPage();
1479 if (page != null && page.isHtmlPage()) {
1480
1481
1482 ((HtmlPage) page).deregisterFramesIfNeeded();
1483 }
1484 }
1485 }
1486
1487
1488
1489
1490
1491
1492 public List<FrameWindow> getFrames() {
1493 final List<BaseFrameElement> frameElements = new ArrayList<>(frameElements_);
1494 Collections.sort(frameElements, DOCUMENT_POSITION_COMPERATOR);
1495
1496 final List<FrameWindow> list = new ArrayList<>(frameElements.size());
1497 for (final BaseFrameElement frameElement : frameElements) {
1498 list.add(frameElement.getEnclosedWindow());
1499 }
1500 return list;
1501 }
1502
1503
1504
1505
1506
1507
1508
1509 public FrameWindow getFrameByName(final String name) throws ElementNotFoundException {
1510 for (final BaseFrameElement frameElement : frameElements_) {
1511 final FrameWindow fw = frameElement.getEnclosedWindow();
1512 if (fw.getName().equals(name)) {
1513 return fw;
1514 }
1515 }
1516
1517 throw new ElementNotFoundException("frame or iframe", DomElement.NAME_ATTRIBUTE, name);
1518 }
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530 public DomElement pressAccessKey(final char accessKey) throws IOException {
1531 final HtmlElement element = getHtmlElementByAccessKey(accessKey);
1532 if (element != null) {
1533 element.focus();
1534 if (element instanceof HtmlAnchor
1535 || element instanceof HtmlArea
1536 || element instanceof HtmlButton
1537 || element instanceof HtmlInput
1538 || element instanceof HtmlLabel
1539 || element instanceof HtmlLegend
1540 || element instanceof HtmlTextArea) {
1541 final Page newPage = element.click();
1542
1543 if (newPage != this && getFocusedElement() == element) {
1544
1545 getFocusedElement().blur();
1546 }
1547 }
1548 }
1549
1550 return getFocusedElement();
1551 }
1552
1553
1554
1555
1556
1557
1558
1559 public HtmlElement tabToNextElement() {
1560 final List<HtmlElement> elements = getTabbableElements();
1561 if (elements.isEmpty()) {
1562 setFocusedElement(null);
1563 return null;
1564 }
1565
1566 final HtmlElement elementToGiveFocus;
1567 final DomElement elementWithFocus = getFocusedElement();
1568 if (elementWithFocus == null) {
1569 elementToGiveFocus = elements.get(0);
1570 }
1571 else {
1572 final int index = elements.indexOf(elementWithFocus);
1573 if (index == -1) {
1574
1575 elementToGiveFocus = elements.get(0);
1576 }
1577 else {
1578 if (index == elements.size() - 1) {
1579 elementToGiveFocus = elements.get(0);
1580 }
1581 else {
1582 elementToGiveFocus = elements.get(index + 1);
1583 }
1584 }
1585 }
1586
1587 setFocusedElement(elementToGiveFocus);
1588 return elementToGiveFocus;
1589 }
1590
1591
1592
1593
1594
1595
1596
1597 public HtmlElement tabToPreviousElement() {
1598 final List<HtmlElement> elements = getTabbableElements();
1599 if (elements.isEmpty()) {
1600 setFocusedElement(null);
1601 return null;
1602 }
1603
1604 final HtmlElement elementToGiveFocus;
1605 final DomElement elementWithFocus = getFocusedElement();
1606 if (elementWithFocus == null) {
1607 elementToGiveFocus = elements.get(elements.size() - 1);
1608 }
1609 else {
1610 final int index = elements.indexOf(elementWithFocus);
1611 if (index == -1) {
1612
1613 elementToGiveFocus = elements.get(elements.size() - 1);
1614 }
1615 else {
1616 if (index == 0) {
1617 elementToGiveFocus = elements.get(elements.size() - 1);
1618 }
1619 else {
1620 elementToGiveFocus = elements.get(index - 1);
1621 }
1622 }
1623 }
1624
1625 setFocusedElement(elementToGiveFocus);
1626 return elementToGiveFocus;
1627 }
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639 @SuppressWarnings("unchecked")
1640 public <E extends HtmlElement> E getHtmlElementById(final String elementId) throws ElementNotFoundException {
1641 final DomElement element = getElementById(elementId);
1642 if (element == null) {
1643 throw new ElementNotFoundException("*", DomElement.ID_ATTRIBUTE, elementId);
1644 }
1645 return (E) element;
1646 }
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656 public List<DomElement> getElementsById(final String elementId) {
1657 if (elementId != null) {
1658 final MappedElementIndexEntry elements = idMap_.get(elementId);
1659 if (elements != null) {
1660 return new ArrayList<>(elements.elements());
1661 }
1662 }
1663 return Collections.emptyList();
1664 }
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675 @SuppressWarnings("unchecked")
1676 public <E extends DomElement> E getElementByName(final String name) throws ElementNotFoundException {
1677 if (name != null) {
1678 final MappedElementIndexEntry elements = nameMap_.get(name);
1679 if (elements != null) {
1680 return (E) elements.first();
1681 }
1682 }
1683 throw new ElementNotFoundException("*", DomElement.NAME_ATTRIBUTE, name);
1684 }
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694 public List<DomElement> getElementsByName(final String name) {
1695 if (name != null) {
1696 final MappedElementIndexEntry elements = nameMap_.get(name);
1697 if (elements != null) {
1698 return new ArrayList<>(elements.elements());
1699 }
1700 }
1701 return Collections.emptyList();
1702 }
1703
1704
1705
1706
1707
1708
1709
1710
1711 public List<DomElement> getElementsByIdAndOrName(final String idAndOrName) {
1712 if (idAndOrName == null) {
1713 return Collections.emptyList();
1714 }
1715 final MappedElementIndexEntry list1 = idMap_.get(idAndOrName);
1716 final MappedElementIndexEntry list2 = nameMap_.get(idAndOrName);
1717 final List<DomElement> list = new ArrayList<>();
1718 if (list1 != null) {
1719 list.addAll(list1.elements());
1720 }
1721 if (list2 != null) {
1722 for (final DomElement elt : list2.elements()) {
1723 if (!list.contains(elt)) {
1724 list.add(elt);
1725 }
1726 }
1727 }
1728 return list;
1729 }
1730
1731
1732
1733
1734
1735
1736 void notifyNodeAdded(final DomNode node) {
1737 if (node instanceof DomElement) {
1738 addMappedElement((DomElement) node, true);
1739
1740 if (node instanceof BaseFrameElement) {
1741 frameElements_.add((BaseFrameElement) node);
1742 }
1743
1744 if (node.getFirstChild() != null) {
1745 for (final Iterator<HtmlElement> iterator = node.new DescendantHtmlElementsIterator();
1746 iterator.hasNext();) {
1747 final HtmlElement child = iterator.next();
1748 if (child instanceof BaseFrameElement) {
1749 frameElements_.add((BaseFrameElement) child);
1750 }
1751 }
1752 }
1753
1754 if ("base".equals(node.getNodeName())) {
1755 calculateBase();
1756 }
1757 }
1758 node.onAddedToPage();
1759 }
1760
1761
1762
1763
1764
1765
1766 void notifyNodeRemoved(final DomNode node) {
1767 if (node instanceof HtmlElement) {
1768 removeMappedElement((HtmlElement) node, true, true);
1769
1770 if (node instanceof BaseFrameElement) {
1771 frameElements_.remove(node);
1772 }
1773 for (final HtmlElement child : node.getHtmlElementDescendants()) {
1774 if (child instanceof BaseFrameElement) {
1775 frameElements_.remove(child);
1776 }
1777 }
1778
1779 if ("base".equals(node.getNodeName())) {
1780 calculateBase();
1781 }
1782 }
1783 }
1784
1785
1786
1787
1788
1789
1790 void addMappedElement(final DomElement element, final boolean recurse) {
1791 if (isAncestorOf(element)) {
1792 addElement(idMap_, element, DomElement.ID_ATTRIBUTE, recurse);
1793 addElement(nameMap_, element, DomElement.NAME_ATTRIBUTE, recurse);
1794 }
1795 }
1796
1797 private void addElement(final Map<String, MappedElementIndexEntry> map, final DomElement element,
1798 final String attribute, final boolean recurse) {
1799 final String value = element.getAttribute(attribute);
1800
1801 if (ATTRIBUTE_NOT_DEFINED != value) {
1802 MappedElementIndexEntry elements = map.get(value);
1803 if (elements == null) {
1804 elements = new MappedElementIndexEntry();
1805 elements.add(element);
1806 map.put(value, elements);
1807 }
1808 else {
1809 elements.add(element);
1810 }
1811 }
1812 if (recurse) {
1813
1814
1815 DomNode nextChild = element.getFirstChild();
1816 while (nextChild != null) {
1817 if (nextChild instanceof DomElement) {
1818 addElement(map, (DomElement) nextChild, attribute, true);
1819 }
1820 nextChild = nextChild.getNextSibling();
1821 }
1822 }
1823 }
1824
1825
1826
1827
1828
1829
1830
1831 void removeMappedElement(final DomElement element, final boolean recurse, final boolean descendant) {
1832 if (descendant || isAncestorOf(element)) {
1833 removeElement(idMap_, element, DomElement.ID_ATTRIBUTE, recurse);
1834 removeElement(nameMap_, element, DomElement.NAME_ATTRIBUTE, recurse);
1835 }
1836 }
1837
1838 private void removeElement(final Map<String, MappedElementIndexEntry> map, final DomElement element,
1839 final String attribute, final boolean recurse) {
1840 final String value = element.getAttribute(attribute);
1841
1842 if (ATTRIBUTE_NOT_DEFINED != value) {
1843 final MappedElementIndexEntry elements = map.remove(value);
1844 if (elements != null) {
1845 elements.remove(element);
1846 if (!elements.elements_.isEmpty()) {
1847 map.put(value, elements);
1848 }
1849 }
1850 }
1851 if (recurse) {
1852 for (final DomElement child : element.getChildElements()) {
1853 removeElement(map, child, attribute, true);
1854 }
1855 }
1856 }
1857
1858
1859
1860
1861
1862
1863
1864 static boolean isMappedElement(final Document document, final String attributeName) {
1865 return document instanceof HtmlPage
1866 && (DomElement.NAME_ATTRIBUTE.equals(attributeName) || DomElement.ID_ATTRIBUTE.equals(attributeName));
1867 }
1868
1869 private void calculateBase() {
1870 final List<HtmlElement> baseElements = getDocumentElement().getStaticElementsByTagName("base");
1871
1872 base_ = null;
1873 for (final HtmlElement baseElement : baseElements) {
1874 if (baseElement instanceof HtmlBase) {
1875 if (base_ != null) {
1876 notifyIncorrectness("Multiple 'base' detected, only the first is used.");
1877 break;
1878 }
1879 base_ = (HtmlBase) baseElement;
1880 }
1881 }
1882 }
1883
1884
1885
1886
1887
1888
1889
1890 void loadFrames() throws FailingHttpStatusCodeException {
1891 for (final BaseFrameElement frameElement : new ArrayList<>(frameElements_)) {
1892
1893
1894
1895 if (frameElement.getEnclosedWindow() != null
1896 && UrlUtils.URL_ABOUT_BLANK == frameElement.getEnclosedPage().getUrl()
1897 && !frameElement.isContentLoaded()) {
1898 frameElement.loadInnerPage();
1899 }
1900 }
1901 }
1902
1903
1904
1905
1906
1907 @Override
1908 public String toString() {
1909 final StringBuilder builder = new StringBuilder()
1910 .append("HtmlPage(")
1911 .append(getUrl())
1912 .append(")@")
1913 .append(hashCode());
1914 return builder.toString();
1915 }
1916
1917
1918
1919
1920
1921
1922 protected List<HtmlMeta> getMetaTags(final String httpEquiv) {
1923 if (getDocumentElement() == null) {
1924 return Collections.emptyList();
1925 }
1926 final List<HtmlMeta> tags = getDocumentElement().getStaticElementsByTagName("meta");
1927 final List<HtmlMeta> foundTags = new ArrayList<>();
1928 for (final HtmlMeta htmlMeta : tags) {
1929 if (httpEquiv.equalsIgnoreCase(htmlMeta.getHttpEquivAttribute())) {
1930 foundTags.add(htmlMeta);
1931 }
1932 }
1933 return foundTags;
1934 }
1935
1936
1937
1938
1939
1940
1941 @Override
1942 protected HtmlPage clone() {
1943 final HtmlPage result = (HtmlPage) super.clone();
1944 result.elementWithFocus_ = null;
1945
1946 result.idMap_ = new ConcurrentHashMap<>();
1947 result.nameMap_ = new ConcurrentHashMap<>();
1948
1949 return result;
1950 }
1951
1952
1953
1954
1955 @Override
1956 public HtmlPage cloneNode(final boolean deep) {
1957
1958 final HtmlPage result = (HtmlPage) super.cloneNode(false);
1959 if (getWebClient().isJavaScriptEnabled()) {
1960 final HtmlUnitScriptable jsObjClone = getScriptableObject().clone();
1961 jsObjClone.setDomNode(result);
1962 }
1963
1964
1965 if (deep) {
1966
1967
1968
1969 result.attributeListeners_ = null;
1970
1971 result.selectionRanges_ = new ArrayList<>(3);
1972
1973 result.afterLoadActions_ = Collections.synchronizedList(new ArrayList<>());
1974 result.frameElements_ = new ArrayList<>();
1975 for (DomNode child = getFirstChild(); child != null; child = child.getNextSibling()) {
1976 result.appendChild(child.cloneNode(true));
1977 }
1978 }
1979 return result;
1980 }
1981
1982
1983
1984
1985
1986
1987
1988
1989 public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1990 WebAssert.notNull("listener", listener);
1991 synchronized (lock_) {
1992 if (attributeListeners_ == null) {
1993 attributeListeners_ = new LinkedHashSet<>();
1994 }
1995 attributeListeners_.add(listener);
1996 }
1997 }
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007 public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
2008 WebAssert.notNull("listener", listener);
2009 synchronized (lock_) {
2010 if (attributeListeners_ != null) {
2011 attributeListeners_.remove(listener);
2012 }
2013 }
2014 }
2015
2016
2017
2018
2019
2020 void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
2021 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2022 if (listeners != null) {
2023 for (final HtmlAttributeChangeListener listener : listeners) {
2024 listener.attributeAdded(event);
2025 }
2026 }
2027 }
2028
2029
2030
2031
2032
2033 void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
2034 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2035 if (listeners != null) {
2036 for (final HtmlAttributeChangeListener listener : listeners) {
2037 listener.attributeReplaced(event);
2038 }
2039 }
2040 }
2041
2042
2043
2044
2045
2046 void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
2047 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2048 if (listeners != null) {
2049 for (final HtmlAttributeChangeListener listener : listeners) {
2050 listener.attributeRemoved(event);
2051 }
2052 }
2053 }
2054
2055 private List<HtmlAttributeChangeListener> safeGetAttributeListeners() {
2056 synchronized (lock_) {
2057 if (attributeListeners_ != null) {
2058 return new ArrayList<>(attributeListeners_);
2059 }
2060 return null;
2061 }
2062 }
2063
2064
2065
2066
2067 @Override
2068 protected void checkChildHierarchy(final org.w3c.dom.Node newChild) throws DOMException {
2069 if (newChild instanceof Element) {
2070 if (getDocumentElement() != null) {
2071 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2072 "The Document may only have a single child Element.");
2073 }
2074 }
2075 else if (newChild instanceof DocumentType) {
2076 if (getDoctype() != null) {
2077 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2078 "The Document may only have a single child DocumentType.");
2079 }
2080 }
2081 else if (!(newChild instanceof Comment || newChild instanceof ProcessingInstruction)) {
2082 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2083 "The Document may not have a child of this type: " + newChild.getNodeType());
2084 }
2085 super.checkChildHierarchy(newChild);
2086 }
2087
2088
2089
2090
2091
2092 public boolean isBeingParsed() {
2093 return parserCount_ > 0;
2094 }
2095
2096
2097
2098
2099
2100
2101 public void registerParsingStart() {
2102 parserCount_++;
2103 }
2104
2105
2106
2107
2108
2109
2110 public void registerParsingEnd() {
2111 parserCount_--;
2112 }
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126 public boolean isParsingHtmlSnippet() {
2127 return snippetParserCount_ > 0;
2128 }
2129
2130
2131
2132
2133
2134
2135 public void registerSnippetParsingStart() {
2136 snippetParserCount_++;
2137 }
2138
2139
2140
2141
2142
2143
2144 public void registerSnippetParsingEnd() {
2145 snippetParserCount_--;
2146 }
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158 public boolean isParsingInlineHtmlSnippet() {
2159 return inlineSnippetParserCount_ > 0;
2160 }
2161
2162
2163
2164
2165
2166
2167 public void registerInlineSnippetParsingStart() {
2168 inlineSnippetParserCount_++;
2169 }
2170
2171
2172
2173
2174
2175
2176 public void registerInlineSnippetParsingEnd() {
2177 inlineSnippetParserCount_--;
2178 }
2179
2180
2181
2182
2183
2184
2185 public Page refresh() throws IOException {
2186 return getWebClient().getPage(getWebResponse().getWebRequest());
2187 }
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197 public void writeInParsedStream(final String string) {
2198 getDOMBuilder().pushInputString(string);
2199 }
2200
2201
2202
2203
2204
2205
2206
2207 public void setDOMBuilder(final HTMLParserDOMBuilder htmlUnitDOMBuilder) {
2208 domBuilder_ = htmlUnitDOMBuilder;
2209 }
2210
2211
2212
2213
2214
2215
2216
2217 public HTMLParserDOMBuilder getDOMBuilder() {
2218 return domBuilder_;
2219 }
2220
2221
2222
2223
2224
2225
2226 public Map<String, String> getNamespaces() {
2227 final org.w3c.dom.NamedNodeMap attributes = getDocumentElement().getAttributes();
2228 final Map<String, String> namespaces = new HashMap<>();
2229 for (int i = 0; i < attributes.getLength(); i++) {
2230 final Attr attr = (Attr) attributes.item(i);
2231 String name = attr.getName();
2232 if (name.startsWith("xmlns")) {
2233 int startPos = 5;
2234 if (name.length() > 5 && name.charAt(5) == ':') {
2235 startPos = 6;
2236 }
2237 name = name.substring(startPos);
2238 namespaces.put(name, attr.getValue());
2239 }
2240 }
2241 return namespaces;
2242 }
2243
2244
2245
2246
2247 @Override
2248 public void setDocumentType(final DocumentType type) {
2249 super.setDocumentType(type);
2250 }
2251
2252
2253
2254
2255
2256
2257
2258
2259 public void save(final File file) throws IOException {
2260 new XmlSerializer().save(this, file);
2261 }
2262
2263
2264
2265
2266
2267 public boolean isQuirksMode() {
2268 return "BackCompat".equals(((HTMLDocument) getScriptableObject()).getCompatMode());
2269 }
2270
2271
2272
2273
2274
2275 @Override
2276 public boolean isAttachedToPage() {
2277 return true;
2278 }
2279
2280
2281
2282
2283 @Override
2284 public boolean isHtmlPage() {
2285 return true;
2286 }
2287
2288
2289
2290
2291
2292 public URL getBaseURL() {
2293 URL baseUrl;
2294 if (base_ == null) {
2295 baseUrl = getUrl();
2296 final WebWindow window = getEnclosingWindow();
2297 final boolean frame = window != null && window != window.getTopWindow();
2298 if (frame) {
2299 final boolean frameSrcIsNotSet = baseUrl == UrlUtils.URL_ABOUT_BLANK;
2300 final boolean frameSrcIsJs = "javascript".equals(baseUrl.getProtocol());
2301 if (frameSrcIsNotSet || frameSrcIsJs) {
2302 baseUrl = window.getTopWindow().getEnclosedPage().getWebResponse()
2303 .getWebRequest().getUrl();
2304 }
2305 }
2306 else if (baseUrl_ != null) {
2307 baseUrl = baseUrl_;
2308 }
2309 }
2310 else {
2311 final String href = base_.getHrefAttribute().trim();
2312 if (StringUtils.isEmpty(href)) {
2313 baseUrl = getUrl();
2314 }
2315 else {
2316 final URL url = getUrl();
2317 try {
2318 if (href.startsWith("http://") || href.startsWith("https://")) {
2319 baseUrl = new URL(href);
2320 }
2321 else if (href.startsWith("//")) {
2322 baseUrl = new URL(String.format("%s:%s", url.getProtocol(), href));
2323 }
2324 else if (href.length() > 0 && href.charAt(0) == '/') {
2325 final int port = Window.getPort(url);
2326 baseUrl = new URL(String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), port, href));
2327 }
2328 else if (url.toString().endsWith("/")) {
2329 baseUrl = new URL(String.format("%s%s", url, href));
2330 }
2331 else {
2332 baseUrl = new URL(UrlUtils.resolveUrl(url, href));
2333 }
2334 }
2335 catch (final MalformedURLException e) {
2336 notifyIncorrectness("Invalid base url: \"" + href + "\", ignoring it");
2337 baseUrl = url;
2338 }
2339 }
2340 }
2341
2342 return baseUrl;
2343 }
2344
2345
2346
2347
2348
2349
2350
2351 public void addAutoCloseable(final AutoCloseable autoCloseable) {
2352 if (autoCloseable == null) {
2353 return;
2354 }
2355
2356 if (autoCloseableList_ == null) {
2357 autoCloseableList_ = new ArrayList<>();
2358 }
2359 autoCloseableList_.add(autoCloseable);
2360 }
2361
2362
2363
2364
2365 @Override
2366 public boolean handles(final Event event) {
2367 if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
2368 return true;
2369 }
2370 return super.handles(event);
2371 }
2372
2373
2374
2375
2376
2377 public void setElementFromPointHandler(final ElementFromPointHandler elementFromPointHandler) {
2378 elementFromPointHandler_ = elementFromPointHandler;
2379 }
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390 public HtmlElement getElementFromPoint(final int x, final int y) {
2391 if (elementFromPointHandler_ == null) {
2392 if (LOG.isWarnEnabled()) {
2393 LOG.warn("ElementFromPointHandler was not specicifed for " + this);
2394 }
2395 if (x <= 0 || y <= 0) {
2396 return null;
2397 }
2398 return getBody();
2399 }
2400 return elementFromPointHandler_.getElementFromPoint(this, x, y);
2401 }
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411 public boolean setFocusedElement(final DomElement newElement) {
2412 return setFocusedElement(newElement, false);
2413 }
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424 public boolean setFocusedElement(final DomElement newElement, final boolean windowActivated) {
2425 if (elementWithFocus_ == newElement && !windowActivated) {
2426
2427 return true;
2428 }
2429
2430 final DomElement oldFocusedElement = elementWithFocus_;
2431 elementWithFocus_ = null;
2432
2433 if (!windowActivated) {
2434 if (oldFocusedElement != null) {
2435 oldFocusedElement.removeFocus();
2436 oldFocusedElement.fireEvent(Event.TYPE_BLUR);
2437
2438 oldFocusedElement.fireEvent(Event.TYPE_FOCUS_OUT);
2439 }
2440 }
2441
2442 elementWithFocus_ = newElement;
2443
2444
2445
2446 if (newElement != null) {
2447 newElement.focus();
2448 newElement.fireEvent(Event.TYPE_FOCUS);
2449
2450 newElement.fireEvent(Event.TYPE_FOCUS_IN);
2451 }
2452
2453
2454
2455 return this == getEnclosingWindow().getEnclosedPage();
2456 }
2457
2458
2459
2460
2461
2462
2463 public DomElement getFocusedElement() {
2464 return elementWithFocus_;
2465 }
2466
2467
2468
2469
2470
2471
2472
2473 public void setElementWithFocus(final DomElement elementWithFocus) {
2474 elementWithFocus_ = elementWithFocus;
2475 }
2476
2477
2478
2479
2480
2481
2482 public HtmlElement getActiveElement() {
2483 final DomElement activeElement = getFocusedElement();
2484 if (activeElement instanceof HtmlElement) {
2485 return (HtmlElement) activeElement;
2486 }
2487
2488 final HtmlElement body = getBody();
2489 if (body != null) {
2490 return body;
2491 }
2492 return null;
2493 }
2494
2495
2496
2497
2498
2499
2500
2501
2502 public List<SimpleRange> getSelectionRanges() {
2503 return selectionRanges_;
2504 }
2505
2506
2507
2508
2509
2510
2511
2512
2513 public void setSelectionRange(final SimpleRange selectionRange) {
2514 selectionRanges_.clear();
2515 selectionRanges_.add(selectionRange);
2516 }
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532 public ScriptResult executeJavaScriptFunction(final Object function, final Object thisObject,
2533 final Object[] args, final DomNode htmlElementScope) {
2534 if (!getWebClient().isJavaScriptEnabled()) {
2535 return new ScriptResult(null);
2536 }
2537
2538 return executeJavaScriptFunction((Function) function, (Scriptable) thisObject, args, htmlElementScope);
2539 }
2540
2541 private ScriptResult executeJavaScriptFunction(final Function function, final Scriptable thisObject,
2542 final Object[] args, final DomNode htmlElementScope) {
2543
2544 final JavaScriptEngine engine = (JavaScriptEngine) getWebClient().getJavaScriptEngine();
2545 final Object result = engine.callFunction(this, function, thisObject, args, htmlElementScope);
2546
2547 return new ScriptResult(result);
2548 }
2549
2550 private void writeObject(final ObjectOutputStream oos) throws IOException {
2551 oos.defaultWriteObject();
2552 oos.writeObject(originalCharset_ == null ? null : originalCharset_.name());
2553 }
2554
2555 private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
2556 ois.defaultReadObject();
2557 final String charsetName = (String) ois.readObject();
2558 if (charsetName != null) {
2559 originalCharset_ = Charset.forName(charsetName);
2560 }
2561 }
2562
2563
2564
2565
2566 @Override
2567 public void setNodeValue(final String value) {
2568
2569 }
2570
2571
2572
2573
2574 @Override
2575 public void setPrefix(final String prefix) {
2576
2577 }
2578
2579
2580
2581
2582 @Override
2583 public void clearComputedStyles() {
2584 if (computedStylesCache_ != null) {
2585 computedStylesCache_.clear();
2586 }
2587 }
2588
2589
2590
2591
2592 @Override
2593 public void clearComputedStyles(final DomElement element) {
2594 if (computedStylesCache_ != null) {
2595 computedStylesCache_.remove(element);
2596 }
2597 }
2598
2599
2600
2601
2602 @Override
2603 public void clearComputedStylesUpToRoot(final DomElement element) {
2604 if (computedStylesCache_ != null) {
2605 computedStylesCache_.remove(element);
2606
2607 DomNode parent = element.getParentNode();
2608 while (parent != null) {
2609 computedStylesCache_.remove(parent);
2610 parent = parent.getParentNode();
2611 }
2612 }
2613 }
2614
2615
2616
2617
2618
2619
2620
2621
2622 public ComputedCssStyleDeclaration getStyleFromCache(final DomElement element,
2623 final String normalizedPseudo) {
2624 return getCssPropertiesCache().get(element, normalizedPseudo);
2625 }
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635 public void putStyleIntoCache(final DomElement element, final String normalizedPseudo,
2636 final ComputedCssStyleDeclaration style) {
2637 getCssPropertiesCache().put(element, normalizedPseudo, style);
2638 }
2639
2640
2641
2642
2643
2644
2645
2646 public List<CssStyleSheet> getStyleSheets() {
2647 final List<CssStyleSheet> styles = new ArrayList<>();
2648 if (getWebClient().getOptions().isCssEnabled()) {
2649 for (final HtmlElement htmlElement : getHtmlElementDescendants()) {
2650 if (htmlElement instanceof HtmlStyle) {
2651 styles.add(((HtmlStyle) htmlElement).getSheet());
2652 continue;
2653 }
2654
2655 if (htmlElement instanceof HtmlLink) {
2656 final HtmlLink link = (HtmlLink) htmlElement;
2657 if (link.isStyleSheetLink()) {
2658 styles.add(link.getSheet());
2659 }
2660 }
2661 }
2662 }
2663 return styles;
2664 }
2665
2666
2667
2668
2669 private ComputedStylesCache getCssPropertiesCache() {
2670 if (computedStylesCache_ == null) {
2671 computedStylesCache_ = new ComputedStylesCache();
2672
2673
2674 final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl();
2675 addDomChangeListener(listener);
2676 addHtmlAttributeChangeListener(listener);
2677 }
2678 return computedStylesCache_;
2679 }
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715 private class DomHtmlAttributeChangeListenerImpl implements DomChangeListener, HtmlAttributeChangeListener {
2716
2717
2718
2719
2720 DomHtmlAttributeChangeListenerImpl() {
2721 super();
2722 }
2723
2724
2725
2726
2727 @Override
2728 public void nodeAdded(final DomChangeEvent event) {
2729 nodeChanged(event.getChangedNode(), null);
2730 }
2731
2732
2733
2734
2735 @Override
2736 public void nodeDeleted(final DomChangeEvent event) {
2737 nodeChanged(event.getChangedNode(), null);
2738 }
2739
2740
2741
2742
2743 @Override
2744 public void attributeAdded(final HtmlAttributeChangeEvent event) {
2745 nodeChanged(event.getHtmlElement(), event.getName());
2746 }
2747
2748
2749
2750
2751 @Override
2752 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
2753 nodeChanged(event.getHtmlElement(), event.getName());
2754 }
2755
2756
2757
2758
2759 @Override
2760 public void attributeReplaced(final HtmlAttributeChangeEvent event) {
2761 nodeChanged(event.getHtmlElement(), event.getName());
2762 }
2763
2764 private void nodeChanged(final DomNode changedNode, final String attribName) {
2765
2766 if (changedNode instanceof HtmlStyle) {
2767 clearComputedStyles();
2768 return;
2769 }
2770 if (changedNode instanceof HtmlLink) {
2771 if (((HtmlLink) changedNode).isStyleSheetLink()) {
2772 clearComputedStyles();
2773 return;
2774 }
2775 }
2776
2777
2778
2779 final boolean clearParents = attribName == null || ATTRIBUTES_AFFECTING_PARENT.contains(attribName);
2780 if (computedStylesCache_ != null) {
2781 computedStylesCache_.nodeChanged(changedNode, clearParents);
2782 }
2783 }
2784 }
2785
2786
2787
2788
2789
2790
2791 private static final class ComputedStylesCache implements Serializable {
2792 private transient WeakHashMap<DomElement, Map<String, ComputedCssStyleDeclaration>>
2793 computedStyles_ = new WeakHashMap<>();
2794
2795
2796
2797
2798 ComputedStylesCache() {
2799 super();
2800 }
2801
2802 public synchronized ComputedCssStyleDeclaration get(final DomElement element,
2803 final String normalizedPseudo) {
2804 final Map<String, ComputedCssStyleDeclaration> elementMap = computedStyles_.get(element);
2805 if (elementMap != null) {
2806 return elementMap.get(normalizedPseudo);
2807 }
2808 return null;
2809 }
2810
2811 public synchronized void put(final DomElement element,
2812 final String normalizedPseudo, final ComputedCssStyleDeclaration style) {
2813 final Map<String, ComputedCssStyleDeclaration>
2814 elementMap = computedStyles_.computeIfAbsent(element, k -> new WeakHashMap<>());
2815 elementMap.put(normalizedPseudo, style);
2816 }
2817
2818 public synchronized void nodeChanged(final DomNode changed, final boolean clearParents) {
2819 final Iterator<Map.Entry<DomElement, Map<String, ComputedCssStyleDeclaration>>>
2820 i = computedStyles_.entrySet().iterator();
2821 while (i.hasNext()) {
2822 final Map.Entry<DomElement, Map<String, ComputedCssStyleDeclaration>> entry = i.next();
2823 final DomElement node = entry.getKey();
2824 if (changed == node
2825 || changed.getParentNode() == node.getParentNode()
2826 || changed.isAncestorOf(node)
2827 || clearParents && node.isAncestorOf(changed)) {
2828 i.remove();
2829 }
2830 }
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859 }
2860
2861 public synchronized void clear() {
2862 computedStyles_.clear();
2863 }
2864
2865 public synchronized Map<String, ComputedCssStyleDeclaration> remove(
2866 final DomNode element) {
2867 return computedStyles_.remove(element);
2868 }
2869
2870 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
2871 in.defaultReadObject();
2872 computedStyles_ = new WeakHashMap<>();
2873 }
2874 }
2875
2876 private static final class MappedElementIndexEntry implements Serializable {
2877 private ArrayList<DomElement> elements_;
2878 private boolean sorted_;
2879
2880 MappedElementIndexEntry() {
2881
2882 elements_ = new ArrayList<>(2);
2883 sorted_ = false;
2884 }
2885
2886 void add(final DomElement element) {
2887 elements_.add(element);
2888 sorted_ = false;
2889 }
2890
2891 DomElement first() {
2892 if (elements_.size() == 0) {
2893 return null;
2894 }
2895
2896 if (sorted_) {
2897 return elements_.get(0);
2898 }
2899
2900 Collections.sort(elements_, DOCUMENT_POSITION_COMPERATOR);
2901 sorted_ = true;
2902
2903 return elements_.get(0);
2904 }
2905
2906 List<DomElement> elements() {
2907 if (sorted_ || elements_.size() == 0) {
2908 return elements_;
2909 }
2910
2911 Collections.sort(elements_, DOCUMENT_POSITION_COMPERATOR);
2912 sorted_ = true;
2913
2914 return elements_;
2915 }
2916
2917 boolean remove(final DomElement element) {
2918 if (elements_.size() == 0) {
2919 return false;
2920 }
2921
2922 return elements_.remove(element);
2923 }
2924 }
2925 }