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