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 for (final BaseFrameElement frameElement : frameElements_) {
1477 final WebWindow window = frameElement.getEnclosedWindow();
1478 getWebClient().deregisterWebWindow(window);
1479 final Page page = window.getEnclosedPage();
1480 if (page != null && page.isHtmlPage()) {
1481
1482
1483 ((HtmlPage) page).deregisterFramesIfNeeded();
1484 }
1485 }
1486 }
1487
1488
1489
1490
1491
1492
1493 public List<FrameWindow> getFrames() {
1494 final List<BaseFrameElement> frameElements = new ArrayList<>(frameElements_);
1495 Collections.sort(frameElements, DOCUMENT_POSITION_COMPERATOR);
1496
1497 final List<FrameWindow> list = new ArrayList<>(frameElements.size());
1498 for (final BaseFrameElement frameElement : frameElements) {
1499 list.add(frameElement.getEnclosedWindow());
1500 }
1501 return list;
1502 }
1503
1504
1505
1506
1507
1508
1509
1510 public FrameWindow getFrameByName(final String name) throws ElementNotFoundException {
1511 for (final BaseFrameElement frameElement : frameElements_) {
1512 final FrameWindow fw = frameElement.getEnclosedWindow();
1513 if (fw.getName().equals(name)) {
1514 return fw;
1515 }
1516 }
1517
1518 throw new ElementNotFoundException("frame or iframe", DomElement.NAME_ATTRIBUTE, name);
1519 }
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531 public DomElement pressAccessKey(final char accessKey) throws IOException {
1532 final HtmlElement element = getHtmlElementByAccessKey(accessKey);
1533 if (element != null) {
1534 element.focus();
1535 if (element instanceof HtmlAnchor
1536 || element instanceof HtmlArea
1537 || element instanceof HtmlButton
1538 || element instanceof HtmlInput
1539 || element instanceof HtmlLabel
1540 || element instanceof HtmlLegend
1541 || element instanceof HtmlTextArea) {
1542 final Page newPage = element.click();
1543
1544 if (newPage != this && getFocusedElement() == element) {
1545
1546 getFocusedElement().blur();
1547 }
1548 }
1549 }
1550
1551 return getFocusedElement();
1552 }
1553
1554
1555
1556
1557
1558
1559
1560 public HtmlElement tabToNextElement() {
1561 final List<HtmlElement> elements = getTabbableElements();
1562 if (elements.isEmpty()) {
1563 setFocusedElement(null);
1564 return null;
1565 }
1566
1567 final HtmlElement elementToGiveFocus;
1568 final DomElement elementWithFocus = getFocusedElement();
1569 if (elementWithFocus == null) {
1570 elementToGiveFocus = elements.get(0);
1571 }
1572 else {
1573 final int index = elements.indexOf(elementWithFocus);
1574 if (index == -1) {
1575
1576 elementToGiveFocus = elements.get(0);
1577 }
1578 else {
1579 if (index == elements.size() - 1) {
1580 elementToGiveFocus = elements.get(0);
1581 }
1582 else {
1583 elementToGiveFocus = elements.get(index + 1);
1584 }
1585 }
1586 }
1587
1588 setFocusedElement(elementToGiveFocus);
1589 return elementToGiveFocus;
1590 }
1591
1592
1593
1594
1595
1596
1597
1598 public HtmlElement tabToPreviousElement() {
1599 final List<HtmlElement> elements = getTabbableElements();
1600 if (elements.isEmpty()) {
1601 setFocusedElement(null);
1602 return null;
1603 }
1604
1605 final HtmlElement elementToGiveFocus;
1606 final DomElement elementWithFocus = getFocusedElement();
1607 if (elementWithFocus == null) {
1608 elementToGiveFocus = elements.get(elements.size() - 1);
1609 }
1610 else {
1611 final int index = elements.indexOf(elementWithFocus);
1612 if (index == -1) {
1613
1614 elementToGiveFocus = elements.get(elements.size() - 1);
1615 }
1616 else {
1617 if (index == 0) {
1618 elementToGiveFocus = elements.get(elements.size() - 1);
1619 }
1620 else {
1621 elementToGiveFocus = elements.get(index - 1);
1622 }
1623 }
1624 }
1625
1626 setFocusedElement(elementToGiveFocus);
1627 return elementToGiveFocus;
1628 }
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640 @SuppressWarnings("unchecked")
1641 public <E extends HtmlElement> E getHtmlElementById(final String elementId) throws ElementNotFoundException {
1642 final DomElement element = getElementById(elementId);
1643 if (element == null) {
1644 throw new ElementNotFoundException("*", DomElement.ID_ATTRIBUTE, elementId);
1645 }
1646 return (E) element;
1647 }
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657 public List<DomElement> getElementsById(final String elementId) {
1658 if (elementId != null) {
1659 final MappedElementIndexEntry elements = idMap_.get(elementId);
1660 if (elements != null) {
1661 return new ArrayList<>(elements.elements());
1662 }
1663 }
1664 return Collections.emptyList();
1665 }
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676 @SuppressWarnings("unchecked")
1677 public <E extends DomElement> E getElementByName(final String name) throws ElementNotFoundException {
1678 if (name != null) {
1679 final MappedElementIndexEntry elements = nameMap_.get(name);
1680 if (elements != null) {
1681 return (E) elements.first();
1682 }
1683 }
1684 throw new ElementNotFoundException("*", DomElement.NAME_ATTRIBUTE, name);
1685 }
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695 public List<DomElement> getElementsByName(final String name) {
1696 if (name != null) {
1697 final MappedElementIndexEntry elements = nameMap_.get(name);
1698 if (elements != null) {
1699 return new ArrayList<>(elements.elements());
1700 }
1701 }
1702 return Collections.emptyList();
1703 }
1704
1705
1706
1707
1708
1709
1710
1711
1712 public List<DomElement> getElementsByIdAndOrName(final String idAndOrName) {
1713 if (idAndOrName == null) {
1714 return Collections.emptyList();
1715 }
1716 final MappedElementIndexEntry list1 = idMap_.get(idAndOrName);
1717 final MappedElementIndexEntry list2 = nameMap_.get(idAndOrName);
1718 final List<DomElement> list = new ArrayList<>();
1719 if (list1 != null) {
1720 list.addAll(list1.elements());
1721 }
1722 if (list2 != null) {
1723 for (final DomElement elt : list2.elements()) {
1724 if (!list.contains(elt)) {
1725 list.add(elt);
1726 }
1727 }
1728 }
1729 return list;
1730 }
1731
1732
1733
1734
1735
1736
1737 void notifyNodeAdded(final DomNode node) {
1738 if (node instanceof DomElement) {
1739 addMappedElement((DomElement) node, true);
1740
1741 if (node instanceof BaseFrameElement) {
1742 frameElements_.add((BaseFrameElement) node);
1743 }
1744
1745 if (node.getFirstChild() != null) {
1746 for (final Iterator<HtmlElement> iterator = node.new DescendantHtmlElementsIterator();
1747 iterator.hasNext();) {
1748 final HtmlElement child = iterator.next();
1749 if (child instanceof BaseFrameElement) {
1750 frameElements_.add((BaseFrameElement) child);
1751 }
1752 }
1753 }
1754
1755 if ("base".equals(node.getNodeName())) {
1756 calculateBase();
1757 }
1758 }
1759 node.onAddedToPage();
1760 }
1761
1762
1763
1764
1765
1766
1767 void notifyNodeRemoved(final DomNode node) {
1768 if (node instanceof HtmlElement) {
1769 removeMappedElement((HtmlElement) node, true, true);
1770
1771 if (node instanceof BaseFrameElement) {
1772 frameElements_.remove(node);
1773 }
1774 for (final HtmlElement child : node.getHtmlElementDescendants()) {
1775 if (child instanceof BaseFrameElement) {
1776 frameElements_.remove(child);
1777 }
1778 }
1779
1780 if ("base".equals(node.getNodeName())) {
1781 calculateBase();
1782 }
1783 }
1784 }
1785
1786
1787
1788
1789
1790
1791 void addMappedElement(final DomElement element, final boolean recurse) {
1792 if (isAncestorOf(element)) {
1793 addElement(idMap_, element, DomElement.ID_ATTRIBUTE, recurse);
1794 addElement(nameMap_, element, DomElement.NAME_ATTRIBUTE, recurse);
1795 }
1796 }
1797
1798 private void addElement(final Map<String, MappedElementIndexEntry> map, final DomElement element,
1799 final String attribute, final boolean recurse) {
1800 final String value = element.getAttribute(attribute);
1801
1802 if (ATTRIBUTE_NOT_DEFINED != value) {
1803 MappedElementIndexEntry elements = map.get(value);
1804 if (elements == null) {
1805 elements = new MappedElementIndexEntry();
1806 elements.add(element);
1807 map.put(value, elements);
1808 }
1809 else {
1810 elements.add(element);
1811 }
1812 }
1813 if (recurse) {
1814
1815
1816 DomNode nextChild = element.getFirstChild();
1817 while (nextChild != null) {
1818 if (nextChild instanceof DomElement) {
1819 addElement(map, (DomElement) nextChild, attribute, true);
1820 }
1821 nextChild = nextChild.getNextSibling();
1822 }
1823 }
1824 }
1825
1826
1827
1828
1829
1830
1831
1832 void removeMappedElement(final DomElement element, final boolean recurse, final boolean descendant) {
1833 if (descendant || isAncestorOf(element)) {
1834 removeElement(idMap_, element, DomElement.ID_ATTRIBUTE, recurse);
1835 removeElement(nameMap_, element, DomElement.NAME_ATTRIBUTE, recurse);
1836 }
1837 }
1838
1839 private void removeElement(final Map<String, MappedElementIndexEntry> map, final DomElement element,
1840 final String attribute, final boolean recurse) {
1841 final String value = element.getAttribute(attribute);
1842
1843 if (ATTRIBUTE_NOT_DEFINED != value) {
1844 final MappedElementIndexEntry elements = map.remove(value);
1845 if (elements != null) {
1846 elements.remove(element);
1847 if (!elements.elements_.isEmpty()) {
1848 map.put(value, elements);
1849 }
1850 }
1851 }
1852 if (recurse) {
1853 for (final DomElement child : element.getChildElements()) {
1854 removeElement(map, child, attribute, true);
1855 }
1856 }
1857 }
1858
1859
1860
1861
1862
1863
1864
1865 static boolean isMappedElement(final Document document, final String attributeName) {
1866 return document instanceof HtmlPage
1867 && (DomElement.NAME_ATTRIBUTE.equals(attributeName) || DomElement.ID_ATTRIBUTE.equals(attributeName));
1868 }
1869
1870 private void calculateBase() {
1871 final List<HtmlElement> baseElements = getDocumentElement().getStaticElementsByTagName("base");
1872
1873 base_ = null;
1874 for (final HtmlElement baseElement : baseElements) {
1875 if (baseElement instanceof HtmlBase) {
1876 if (base_ != null) {
1877 notifyIncorrectness("Multiple 'base' detected, only the first is used.");
1878 break;
1879 }
1880 base_ = (HtmlBase) baseElement;
1881 }
1882 }
1883 }
1884
1885
1886
1887
1888
1889
1890
1891 void loadFrames() throws FailingHttpStatusCodeException {
1892 for (final BaseFrameElement frameElement : new ArrayList<>(frameElements_)) {
1893
1894
1895
1896 if (frameElement.getEnclosedWindow() != null
1897 && UrlUtils.URL_ABOUT_BLANK == frameElement.getEnclosedPage().getUrl()
1898 && !frameElement.isContentLoaded()) {
1899 frameElement.loadInnerPage();
1900 }
1901 }
1902 }
1903
1904
1905
1906
1907
1908 @Override
1909 public String toString() {
1910 final StringBuilder builder = new StringBuilder()
1911 .append("HtmlPage(")
1912 .append(getUrl())
1913 .append(")@")
1914 .append(hashCode());
1915 return builder.toString();
1916 }
1917
1918
1919
1920
1921
1922
1923 protected List<HtmlMeta> getMetaTags(final String httpEquiv) {
1924 if (getDocumentElement() == null) {
1925 return Collections.emptyList();
1926 }
1927 final List<HtmlMeta> tags = getDocumentElement().getStaticElementsByTagName("meta");
1928 final List<HtmlMeta> foundTags = new ArrayList<>();
1929 for (final HtmlMeta htmlMeta : tags) {
1930 if (httpEquiv.equalsIgnoreCase(htmlMeta.getHttpEquivAttribute())) {
1931 foundTags.add(htmlMeta);
1932 }
1933 }
1934 return foundTags;
1935 }
1936
1937
1938
1939
1940
1941
1942 @Override
1943 protected HtmlPage clone() {
1944 final HtmlPage result = (HtmlPage) super.clone();
1945 result.elementWithFocus_ = null;
1946
1947 result.idMap_ = new ConcurrentHashMap<>();
1948 result.nameMap_ = new ConcurrentHashMap<>();
1949
1950 return result;
1951 }
1952
1953
1954
1955
1956 @Override
1957 public HtmlPage cloneNode(final boolean deep) {
1958
1959 final HtmlPage result = (HtmlPage) super.cloneNode(false);
1960 if (getWebClient().isJavaScriptEnabled()) {
1961 final HtmlUnitScriptable jsObjClone = getScriptableObject().clone();
1962 jsObjClone.setDomNode(result);
1963 }
1964
1965
1966 if (deep) {
1967
1968
1969
1970 result.attributeListeners_ = null;
1971
1972 result.selectionRanges_ = new ArrayList<>(3);
1973
1974 result.afterLoadActions_ = Collections.synchronizedList(new ArrayList<>());
1975 result.frameElements_ = new ArrayList<>();
1976 for (DomNode child = getFirstChild(); child != null; child = child.getNextSibling()) {
1977 result.appendChild(child.cloneNode(true));
1978 }
1979 }
1980 return result;
1981 }
1982
1983
1984
1985
1986
1987
1988
1989
1990 public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1991 WebAssert.notNull("listener", listener);
1992 synchronized (lock_) {
1993 if (attributeListeners_ == null) {
1994 attributeListeners_ = new LinkedHashSet<>();
1995 }
1996 attributeListeners_.add(listener);
1997 }
1998 }
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008 public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
2009 WebAssert.notNull("listener", listener);
2010 synchronized (lock_) {
2011 if (attributeListeners_ != null) {
2012 attributeListeners_.remove(listener);
2013 }
2014 }
2015 }
2016
2017
2018
2019
2020
2021 void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
2022 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2023 if (listeners != null) {
2024 for (final HtmlAttributeChangeListener listener : listeners) {
2025 listener.attributeAdded(event);
2026 }
2027 }
2028 }
2029
2030
2031
2032
2033
2034 void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
2035 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2036 if (listeners != null) {
2037 for (final HtmlAttributeChangeListener listener : listeners) {
2038 listener.attributeReplaced(event);
2039 }
2040 }
2041 }
2042
2043
2044
2045
2046
2047 void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
2048 final List<HtmlAttributeChangeListener> listeners = safeGetAttributeListeners();
2049 if (listeners != null) {
2050 for (final HtmlAttributeChangeListener listener : listeners) {
2051 listener.attributeRemoved(event);
2052 }
2053 }
2054 }
2055
2056 private List<HtmlAttributeChangeListener> safeGetAttributeListeners() {
2057 synchronized (lock_) {
2058 if (attributeListeners_ != null) {
2059 return new ArrayList<>(attributeListeners_);
2060 }
2061 return null;
2062 }
2063 }
2064
2065
2066
2067
2068 @Override
2069 protected void checkChildHierarchy(final org.w3c.dom.Node newChild) throws DOMException {
2070 if (newChild instanceof Element) {
2071 if (getDocumentElement() != null) {
2072 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2073 "The Document may only have a single child Element.");
2074 }
2075 }
2076 else if (newChild instanceof DocumentType) {
2077 if (getDoctype() != null) {
2078 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2079 "The Document may only have a single child DocumentType.");
2080 }
2081 }
2082 else if (!(newChild instanceof Comment || newChild instanceof ProcessingInstruction)) {
2083 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
2084 "The Document may not have a child of this type: " + newChild.getNodeType());
2085 }
2086 super.checkChildHierarchy(newChild);
2087 }
2088
2089
2090
2091
2092
2093 public boolean isBeingParsed() {
2094 return parserCount_ > 0;
2095 }
2096
2097
2098
2099
2100
2101
2102 public void registerParsingStart() {
2103 parserCount_++;
2104 }
2105
2106
2107
2108
2109
2110
2111 public void registerParsingEnd() {
2112 parserCount_--;
2113 }
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127 public boolean isParsingHtmlSnippet() {
2128 return snippetParserCount_ > 0;
2129 }
2130
2131
2132
2133
2134
2135
2136 public void registerSnippetParsingStart() {
2137 snippetParserCount_++;
2138 }
2139
2140
2141
2142
2143
2144
2145 public void registerSnippetParsingEnd() {
2146 snippetParserCount_--;
2147 }
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159 public boolean isParsingInlineHtmlSnippet() {
2160 return inlineSnippetParserCount_ > 0;
2161 }
2162
2163
2164
2165
2166
2167
2168 public void registerInlineSnippetParsingStart() {
2169 inlineSnippetParserCount_++;
2170 }
2171
2172
2173
2174
2175
2176
2177 public void registerInlineSnippetParsingEnd() {
2178 inlineSnippetParserCount_--;
2179 }
2180
2181
2182
2183
2184
2185
2186 public Page refresh() throws IOException {
2187 return getWebClient().getPage(getWebResponse().getWebRequest());
2188 }
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198 public void writeInParsedStream(final String string) {
2199 getDOMBuilder().pushInputString(string);
2200 }
2201
2202
2203
2204
2205
2206
2207
2208 public void setDOMBuilder(final HTMLParserDOMBuilder htmlUnitDOMBuilder) {
2209 domBuilder_ = htmlUnitDOMBuilder;
2210 }
2211
2212
2213
2214
2215
2216
2217
2218 public HTMLParserDOMBuilder getDOMBuilder() {
2219 return domBuilder_;
2220 }
2221
2222
2223
2224
2225
2226
2227 public Map<String, String> getNamespaces() {
2228 final org.w3c.dom.NamedNodeMap attributes = getDocumentElement().getAttributes();
2229 final Map<String, String> namespaces = new HashMap<>();
2230 for (int i = 0; i < attributes.getLength(); i++) {
2231 final Attr attr = (Attr) attributes.item(i);
2232 String name = attr.getName();
2233 if (name.startsWith("xmlns")) {
2234 int startPos = 5;
2235 if (name.length() > 5 && name.charAt(5) == ':') {
2236 startPos = 6;
2237 }
2238 name = name.substring(startPos);
2239 namespaces.put(name, attr.getValue());
2240 }
2241 }
2242 return namespaces;
2243 }
2244
2245
2246
2247
2248 @Override
2249 public void setDocumentType(final DocumentType type) {
2250 super.setDocumentType(type);
2251 }
2252
2253
2254
2255
2256
2257
2258
2259
2260 public void save(final File file) throws IOException {
2261 new XmlSerializer().save(this, file);
2262 }
2263
2264
2265
2266
2267
2268 public boolean isQuirksMode() {
2269 return "BackCompat".equals(((HTMLDocument) getScriptableObject()).getCompatMode());
2270 }
2271
2272
2273
2274
2275
2276 @Override
2277 public boolean isAttachedToPage() {
2278 return true;
2279 }
2280
2281
2282
2283
2284 @Override
2285 public boolean isHtmlPage() {
2286 return true;
2287 }
2288
2289
2290
2291
2292
2293 public URL getBaseURL() {
2294 URL baseUrl;
2295 if (base_ == null) {
2296 baseUrl = getUrl();
2297 final WebWindow window = getEnclosingWindow();
2298 final boolean frame = window != null && window != window.getTopWindow();
2299 if (frame) {
2300 final boolean frameSrcIsNotSet = baseUrl == UrlUtils.URL_ABOUT_BLANK;
2301 final boolean frameSrcIsJs = "javascript".equals(baseUrl.getProtocol());
2302 if (frameSrcIsNotSet || frameSrcIsJs) {
2303 baseUrl = window.getTopWindow().getEnclosedPage().getWebResponse()
2304 .getWebRequest().getUrl();
2305 }
2306 }
2307 else if (baseUrl_ != null) {
2308 baseUrl = baseUrl_;
2309 }
2310 }
2311 else {
2312 final String href = base_.getHrefAttribute().trim();
2313 if (StringUtils.isEmpty(href)) {
2314 baseUrl = getUrl();
2315 }
2316 else {
2317 final URL url = getUrl();
2318 try {
2319 if (href.startsWith("http://") || href.startsWith("https://")) {
2320 baseUrl = new URL(href);
2321 }
2322 else if (href.startsWith("//")) {
2323 baseUrl = new URL(String.format("%s:%s", url.getProtocol(), href));
2324 }
2325 else if (href.length() > 0 && href.charAt(0) == '/') {
2326 final int port = Window.getPort(url);
2327 baseUrl = new URL(String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), port, href));
2328 }
2329 else if (url.toString().endsWith("/")) {
2330 baseUrl = new URL(String.format("%s%s", url, href));
2331 }
2332 else {
2333 baseUrl = new URL(UrlUtils.resolveUrl(url, href));
2334 }
2335 }
2336 catch (final MalformedURLException e) {
2337 notifyIncorrectness("Invalid base url: \"" + href + "\", ignoring it");
2338 baseUrl = url;
2339 }
2340 }
2341 }
2342
2343 return baseUrl;
2344 }
2345
2346
2347
2348
2349
2350
2351
2352 public void addAutoCloseable(final AutoCloseable autoCloseable) {
2353 if (autoCloseable == null) {
2354 return;
2355 }
2356
2357 if (autoCloseableList_ == null) {
2358 autoCloseableList_ = new ArrayList<>();
2359 }
2360 autoCloseableList_.add(autoCloseable);
2361 }
2362
2363
2364
2365
2366 @Override
2367 public boolean handles(final Event event) {
2368 if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
2369 return true;
2370 }
2371 return super.handles(event);
2372 }
2373
2374
2375
2376
2377
2378 public void setElementFromPointHandler(final ElementFromPointHandler elementFromPointHandler) {
2379 elementFromPointHandler_ = elementFromPointHandler;
2380 }
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391 public HtmlElement getElementFromPoint(final int x, final int y) {
2392 if (elementFromPointHandler_ == null) {
2393 if (LOG.isWarnEnabled()) {
2394 LOG.warn("ElementFromPointHandler was not specicifed for " + this);
2395 }
2396 if (x <= 0 || y <= 0) {
2397 return null;
2398 }
2399 return getBody();
2400 }
2401 return elementFromPointHandler_.getElementFromPoint(this, x, y);
2402 }
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412 public boolean setFocusedElement(final DomElement newElement) {
2413 return setFocusedElement(newElement, false);
2414 }
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425 public boolean setFocusedElement(final DomElement newElement, final boolean windowActivated) {
2426 if (elementWithFocus_ == newElement && !windowActivated) {
2427
2428 return true;
2429 }
2430
2431 final DomElement oldFocusedElement = elementWithFocus_;
2432 elementWithFocus_ = null;
2433
2434 if (!windowActivated) {
2435 if (oldFocusedElement != null) {
2436 oldFocusedElement.removeFocus();
2437 oldFocusedElement.fireEvent(Event.TYPE_BLUR);
2438
2439 oldFocusedElement.fireEvent(Event.TYPE_FOCUS_OUT);
2440 }
2441 }
2442
2443 elementWithFocus_ = newElement;
2444
2445
2446
2447 if (newElement != null) {
2448 newElement.focus();
2449 newElement.fireEvent(Event.TYPE_FOCUS);
2450
2451 newElement.fireEvent(Event.TYPE_FOCUS_IN);
2452 }
2453
2454
2455
2456 return this == getEnclosingWindow().getEnclosedPage();
2457 }
2458
2459
2460
2461
2462
2463
2464 public DomElement getFocusedElement() {
2465 return elementWithFocus_;
2466 }
2467
2468
2469
2470
2471
2472
2473
2474 public void setElementWithFocus(final DomElement elementWithFocus) {
2475 elementWithFocus_ = elementWithFocus;
2476 }
2477
2478
2479
2480
2481
2482
2483 public HtmlElement getActiveElement() {
2484 final DomElement activeElement = getFocusedElement();
2485 if (activeElement instanceof HtmlElement) {
2486 return (HtmlElement) activeElement;
2487 }
2488
2489 final HtmlElement body = getBody();
2490 if (body != null) {
2491 return body;
2492 }
2493 return null;
2494 }
2495
2496
2497
2498
2499
2500
2501
2502
2503 public List<SimpleRange> getSelectionRanges() {
2504 return selectionRanges_;
2505 }
2506
2507
2508
2509
2510
2511
2512
2513
2514 public void setSelectionRange(final SimpleRange selectionRange) {
2515 selectionRanges_.clear();
2516 selectionRanges_.add(selectionRange);
2517 }
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533 public ScriptResult executeJavaScriptFunction(final Object function, final Object thisObject,
2534 final Object[] args, final DomNode htmlElementScope) {
2535 if (!getWebClient().isJavaScriptEnabled()) {
2536 return new ScriptResult(null);
2537 }
2538
2539 return executeJavaScriptFunction((Function) function, (Scriptable) thisObject, args, htmlElementScope);
2540 }
2541
2542 private ScriptResult executeJavaScriptFunction(final Function function, final Scriptable thisObject,
2543 final Object[] args, final DomNode htmlElementScope) {
2544
2545 final JavaScriptEngine engine = (JavaScriptEngine) getWebClient().getJavaScriptEngine();
2546 final Object result = engine.callFunction(this, function, thisObject, args, htmlElementScope);
2547
2548 return new ScriptResult(result);
2549 }
2550
2551 private void writeObject(final ObjectOutputStream oos) throws IOException {
2552 oos.defaultWriteObject();
2553 oos.writeObject(originalCharset_ == null ? null : originalCharset_.name());
2554 }
2555
2556 private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
2557 ois.defaultReadObject();
2558 final String charsetName = (String) ois.readObject();
2559 if (charsetName != null) {
2560 originalCharset_ = Charset.forName(charsetName);
2561 }
2562 }
2563
2564
2565
2566
2567 @Override
2568 public void setNodeValue(final String value) {
2569
2570 }
2571
2572
2573
2574
2575 @Override
2576 public void setPrefix(final String prefix) {
2577
2578 }
2579
2580
2581
2582
2583 @Override
2584 public void clearComputedStyles() {
2585 if (computedStylesCache_ != null) {
2586 computedStylesCache_.clear();
2587 }
2588 }
2589
2590
2591
2592
2593 @Override
2594 public void clearComputedStyles(final DomElement element) {
2595 if (computedStylesCache_ != null) {
2596 computedStylesCache_.remove(element);
2597 }
2598 }
2599
2600
2601
2602
2603 @Override
2604 public void clearComputedStylesUpToRoot(final DomElement element) {
2605 if (computedStylesCache_ != null) {
2606 computedStylesCache_.remove(element);
2607
2608 DomNode parent = element.getParentNode();
2609 while (parent != null) {
2610 computedStylesCache_.remove(parent);
2611 parent = parent.getParentNode();
2612 }
2613 }
2614 }
2615
2616
2617
2618
2619
2620
2621
2622
2623 public ComputedCssStyleDeclaration getStyleFromCache(final DomElement element,
2624 final String normalizedPseudo) {
2625 return getCssPropertiesCache().get(element, normalizedPseudo);
2626 }
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636 public void putStyleIntoCache(final DomElement element, final String normalizedPseudo,
2637 final ComputedCssStyleDeclaration style) {
2638 getCssPropertiesCache().put(element, normalizedPseudo, style);
2639 }
2640
2641
2642
2643
2644
2645
2646
2647 public List<CssStyleSheet> getStyleSheets() {
2648 final List<CssStyleSheet> styles = new ArrayList<>();
2649 if (getWebClient().getOptions().isCssEnabled()) {
2650 for (final HtmlElement htmlElement : getHtmlElementDescendants()) {
2651 if (htmlElement instanceof HtmlStyle) {
2652 styles.add(((HtmlStyle) htmlElement).getSheet());
2653 continue;
2654 }
2655
2656 if (htmlElement instanceof HtmlLink) {
2657 final HtmlLink link = (HtmlLink) htmlElement;
2658 if (link.isStyleSheetLink()) {
2659 styles.add(link.getSheet());
2660 }
2661 }
2662 }
2663 }
2664 return styles;
2665 }
2666
2667
2668
2669
2670 private ComputedStylesCache getCssPropertiesCache() {
2671 if (computedStylesCache_ == null) {
2672 computedStylesCache_ = new ComputedStylesCache();
2673
2674
2675 final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl();
2676 addDomChangeListener(listener);
2677 addHtmlAttributeChangeListener(listener);
2678 }
2679 return computedStylesCache_;
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
2716 private class DomHtmlAttributeChangeListenerImpl implements DomChangeListener, HtmlAttributeChangeListener {
2717
2718
2719
2720
2721 DomHtmlAttributeChangeListenerImpl() {
2722 super();
2723 }
2724
2725
2726
2727
2728 @Override
2729 public void nodeAdded(final DomChangeEvent event) {
2730 nodeChanged(event.getChangedNode(), null);
2731 }
2732
2733
2734
2735
2736 @Override
2737 public void nodeDeleted(final DomChangeEvent event) {
2738 nodeChanged(event.getChangedNode(), null);
2739 }
2740
2741
2742
2743
2744 @Override
2745 public void attributeAdded(final HtmlAttributeChangeEvent event) {
2746 nodeChanged(event.getHtmlElement(), event.getName());
2747 }
2748
2749
2750
2751
2752 @Override
2753 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
2754 nodeChanged(event.getHtmlElement(), event.getName());
2755 }
2756
2757
2758
2759
2760 @Override
2761 public void attributeReplaced(final HtmlAttributeChangeEvent event) {
2762 nodeChanged(event.getHtmlElement(), event.getName());
2763 }
2764
2765 private void nodeChanged(final DomNode changedNode, final String attribName) {
2766
2767 if (changedNode instanceof HtmlStyle) {
2768 clearComputedStyles();
2769 return;
2770 }
2771 if (changedNode instanceof HtmlLink) {
2772 if (((HtmlLink) changedNode).isStyleSheetLink()) {
2773 clearComputedStyles();
2774 return;
2775 }
2776 }
2777
2778
2779
2780 final boolean clearParents = attribName == null || ATTRIBUTES_AFFECTING_PARENT.contains(attribName);
2781 if (computedStylesCache_ != null) {
2782 computedStylesCache_.nodeChanged(changedNode, clearParents);
2783 }
2784 }
2785 }
2786
2787
2788
2789
2790
2791
2792 private static final class ComputedStylesCache implements Serializable {
2793 private transient WeakHashMap<DomElement, Map<String, ComputedCssStyleDeclaration>>
2794 computedStyles_ = new WeakHashMap<>();
2795
2796
2797
2798
2799 ComputedStylesCache() {
2800 super();
2801 }
2802
2803 public synchronized ComputedCssStyleDeclaration get(final DomElement element,
2804 final String normalizedPseudo) {
2805 final Map<String, ComputedCssStyleDeclaration> elementMap = computedStyles_.get(element);
2806 if (elementMap != null) {
2807 return elementMap.get(normalizedPseudo);
2808 }
2809 return null;
2810 }
2811
2812 public synchronized void put(final DomElement element,
2813 final String normalizedPseudo, final ComputedCssStyleDeclaration style) {
2814 final Map<String, ComputedCssStyleDeclaration>
2815 elementMap = computedStyles_.computeIfAbsent(element, k -> new WeakHashMap<>());
2816 elementMap.put(normalizedPseudo, style);
2817 }
2818
2819 public synchronized void nodeChanged(final DomNode changed, final boolean clearParents) {
2820 final Iterator<Map.Entry<DomElement, Map<String, ComputedCssStyleDeclaration>>>
2821 i = computedStyles_.entrySet().iterator();
2822 while (i.hasNext()) {
2823 final Map.Entry<DomElement, Map<String, ComputedCssStyleDeclaration>> entry = i.next();
2824 final DomElement node = entry.getKey();
2825 if (changed == node
2826 || changed.getParentNode() == node.getParentNode()
2827 || changed.isAncestorOf(node)
2828 || clearParents && node.isAncestorOf(changed)) {
2829 i.remove();
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
2862 public synchronized void clear() {
2863 computedStyles_.clear();
2864 }
2865
2866 public synchronized Map<String, ComputedCssStyleDeclaration> remove(
2867 final DomNode element) {
2868 return computedStyles_.remove(element);
2869 }
2870
2871 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
2872 in.defaultReadObject();
2873 computedStyles_ = new WeakHashMap<>();
2874 }
2875 }
2876
2877 private static final class MappedElementIndexEntry implements Serializable {
2878 private final ArrayList<DomElement> elements_;
2879 private boolean sorted_;
2880
2881 MappedElementIndexEntry() {
2882
2883 elements_ = new ArrayList<>(2);
2884 sorted_ = false;
2885 }
2886
2887 void add(final DomElement element) {
2888 elements_.add(element);
2889 sorted_ = false;
2890 }
2891
2892 DomElement first() {
2893 if (elements_.size() == 0) {
2894 return null;
2895 }
2896
2897 if (sorted_) {
2898 return elements_.get(0);
2899 }
2900
2901 Collections.sort(elements_, DOCUMENT_POSITION_COMPERATOR);
2902 sorted_ = true;
2903
2904 return elements_.get(0);
2905 }
2906
2907 List<DomElement> elements() {
2908 if (sorted_ || elements_.size() == 0) {
2909 return elements_;
2910 }
2911
2912 Collections.sort(elements_, DOCUMENT_POSITION_COMPERATOR);
2913 sorted_ = true;
2914
2915 return elements_;
2916 }
2917
2918 boolean remove(final DomElement element) {
2919 if (elements_.size() == 0) {
2920 return false;
2921 }
2922
2923 return elements_.remove(element);
2924 }
2925 }
2926 }