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