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;
16  
17  import static java.nio.charset.StandardCharsets.ISO_8859_1;
18  import static java.nio.charset.StandardCharsets.UTF_8;
19  import static org.htmlunit.BrowserVersionFeatures.HTTP_HEADER_CH_UA;
20  import static org.htmlunit.BrowserVersionFeatures.HTTP_HEADER_PRIORITY;
21  
22  import java.io.BufferedInputStream;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.ObjectInputStream;
27  import java.io.Serializable;
28  import java.lang.ref.WeakReference;
29  import java.net.MalformedURLException;
30  import java.net.URL;
31  import java.net.URLConnection;
32  import java.net.URLDecoder;
33  import java.nio.charset.Charset;
34  import java.nio.file.Files;
35  import java.util.ArrayList;
36  import java.util.Collections;
37  import java.util.ConcurrentModificationException;
38  import java.util.Date;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.Iterator;
42  import java.util.LinkedHashMap;
43  import java.util.LinkedHashSet;
44  import java.util.List;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.Objects;
48  import java.util.Optional;
49  import java.util.Set;
50  import java.util.concurrent.ConcurrentLinkedDeque;
51  import java.util.concurrent.Executor;
52  import java.util.concurrent.ExecutorService;
53  import java.util.concurrent.Executors;
54  import java.util.concurrent.ThreadFactory;
55  import java.util.concurrent.ThreadPoolExecutor;
56  
57  import org.apache.commons.logging.Log;
58  import org.apache.commons.logging.LogFactory;
59  import org.apache.http.NoHttpResponseException;
60  import org.apache.http.client.CredentialsProvider;
61  import org.apache.http.cookie.MalformedCookieException;
62  import org.htmlunit.attachment.Attachment;
63  import org.htmlunit.attachment.AttachmentHandler;
64  import org.htmlunit.csp.Policy;
65  import org.htmlunit.csp.url.URI;
66  import org.htmlunit.css.ComputedCssStyleDeclaration;
67  import org.htmlunit.cssparser.parser.CSSErrorHandler;
68  import org.htmlunit.cssparser.parser.javacc.CSS3Parser;
69  import org.htmlunit.html.BaseFrameElement;
70  import org.htmlunit.html.DomElement;
71  import org.htmlunit.html.DomNode;
72  import org.htmlunit.html.FrameWindow;
73  import org.htmlunit.html.FrameWindow.PageDenied;
74  import org.htmlunit.html.HtmlElement;
75  import org.htmlunit.html.HtmlInlineFrame;
76  import org.htmlunit.html.HtmlPage;
77  import org.htmlunit.html.XHtmlPage;
78  import org.htmlunit.html.parser.HTMLParser;
79  import org.htmlunit.html.parser.HTMLParserListener;
80  import org.htmlunit.http.HttpStatus;
81  import org.htmlunit.http.HttpUtils;
82  import org.htmlunit.httpclient.HttpClientConverter;
83  import org.htmlunit.javascript.AbstractJavaScriptEngine;
84  import org.htmlunit.javascript.DefaultJavaScriptErrorListener;
85  import org.htmlunit.javascript.HtmlUnitScriptable;
86  import org.htmlunit.javascript.JavaScriptEngine;
87  import org.htmlunit.javascript.JavaScriptErrorListener;
88  import org.htmlunit.javascript.background.JavaScriptJobManager;
89  import org.htmlunit.javascript.host.BroadcastChannel;
90  import org.htmlunit.javascript.host.Location;
91  import org.htmlunit.javascript.host.Window;
92  import org.htmlunit.javascript.host.dom.Node;
93  import org.htmlunit.javascript.host.event.Event;
94  import org.htmlunit.javascript.host.file.Blob;
95  import org.htmlunit.javascript.host.html.HTMLIFrameElement;
96  import org.htmlunit.protocol.data.DataURLConnection;
97  import org.htmlunit.util.Cookie;
98  import org.htmlunit.util.HeaderUtils;
99  import org.htmlunit.util.MimeType;
100 import org.htmlunit.util.NameValuePair;
101 import org.htmlunit.util.StringUtils;
102 import org.htmlunit.util.UrlUtils;
103 import org.htmlunit.websocket.JettyWebSocketAdapter.JettyWebSocketAdapterFactory;
104 import org.htmlunit.websocket.WebSocketAdapter;
105 import org.htmlunit.websocket.WebSocketAdapterFactory;
106 import org.htmlunit.websocket.WebSocketListener;
107 import org.htmlunit.webstart.WebStartHandler;
108 
109 /**
110  * The main starting point in HtmlUnit: this class simulates a web browser.
111  * <p>
112  * A standard usage of HtmlUnit will start with using the {@link #getPage(String)} method
113  * (or {@link #getPage(URL)}) to load a first {@link Page}
114  * and will continue with further processing on this page depending on its type.
115  * </p>
116  * <b>Example:</b><br>
117  * <br>
118  * <code>
119  * final WebClient webClient = new WebClient();<br>
120  * final {@link HtmlPage} startPage = webClient.getPage("http://htmlunit.sf.net");<br>
121  * assertEquals("HtmlUnit - Welcome to HtmlUnit", startPage.{@link HtmlPage#getTitleText() getTitleText}());
122  * </code>
123  * <p>
124  * Note: a {@link WebClient} instance is <b>not thread safe</b>. It is intended to be used from a single thread.
125  * </p>
126  * @author Mike Bowler
127  * @author Mike J. Bresnahan
128  * @author Dominique Broeglin
129  * @author Noboru Sinohara
130  * @author Chen Jun
131  * @author David K. Taylor
132  * @author Christian Sell
133  * @author Ben Curren
134  * @author Marc Guillemot
135  * @author Chris Erskine
136  * @author Daniel Gredler
137  * @author Sergey Gorelkin
138  * @author Hans Donner
139  * @author Paul King
140  * @author Ahmed Ashour
141  * @author Bruce Chapman
142  * @author Sudhan Moghe
143  * @author Martin Tamme
144  * @author Amit Manjhi
145  * @author Nicolas Belisle
146  * @author Ronald Brill
147  * @author Frank Danek
148  * @author Joerg Werner
149  * @author Anton Demydenko
150  * @author Sergio Moreno
151  * @author Lai Quang Duong
152  * @author René Schwietzke
153  * @author Sven Strickroth
154  */
155 @SuppressWarnings("PMD.TooManyFields")
156 public class WebClient implements Serializable, AutoCloseable {
157 
158     /** Logging support. */
159     private static final Log LOG = LogFactory.getLog(WebClient.class);
160 
161     /** Like the Firefox default value for {@code network.http.redirection-limit}. */
162     private static final int ALLOWED_REDIRECTIONS_SAME_URL = 20;
163     private static final WebResponseData RESPONSE_DATA_NO_HTTP_RESPONSE = new WebResponseData(
164             0, "No HTTP Response", Collections.emptyList());
165 
166     /**
167      * These response headers are not copied from a 304 response to the cached
168      * response headers. This list is based on Chromium http_response_headers.cc
169      */
170     private static final String[] DISCARDING_304_RESPONSE_HEADER_NAMES = {
171         "connection",
172         "proxy-connection",
173         "keep-alive",
174         "www-authenticate",
175         "proxy-authenticate",
176         "proxy-authorization",
177         "te",
178         "trailer",
179         "transfer-encoding",
180         "upgrade",
181         "content-location",
182         "content-md5",
183         "etag",
184         "content-encoding",
185         "content-range",
186         "content-type",
187         "content-length",
188         "x-frame-options",
189         "x-xss-protection",
190     };
191 
192     private static final String[] DISCARDING_304_HEADER_PREFIXES = {
193         "x-content-",
194         "x-webkit-"
195     };
196 
197     private transient WebConnection webConnection_;
198     private CredentialsProvider credentialsProvider_ = new DefaultCredentialsProvider();
199     private CookieManager cookieManager_ = new CookieManager();
200     private WebSocketAdapterFactory webSocketAdapterFactory_;
201     private transient AbstractJavaScriptEngine<?> scriptEngine_;
202     private transient List<LoadJob> loadQueue_;
203     private final Map<String, String> requestHeaders_ = Collections.synchronizedMap(new HashMap<>(89));
204     private IncorrectnessListener incorrectnessListener_ = new IncorrectnessListenerImpl();
205     private WebConsole webConsole_;
206     private transient ExecutorService executor_;
207 
208     private AlertHandler alertHandler_;
209     private ConfirmHandler confirmHandler_;
210     private PromptHandler promptHandler_;
211     private StatusHandler statusHandler_;
212     private AttachmentHandler attachmentHandler_;
213     private ClipboardHandler clipboardHandler_;
214     private PrintHandler printHandler_;
215     private WebStartHandler webStartHandler_;
216     private FrameContentHandler frameContentHandler_;
217 
218     private AjaxController ajaxController_ = new AjaxController();
219 
220     private final BrowserVersion browserVersion_;
221     private PageCreator pageCreator_ = new DefaultPageCreator();
222 
223     // we need a separate one to be sure the one is always informed as first
224     // one. Only then we can make sure our state is consistent when the others
225     // are informed.
226     private CurrentWindowTracker currentWindowTracker_;
227     private final Set<WebWindowListener> webWindowListeners_ = new HashSet<>(5);
228 
229     private final List<TopLevelWindow> topLevelWindows_ =
230             Collections.synchronizedList(new ArrayList<>()); // top-level windows
231     private final List<WebWindow> windows_ = Collections.synchronizedList(new ArrayList<>()); // all windows
232     private transient List<WeakReference<JavaScriptJobManager>> jobManagers_ =
233             Collections.synchronizedList(new ArrayList<>());
234     private WebWindow currentWindow_;
235 
236     private HTMLParserListener htmlParserListener_;
237     private CSSErrorHandler cssErrorHandler_ = new DefaultCssErrorHandler();
238     private OnbeforeunloadHandler onbeforeunloadHandler_;
239     private Cache cache_ = new Cache();
240 
241     // mini pool to save resource when parsing CSS
242     private transient CSS3ParserPool css3ParserPool_ = new CSS3ParserPool();
243 
244     /** target "_blank". */
245     public static final String TARGET_BLANK = "_blank";
246 
247     /** target "_self". */
248     public static final String TARGET_SELF = "_self";
249 
250     /** target "_parent". */
251     private static final String TARGET_PARENT = "_parent";
252     /** target "_top". */
253     private static final String TARGET_TOP = "_top";
254 
255     private ScriptPreProcessor scriptPreProcessor_;
256 
257     private RefreshHandler refreshHandler_ = new NiceRefreshHandler(2);
258     private JavaScriptErrorListener javaScriptErrorListener_ = new DefaultJavaScriptErrorListener();
259 
260     private final WebClientOptions options_ = new WebClientOptions();
261     private final boolean javaScriptEngineEnabled_;
262     private final StorageHolder storageHolder_ = new StorageHolder();
263 
264     private transient Set<BroadcastChannel> broadcastChannel_ = new HashSet<>();
265 
266     /**
267      * Creates a web client instance using the browser version returned by
268      * {@link BrowserVersion#getDefault()}.
269      */
270     public WebClient() {
271         this(BrowserVersion.getDefault());
272     }
273 
274     /**
275      * Creates a web client instance using the specified {@link BrowserVersion}.
276      * @param browserVersion the browser version to simulate
277      */
278     public WebClient(final BrowserVersion browserVersion) {
279         this(browserVersion, null, -1);
280     }
281 
282     /**
283      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
284      * @param browserVersion the browser version to simulate
285      * @param proxyHost the server that will act as proxy or null for no proxy
286      * @param proxyPort the port to use on the proxy server
287      */
288     public WebClient(final BrowserVersion browserVersion, final String proxyHost, final int proxyPort) {
289         this(browserVersion, true, proxyHost, proxyPort, null);
290     }
291 
292     /**
293      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
294      * @param browserVersion the browser version to simulate
295      * @param proxyHost the server that will act as proxy or null for no proxy
296      * @param proxyPort the port to use on the proxy server
297      * @param proxyScheme the scheme http/https
298      */
299     public WebClient(final BrowserVersion browserVersion,
300             final String proxyHost, final int proxyPort, final String proxyScheme) {
301         this(browserVersion, true, proxyHost, proxyPort, proxyScheme);
302     }
303 
304     /**
305      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
306      * @param browserVersion the browser version to simulate
307      * @param javaScriptEngineEnabled set to false if the simulated browser should not support javaScript
308      * @param proxyHost the server that will act as proxy or null for no proxy
309      * @param proxyPort the port to use on the proxy server
310      */
311     public WebClient(final BrowserVersion browserVersion, final boolean javaScriptEngineEnabled,
312             final String proxyHost, final int proxyPort) {
313         this(browserVersion, javaScriptEngineEnabled, proxyHost, proxyPort, null);
314     }
315 
316     /**
317      * Creates an instance that will use the specified {@link BrowserVersion} and proxy server.
318      * @param browserVersion the browser version to simulate
319      * @param javaScriptEngineEnabled set to false if the simulated browser should not support javaScript
320      * @param proxyHost the server that will act as proxy or null for no proxy
321      * @param proxyPort the port to use on the proxy server
322      * @param proxyScheme the scheme http/https
323      */
324     public WebClient(final BrowserVersion browserVersion, final boolean javaScriptEngineEnabled,
325             final String proxyHost, final int proxyPort, final String proxyScheme) {
326         WebAssert.notNull("browserVersion", browserVersion);
327 
328         browserVersion_ = browserVersion;
329         javaScriptEngineEnabled_ = javaScriptEngineEnabled;
330 
331         if (proxyHost == null) {
332             getOptions().setProxyConfig(new ProxyConfig());
333         }
334         else {
335             getOptions().setProxyConfig(new ProxyConfig(proxyHost, proxyPort, proxyScheme));
336         }
337 
338         webConnection_ = new HttpWebConnection(this); // this has to be done after the browser version was set
339         if (javaScriptEngineEnabled_) {
340             scriptEngine_ = new JavaScriptEngine(this);
341         }
342         loadQueue_ = new ArrayList<>();
343 
344         webSocketAdapterFactory_ = new JettyWebSocketAdapterFactory();
345 
346         // The window must be constructed AFTER the script engine.
347         currentWindowTracker_ = new CurrentWindowTracker(this, true);
348         currentWindow_ = new TopLevelWindow("", this);
349     }
350 
351     /**
352      * Our simple impl of a ThreadFactory (decorator) to be able to name
353      * our threads.
354      */
355     private static final class ThreadNamingFactory implements ThreadFactory {
356         private static int ID_ = 1;
357         private final ThreadFactory baseFactory_;
358 
359         ThreadNamingFactory(final ThreadFactory aBaseFactory) {
360             baseFactory_ = aBaseFactory;
361         }
362 
363         @Override
364         public Thread newThread(final Runnable aRunnable) {
365             final Thread thread = baseFactory_.newThread(aRunnable);
366             thread.setName("WebClient Thread " + ID_++);
367             return thread;
368         }
369     }
370 
371     /**
372      * Returns the object that will resolve all URL requests.
373      *
374      * @return the connection that will be used
375      */
376     public WebConnection getWebConnection() {
377         return webConnection_;
378     }
379 
380     /**
381      * Sets the object that will resolve all URL requests.
382      *
383      * @param webConnection the new web connection
384      */
385     public void setWebConnection(final WebConnection webConnection) {
386         WebAssert.notNull("webConnection", webConnection);
387         webConnection_ = webConnection;
388     }
389 
390     /**
391      * Send a request to a server and return a Page that represents the
392      * response from the server. This page will be used to populate the provided window.
393      * <p>
394      * The returned {@link Page} will be created by the {@link PageCreator}
395      * configured by {@link #setPageCreator(PageCreator)}, if any.
396      * <p>
397      * The {@link DefaultPageCreator} will create a {@link Page} depending on the content type of the HTTP response,
398      * basically {@link HtmlPage} for HTML content, {@link org.htmlunit.xml.XmlPage} for XML content,
399      * {@link TextPage} for other text content and {@link UnexpectedPage} for anything else.
400      *
401      * @param webWindow the WebWindow to load the result of the request into
402      * @param webRequest the web request
403      * @param <P> the page type
404      * @return the page returned by the server when the specified request was made in the specified window
405      * @throws IOException if an IO error occurs
406      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
407      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
408      *
409      * @see WebRequest
410      */
411     public <P extends Page> P getPage(final WebWindow webWindow, final WebRequest webRequest)
412             throws IOException, FailingHttpStatusCodeException {
413         return getPage(webWindow, webRequest, true);
414     }
415 
416     /**
417      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
418      *
419      * Send a request to a server and return a Page that represents the
420      * response from the server. This page will be used to populate the provided window.
421      * <p>
422      * The returned {@link Page} will be created by the {@link PageCreator}
423      * configured by {@link #setPageCreator(PageCreator)}, if any.
424      * <p>
425      * The {@link DefaultPageCreator} will create a {@link Page} depending on the content type of the HTTP response,
426      * basically {@link HtmlPage} for HTML content, {@link org.htmlunit.xml.XmlPage} for XML content,
427      * {@link TextPage} for other text content and {@link UnexpectedPage} for anything else.
428      *
429      * @param webWindow the WebWindow to load the result of the request into
430      * @param webRequest the web request
431      * @param addToHistory true if the page should be part of the history
432      * @param <P> the page type
433      * @return the page returned by the server when the specified request was made in the specified window
434      * @throws IOException if an IO error occurs
435      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
436      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
437      *
438      * @see WebRequest
439      */
440     @SuppressWarnings("unchecked")
441     <P extends Page> P getPage(final WebWindow webWindow, final WebRequest webRequest,
442             final boolean addToHistory)
443         throws IOException, FailingHttpStatusCodeException {
444 
445         final Page page = webWindow.getEnclosedPage();
446 
447         if (page != null) {
448             final URL prev = page.getUrl();
449             final URL current = webRequest.getUrl();
450             if (UrlUtils.sameFile(current, prev)
451                         && current.getRef() != null
452                         && !Objects.equals(current.getRef(), prev.getRef())) {
453                 // We're just navigating to an anchor within the current page.
454                 page.getWebResponse().getWebRequest().setUrl(current);
455                 if (addToHistory) {
456                     webWindow.getHistory().addPage(page);
457                 }
458 
459                 // clear the cache because the anchors are now matched by
460                 // the target pseudo style
461                 if (page instanceof HtmlPage) {
462                     ((HtmlPage) page).clearComputedStyles();
463                 }
464 
465                 final Window window = webWindow.getScriptableObject();
466                 if (window != null) { // js enabled
467                     window.getLocation().setHash(current.getRef());
468                 }
469                 return (P) page;
470             }
471 
472             if (page.isHtmlPage()) {
473                 final HtmlPage htmlPage = (HtmlPage) page;
474                 if (!htmlPage.isOnbeforeunloadAccepted()) {
475                     LOG.debug("The registered OnbeforeunloadHandler rejected to load a new page.");
476                     return (P) page;
477                 }
478             }
479         }
480 
481         if (LOG.isDebugEnabled()) {
482             LOG.debug("Get page for window named '" + webWindow.getName() + "', using " + webRequest);
483         }
484 
485         WebResponse webResponse;
486         final String protocol = webRequest.getUrl().getProtocol();
487         if ("javascript".equals(protocol)) {
488             webResponse = makeWebResponseForJavaScriptUrl(webWindow, webRequest.getUrl(), webRequest.getCharset());
489             if (webWindow.getEnclosedPage() != null && webWindow.getEnclosedPage().getWebResponse() == webResponse) {
490                 // a javascript:... url with result of type undefined didn't changed the page
491                 return (P) webWindow.getEnclosedPage();
492             }
493         }
494         else {
495             try {
496                 webResponse = loadWebResponse(webRequest);
497             }
498             catch (final NoHttpResponseException e) {
499                 webResponse = new WebResponse(RESPONSE_DATA_NO_HTTP_RESPONSE, webRequest, 0);
500             }
501         }
502 
503         printContentIfNecessary(webResponse);
504         loadWebResponseInto(webResponse, webWindow);
505 
506         // start execution here
507         // note: we have to do this also if the server reports an error!
508         //       e.g. if the server returns a 404 error page that includes javascript
509         if (scriptEngine_ != null) {
510             scriptEngine_.registerWindowAndMaybeStartEventLoop(webWindow);
511         }
512 
513         // check and report problems if needed
514         throwFailingHttpStatusCodeExceptionIfNecessary(webResponse);
515         return (P) webWindow.getEnclosedPage();
516     }
517 
518     /**
519      * Convenient method to build a URL and load it into the current WebWindow as it would be done
520      * by {@link #getPage(WebWindow, WebRequest)}.
521      * @param url the URL of the new content; in contrast to real browsers plain file url's are not supported.
522      *        You have to use the 'file', 'data', 'blob', 'http' or 'https' protocol.
523      * @param <P> the page type
524      * @return the new page
525      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
526      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
527      * @throws IOException if an IO problem occurs
528      * @throws MalformedURLException if no URL can be created from the provided string
529      */
530     public <P extends Page> P getPage(final String url) throws IOException, FailingHttpStatusCodeException,
531         MalformedURLException {
532         return getPage(UrlUtils.toUrlUnsafe(url));
533     }
534 
535     /**
536      * Convenient method to load a URL into the current top WebWindow as it would be done
537      * by {@link #getPage(WebWindow, WebRequest)}.
538      * @param url the URL of the new content; in contrast to real browsers plain file url's are not supported.
539      *        You have to use the 'file', 'data', 'blob', 'http' or 'https' protocol.
540      * @param <P> the page type
541      * @return the new page
542      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
543      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
544      * @throws IOException if an IO problem occurs
545      */
546     public <P extends Page> P getPage(final URL url) throws IOException, FailingHttpStatusCodeException {
547         final WebRequest request = new WebRequest(url, getBrowserVersion().getHtmlAcceptHeader(),
548                                                           getBrowserVersion().getAcceptEncodingHeader());
549         request.setCharset(UTF_8);
550         return getPage(getCurrentWindow().getTopWindow(), request);
551     }
552 
553     /**
554      * Convenient method to load a web request into the current top WebWindow.
555      * @param request the request parameters
556      * @param <P> the page type
557      * @return the new page
558      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
559      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
560      * @throws IOException if an IO problem occurs
561      * @see #getPage(WebWindow,WebRequest)
562      */
563     public <P extends Page> P getPage(final WebRequest request) throws IOException,
564         FailingHttpStatusCodeException {
565         return getPage(getCurrentWindow().getTopWindow(), request);
566     }
567 
568     /**
569      * <p>Creates a page based on the specified response and inserts it into the specified window. All page
570      * initialization and event notification is handled here.</p>
571      *
572      * <p>Note that if the page created is an attachment page, and an {@link AttachmentHandler} has been
573      * registered with this client, the page is <b>not</b> loaded into the specified window; in this case,
574      * the page is loaded into a new window, and attachment handling is delegated to the registered
575      * <code>AttachmentHandler</code>.</p>
576      *
577      * @param webResponse the response that will be used to create the new page
578      * @param webWindow the window that the new page will be placed within
579      * @throws IOException if an IO error occurs
580      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
581      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
582      * @return the newly created page
583      * @see #setAttachmentHandler(AttachmentHandler)
584      */
585     public Page loadWebResponseInto(final WebResponse webResponse, final WebWindow webWindow)
586         throws IOException, FailingHttpStatusCodeException {
587         return loadWebResponseInto(webResponse, webWindow, null);
588     }
589 
590     /**
591      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
592      *
593      * <p>Creates a page based on the specified response and inserts it into the specified window. All page
594      * initialization and event notification is handled here.</p>
595      *
596      * <p>Note that if the page created is an attachment page, and an {@link AttachmentHandler} has been
597      * registered with this client, the page is <b>not</b> loaded into the specified window; in this case,
598      * the page is loaded into a new window, and attachment handling is delegated to the registered
599      * <code>AttachmentHandler</code>.</p>
600      *
601      * @param webResponse the response that will be used to create the new page
602      * @param webWindow the window that the new page will be placed within
603      * @param forceAttachmentWithFilename if not {@code null}, handle this as an attachment with the specified name
604      *        or if an empty string ("") use the filename provided in the response
605      * @throws IOException if an IO error occurs
606      * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property
607      *         {@link WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)} is set to true
608      * @return the newly created page
609      * @see #setAttachmentHandler(AttachmentHandler)
610      */
611     public Page loadWebResponseInto(final WebResponse webResponse, final WebWindow webWindow,
612             String forceAttachmentWithFilename)
613             throws IOException, FailingHttpStatusCodeException {
614         WebAssert.notNull("webResponse", webResponse);
615         WebAssert.notNull("webWindow", webWindow);
616 
617         if (webResponse.getStatusCode() == HttpStatus.NO_CONTENT_204) {
618             return webWindow.getEnclosedPage();
619         }
620 
621         if (webStartHandler_ != null && "application/x-java-jnlp-file".equals(webResponse.getContentType())) {
622             webStartHandler_.handleJnlpResponse(webResponse);
623             return webWindow.getEnclosedPage();
624         }
625 
626         if (attachmentHandler_ != null
627                 && (forceAttachmentWithFilename != null || attachmentHandler_.isAttachment(webResponse))) {
628 
629             // check content disposition header for nothing provided
630             if (StringUtils.isEmptyOrNull(forceAttachmentWithFilename)) {
631                 final String disp = webResponse.getResponseHeaderValue(HttpHeader.CONTENT_DISPOSITION);
632                 forceAttachmentWithFilename = Attachment.getSuggestedFilename(disp);
633             }
634 
635             if (attachmentHandler_.handleAttachment(webResponse,
636                         StringUtils.isEmptyOrNull(forceAttachmentWithFilename) ? null : forceAttachmentWithFilename)) {
637                 // the handling is done by the attachment handler;
638                 // do not open a new window
639                 return webWindow.getEnclosedPage();
640             }
641 
642             final WebWindow w = openWindow(null, null, webWindow);
643             final Page page = pageCreator_.createPage(webResponse, w);
644             attachmentHandler_.handleAttachment(page,
645                                 StringUtils.isEmptyOrNull(forceAttachmentWithFilename)
646                                         ? null : forceAttachmentWithFilename);
647             return page;
648         }
649 
650         final Page oldPage = webWindow.getEnclosedPage();
651         if (oldPage != null) {
652             // Remove the old page before create new one.
653             oldPage.cleanUp();
654         }
655 
656         Page newPage = null;
657         FrameWindow.PageDenied pageDenied = PageDenied.NONE;
658         if (windows_.contains(webWindow)) {
659             if (webWindow instanceof FrameWindow) {
660                 final String contentSecurityPolicy =
661                         webResponse.getResponseHeaderValue(HttpHeader.CONTENT_SECURIRY_POLICY);
662                 if (StringUtils.isNotBlank(contentSecurityPolicy)) {
663                     final URL origin = UrlUtils.getUrlWithoutPathRefQuery(
664                             ((FrameWindow) webWindow).getEnclosingPage().getUrl());
665                     final URL source = UrlUtils.getUrlWithoutPathRefQuery(webResponse.getWebRequest().getUrl());
666                     final Policy policy = Policy.parseSerializedCSP(contentSecurityPolicy,
667                                                     Policy.PolicyErrorConsumer.ignored);
668                     if (!policy.allowsFrameAncestor(
669                             Optional.of(URI.parseURI(source.toExternalForm()).orElse(null)),
670                             Optional.of(URI.parseURI(origin.toExternalForm()).orElse(null)))) {
671                         pageDenied = PageDenied.BY_CONTENT_SECURIRY_POLICY;
672 
673                         if (LOG.isWarnEnabled()) {
674                             LOG.warn("Load denied by Content-Security-Policy: '" + contentSecurityPolicy + "' - "
675                                     + webResponse.getWebRequest().getUrl() + "' does not permit framing.");
676                         }
677                     }
678                 }
679 
680                 if (pageDenied == PageDenied.NONE) {
681                     final String xFrameOptions = webResponse.getResponseHeaderValue(HttpHeader.X_FRAME_OPTIONS);
682                     if ("DENY".equalsIgnoreCase(xFrameOptions)) {
683                         pageDenied = PageDenied.BY_X_FRAME_OPTIONS;
684 
685                         if (LOG.isWarnEnabled()) {
686                             LOG.warn("Load denied by X-Frame-Options: DENY; - '"
687                                     + webResponse.getWebRequest().getUrl() + "' does not permit framing.");
688                         }
689                     }
690                 }
691             }
692 
693             if (pageDenied == PageDenied.NONE) {
694                 newPage = pageCreator_.createPage(webResponse, webWindow);
695             }
696             else {
697                 try {
698                     final WebResponse aboutBlank = loadWebResponse(WebRequest.newAboutBlankRequest());
699                     newPage = pageCreator_.createPage(aboutBlank, webWindow);
700                     // TODO - maybe we have to attach to original request/response to the page
701 
702                     ((FrameWindow) webWindow).setPageDenied(pageDenied);
703                 }
704                 catch (final IOException ignored) {
705                     // ignore
706                 }
707             }
708 
709             if (windows_.contains(webWindow)) {
710                 fireWindowContentChanged(new WebWindowEvent(webWindow, WebWindowEvent.CHANGE, oldPage, newPage));
711 
712                 // The page being loaded may already have been replaced by another page via JavaScript code.
713                 if (webWindow.getEnclosedPage() == newPage) {
714                     newPage.initialize();
715                     // hack: onload should be fired the same way for all type of pages
716                     // here is a hack to handle non HTML pages
717                     if (isJavaScriptEnabled()
718                             && webWindow instanceof FrameWindow && !newPage.isHtmlPage()) {
719                         final FrameWindow fw = (FrameWindow) webWindow;
720                         final BaseFrameElement frame = fw.getFrameElement();
721                         if (frame.hasEventHandlers("onload")) {
722                             if (LOG.isDebugEnabled()) {
723                                 LOG.debug("Executing onload handler for " + frame);
724                             }
725                             final Event event = new Event(frame, Event.TYPE_LOAD);
726                             ((Node) frame.getScriptableObject()).executeEventLocally(event);
727                         }
728                     }
729                 }
730             }
731         }
732         return newPage;
733     }
734 
735     /**
736      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
737      *
738      * <p>Logs the response's content if its status code indicates a request failure and
739      * {@link WebClientOptions#isPrintContentOnFailingStatusCode()} returns {@code true}.
740      *
741      * @param webResponse the response whose content may be logged
742      */
743     public void printContentIfNecessary(final WebResponse webResponse) {
744         if (getOptions().isPrintContentOnFailingStatusCode()
745                 && !webResponse.isSuccess() && LOG.isInfoEnabled()) {
746             final String contentType = webResponse.getContentType();
747             LOG.info("statusCode=[" + webResponse.getStatusCode() + "] contentType=[" + contentType + "]");
748             LOG.info(webResponse.getContentAsString());
749         }
750     }
751 
752     /**
753      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
754      *
755      * <p>Throws a {@link FailingHttpStatusCodeException} if the request's status code indicates a request
756      * failure and {@link WebClientOptions#isThrowExceptionOnFailingStatusCode()} returns {@code true}.
757      *
758      * @param webResponse the response which may trigger a {@link FailingHttpStatusCodeException}
759      */
760     public void throwFailingHttpStatusCodeExceptionIfNecessary(final WebResponse webResponse) {
761         if (getOptions().isThrowExceptionOnFailingStatusCode() && !webResponse.isSuccessOrUseProxyOrNotModified()) {
762             throw new FailingHttpStatusCodeException(webResponse);
763         }
764     }
765 
766     /**
767      * Adds a header which will be sent with EVERY request from this client.
768      * This list is empty per default; use this to add specific headers for your
769      * case.
770      * @param name the name of the header to add
771      * @param value the value of the header to add
772      * @see #removeRequestHeader(String)
773      */
774     public void addRequestHeader(final String name, final String value) {
775         if (HttpHeader.COOKIE_LC.equalsIgnoreCase(name)) {
776             throw new IllegalArgumentException("Do not add 'Cookie' header, use .getCookieManager() instead");
777         }
778         requestHeaders_.put(name, value);
779     }
780 
781     /**
782      * Removes a header from being sent with EVERY request from this client.
783      * This list is empty per default; use this method to remove specific headers
784      * your have added using {{@link #addRequestHeader(String, String)} before.<br>
785      * You can't use this to avoid sending standard headers like "Accept-Language"
786      * or "Sec-Fetch-Dest".
787      * @param name the name of the header to remove
788      * @see #addRequestHeader
789      */
790     public void removeRequestHeader(final String name) {
791         requestHeaders_.remove(name);
792     }
793 
794     /**
795      * Sets the credentials provider that will provide authentication information when
796      * trying to access protected information on a web server. This information is
797      * required when the server is using Basic HTTP authentication, NTLM authentication,
798      * or Digest authentication.
799      * @param credentialsProvider the new credentials provider to use to authenticate
800      */
801     public void setCredentialsProvider(final CredentialsProvider credentialsProvider) {
802         WebAssert.notNull("credentialsProvider", credentialsProvider);
803         credentialsProvider_ = credentialsProvider;
804     }
805 
806     /**
807      * Returns the credentials provider for this client instance. By default, this
808      * method returns an instance of {@link DefaultCredentialsProvider}.
809      * @return the credentials provider for this client instance
810      */
811     public CredentialsProvider getCredentialsProvider() {
812         return credentialsProvider_;
813     }
814 
815     /**
816      * This method is intended for testing only - use at your own risk.
817      * @return the current JavaScript engine (never {@code null})
818      */
819     public AbstractJavaScriptEngine<?> getJavaScriptEngine() {
820         return scriptEngine_;
821     }
822 
823     /**
824      * This method is intended for testing only - use at your own risk.
825      *
826      * @param engine the new script engine to use
827      */
828     public void setJavaScriptEngine(final AbstractJavaScriptEngine<?> engine) {
829         if (engine == null) {
830             throw new IllegalArgumentException("Can't set JavaScriptEngine to null");
831         }
832         scriptEngine_ = engine;
833     }
834 
835     /**
836      * Returns the cookie manager used by this web client.
837      * @return the cookie manager used by this web client
838      */
839     public CookieManager getCookieManager() {
840         return cookieManager_;
841     }
842 
843     /**
844      * Sets the cookie manager used by this web client.
845      * @param cookieManager the cookie manager used by this web client
846      */
847     public void setCookieManager(final CookieManager cookieManager) {
848         WebAssert.notNull("cookieManager", cookieManager);
849         cookieManager_ = cookieManager;
850     }
851 
852     /**
853      * Sets the alert handler for this webclient.
854      * @param alertHandler the new alerthandler or null if none is specified
855      */
856     public void setAlertHandler(final AlertHandler alertHandler) {
857         alertHandler_ = alertHandler;
858     }
859 
860     /**
861      * Returns the alert handler for this webclient.
862      * @return the alert handler or null if one hasn't been set
863      */
864     public AlertHandler getAlertHandler() {
865         return alertHandler_;
866     }
867 
868     /**
869      * Sets the handler that will be executed when the JavaScript method Window.confirm() is called.
870      * @param handler the new handler or null if no handler is to be used
871      */
872     public void setConfirmHandler(final ConfirmHandler handler) {
873         confirmHandler_ = handler;
874     }
875 
876     /**
877      * Returns the confirm handler.
878      * @return the confirm handler or null if one hasn't been set
879      */
880     public ConfirmHandler getConfirmHandler() {
881         return confirmHandler_;
882     }
883 
884     /**
885      * Sets the handler that will be executed when the JavaScript method Window.prompt() is called.
886      * @param handler the new handler or null if no handler is to be used
887      */
888     public void setPromptHandler(final PromptHandler handler) {
889         promptHandler_ = handler;
890     }
891 
892     /**
893      * Returns the prompt handler.
894      * @return the prompt handler or null if one hasn't been set
895      */
896     public PromptHandler getPromptHandler() {
897         return promptHandler_;
898     }
899 
900     /**
901      * Sets the status handler for this webclient.
902      * @param statusHandler the new status handler or null if none is specified
903      */
904     public void setStatusHandler(final StatusHandler statusHandler) {
905         statusHandler_ = statusHandler;
906     }
907 
908     /**
909      * Returns the status handler for this {@link WebClient}.
910      * @return the status handler or null if one hasn't been set
911      */
912     public StatusHandler getStatusHandler() {
913         return statusHandler_;
914     }
915 
916     /**
917      * Returns the executor for this {@link WebClient}.
918      * @return the executor
919      */
920     public synchronized Executor getExecutor() {
921         if (executor_ == null) {
922             final ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
923             threadPoolExecutor.setThreadFactory(new ThreadNamingFactory(threadPoolExecutor.getThreadFactory()));
924             // threadPoolExecutor.prestartAllCoreThreads();
925             executor_ = threadPoolExecutor;
926         }
927 
928         return executor_;
929     }
930 
931     /**
932      * Changes the ExecutorService for this {@link WebClient}.
933      * You have to call this before the first use of the executor, otherwise
934      * an IllegalStateExceptions is thrown.
935      * @param executor the new Executor.
936      */
937     public synchronized void setExecutor(final ExecutorService executor) {
938         if (executor_ != null) {
939             throw new IllegalStateException("Can't change the executor after first use.");
940         }
941 
942         executor_ = executor;
943     }
944 
945     /**
946      * Sets the javascript error listener for this {@link WebClient}.
947      * When setting to null, the {@link DefaultJavaScriptErrorListener} is used.
948      * @param javaScriptErrorListener the new JavaScriptErrorListener or null if none is specified
949      */
950     public void setJavaScriptErrorListener(final JavaScriptErrorListener javaScriptErrorListener) {
951         if (javaScriptErrorListener == null) {
952             javaScriptErrorListener_ = new DefaultJavaScriptErrorListener();
953         }
954         else {
955             javaScriptErrorListener_ = javaScriptErrorListener;
956         }
957     }
958 
959     /**
960      * Returns the javascript error listener for this {@link WebClient}.
961      * @return the javascript error listener or null if one hasn't been set
962      */
963     public JavaScriptErrorListener getJavaScriptErrorListener() {
964         return javaScriptErrorListener_;
965     }
966 
967     /**
968      * Returns the current browser version.
969      * @return the current browser version
970      */
971     public BrowserVersion getBrowserVersion() {
972         return browserVersion_;
973     }
974 
975     /**
976      * Returns the "current" window for this client. This window (or its top window) will be used
977      * when <code>getPage(...)</code> is called without specifying a window.
978      * @return the "current" window for this client
979      */
980     public WebWindow getCurrentWindow() {
981         return currentWindow_;
982     }
983 
984     /**
985      * Sets the "current" window for this client. This is the window that will be used when
986      * <code>getPage(...)</code> is called without specifying a window.
987      * @param window the new "current" window for this client
988      */
989     public void setCurrentWindow(final WebWindow window) {
990         WebAssert.notNull("window", window);
991         if (currentWindow_ == window) {
992             return;
993         }
994         // onBlur event is triggered for focused element of old current window
995         if (currentWindow_ != null && !currentWindow_.isClosed()) {
996             final Page enclosedPage = currentWindow_.getEnclosedPage();
997             if (enclosedPage != null && enclosedPage.isHtmlPage()) {
998                 final DomElement focusedElement = ((HtmlPage) enclosedPage).getFocusedElement();
999                 if (focusedElement != null) {
1000                     focusedElement.fireEvent(Event.TYPE_BLUR);
1001                 }
1002             }
1003         }
1004         currentWindow_ = window;
1005 
1006         // when marking an iframe window as current we have no need to move the focus
1007         final boolean isIFrame = currentWindow_ instanceof FrameWindow
1008                 && ((FrameWindow) currentWindow_).getFrameElement() instanceof HtmlInlineFrame;
1009         if (!isIFrame) {
1010             //1. activeElement becomes focused element for new current window
1011             //2. onFocus event is triggered for focusedElement of new current window
1012             final Page enclosedPage = currentWindow_.getEnclosedPage();
1013             if (enclosedPage != null && enclosedPage.isHtmlPage()) {
1014                 final HtmlPage enclosedHtmlPage = (HtmlPage) enclosedPage;
1015                 final HtmlElement activeElement = enclosedHtmlPage.getActiveElement();
1016                 if (activeElement != null) {
1017                     enclosedHtmlPage.setFocusedElement(activeElement, true);
1018                 }
1019             }
1020         }
1021     }
1022 
1023     /**
1024      * Adds a listener for {@link WebWindowEvent}s. All events from all windows associated with this
1025      * client will be sent to the specified listener.
1026      * @param listener a listener
1027      */
1028     public void addWebWindowListener(final WebWindowListener listener) {
1029         WebAssert.notNull("listener", listener);
1030         webWindowListeners_.add(listener);
1031     }
1032 
1033     /**
1034      * Removes a listener for {@link WebWindowEvent}s.
1035      * @param listener a listener
1036      */
1037     public void removeWebWindowListener(final WebWindowListener listener) {
1038         WebAssert.notNull("listener", listener);
1039         webWindowListeners_.remove(listener);
1040     }
1041 
1042     private void fireWindowContentChanged(final WebWindowEvent event) {
1043         if (currentWindowTracker_ != null) {
1044             currentWindowTracker_.webWindowContentChanged(event);
1045         }
1046         for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
1047             listener.webWindowContentChanged(event);
1048         }
1049     }
1050 
1051     private void fireWindowOpened(final WebWindowEvent event) {
1052         if (currentWindowTracker_ != null) {
1053             currentWindowTracker_.webWindowOpened(event);
1054         }
1055         for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
1056             listener.webWindowOpened(event);
1057         }
1058     }
1059 
1060     private void fireWindowClosed(final WebWindowEvent event) {
1061         if (currentWindowTracker_ != null) {
1062             currentWindowTracker_.webWindowClosed(event);
1063         }
1064 
1065         for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
1066             listener.webWindowClosed(event);
1067         }
1068 
1069         // to open a new top level window if all others are gone
1070         if (currentWindowTracker_ != null) {
1071             currentWindowTracker_.afterWebWindowClosedListenersProcessed(event);
1072         }
1073     }
1074 
1075     /**
1076      * Open a new window with the specified name. If the URL is non-null then attempt to load
1077      * a page from that location and put it in the new window.
1078      *
1079      * @param url the URL to load content from or null if no content is to be loaded
1080      * @param windowName the name of the new window
1081      * @return the new window
1082      */
1083     public WebWindow openWindow(final URL url, final String windowName) {
1084         WebAssert.notNull("windowName", windowName);
1085         return openWindow(url, windowName, getCurrentWindow());
1086     }
1087 
1088     /**
1089      * Open a new window with the specified name. If the URL is non-null then attempt to load
1090      * a page from that location and put it in the new window.
1091      *
1092      * @param url the URL to load content from or null if no content is to be loaded
1093      * @param windowName the name of the new window
1094      * @param opener the web window that is calling openWindow
1095      * @return the new window
1096      */
1097     public WebWindow openWindow(final URL url, final String windowName, final WebWindow opener) {
1098         final WebWindow window = openTargetWindow(opener, windowName, TARGET_BLANK);
1099         if (url == null) {
1100             initializeEmptyWindow(window, window.getEnclosedPage());
1101         }
1102         else {
1103             try {
1104                 final WebRequest request = new WebRequest(url, getBrowserVersion().getHtmlAcceptHeader(),
1105                                                                 getBrowserVersion().getAcceptEncodingHeader());
1106                 request.setCharset(UTF_8);
1107 
1108                 final Page openerPage = opener.getEnclosedPage();
1109                 if (openerPage != null && openerPage.getUrl() != null) {
1110                     request.setRefererHeader(openerPage.getUrl());
1111                 }
1112                 getPage(window, request);
1113             }
1114             catch (final IOException e) {
1115                 LOG.error("Error loading content into window", e);
1116             }
1117         }
1118         return window;
1119     }
1120 
1121     /**
1122      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1123      *
1124      * Open the window with the specified name. The name may be a special
1125      * target name of _self, _parent, _top, or _blank. An empty or null
1126      * name is set to the default. The special target names are relative to
1127      * the opener window.
1128      *
1129      * @param opener the web window that is calling openWindow
1130      * @param windowName the name of the new window
1131      * @param defaultName the default target if no name is given
1132      * @return the new window
1133      */
1134     public WebWindow openTargetWindow(
1135             final WebWindow opener, final String windowName, final String defaultName) {
1136 
1137         WebAssert.notNull("opener", opener);
1138         WebAssert.notNull("defaultName", defaultName);
1139 
1140         String windowToOpen = windowName;
1141         if (windowToOpen == null || windowToOpen.isEmpty()) {
1142             windowToOpen = defaultName;
1143         }
1144 
1145         WebWindow webWindow = resolveWindow(opener, windowToOpen);
1146 
1147         if (webWindow == null) {
1148             if (TARGET_BLANK.equals(windowToOpen)) {
1149                 windowToOpen = "";
1150             }
1151             webWindow = new TopLevelWindow(windowToOpen, this);
1152         }
1153 
1154         if (webWindow instanceof TopLevelWindow && webWindow != opener.getTopWindow()) {
1155             ((TopLevelWindow) webWindow).setOpener(opener);
1156         }
1157 
1158         return webWindow;
1159     }
1160 
1161     private WebWindow resolveWindow(final WebWindow opener, final String name) {
1162         if (name == null || name.isEmpty() || TARGET_SELF.equals(name)) {
1163             return opener;
1164         }
1165 
1166         if (TARGET_PARENT.equals(name)) {
1167             return opener.getParentWindow();
1168         }
1169 
1170         if (TARGET_TOP.equals(name)) {
1171             return opener.getTopWindow();
1172         }
1173 
1174         if (TARGET_BLANK.equals(name)) {
1175             return null;
1176         }
1177 
1178         // first search for frame windows inside our window hierarchy
1179         WebWindow window = opener;
1180         while (true) {
1181             final Page page = window.getEnclosedPage();
1182             if (page != null && page.isHtmlPage()) {
1183                 try {
1184                     final FrameWindow frame = ((HtmlPage) page).getFrameByName(name);
1185                     final HtmlUnitScriptable scriptable = frame.getFrameElement().getScriptableObject();
1186                     if (scriptable instanceof HTMLIFrameElement) {
1187                         ((HTMLIFrameElement) scriptable).onRefresh();
1188                     }
1189                     return frame;
1190                 }
1191                 catch (final ElementNotFoundException expected) {
1192                     // Fall through
1193                 }
1194             }
1195 
1196             if (window == window.getParentWindow()) {
1197                 // TODO: should getParentWindow() return null on top windows?
1198                 break;
1199             }
1200             window = window.getParentWindow();
1201         }
1202 
1203         try {
1204             return getWebWindowByName(name);
1205         }
1206         catch (final WebWindowNotFoundException expected) {
1207             // Fall through - a new window will be created below
1208         }
1209         return null;
1210     }
1211 
1212     /**
1213      * <p><span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span></p>
1214      *
1215      * Opens a new dialog window.
1216      * @param url the URL of the document to load and display
1217      * @param opener the web window that is opening the dialog
1218      * @param dialogArguments the object to make available inside the dialog via <code>window.dialogArguments</code>
1219      * @return the new dialog window
1220      * @throws IOException if there is an IO error
1221      */
1222     public DialogWindow openDialogWindow(final URL url, final WebWindow opener, final Object dialogArguments)
1223         throws IOException {
1224 
1225         WebAssert.notNull("url", url);
1226         WebAssert.notNull("opener", opener);
1227 
1228         final DialogWindow window = new DialogWindow(this, dialogArguments);
1229 
1230         final HtmlPage openerPage = (HtmlPage) opener.getEnclosedPage();
1231         final WebRequest request = new WebRequest(url, getBrowserVersion().getHtmlAcceptHeader(),
1232                                                         getBrowserVersion().getAcceptEncodingHeader());
1233         request.setCharset(UTF_8);
1234 
1235         if (openerPage != null) {
1236             request.setRefererHeader(openerPage.getUrl());
1237         }
1238 
1239         getPage(window, request);
1240 
1241         return window;
1242     }
1243 
1244     /**
1245      * Sets the object that will be used to create pages. Set this if you want
1246      * to customize the type of page that is returned for a given content type.
1247      *
1248      * @param pageCreator the new page creator
1249      */
1250     public void setPageCreator(final PageCreator pageCreator) {
1251         WebAssert.notNull("pageCreator", pageCreator);
1252         pageCreator_ = pageCreator;
1253     }
1254 
1255     /**
1256      * Returns the current page creator.
1257      *
1258      * @return the page creator
1259      */
1260     public PageCreator getPageCreator() {
1261         return pageCreator_;
1262     }
1263 
1264     /**
1265      * Returns the first {@link WebWindow} that matches the specified name.
1266      *
1267      * @param name the name to search for
1268      * @return the {@link WebWindow} with the specified name
1269      * @throws WebWindowNotFoundException if the {@link WebWindow} can't be found
1270      * @see #getWebWindows()
1271      * @see #getTopLevelWindows()
1272      */
1273     public WebWindow getWebWindowByName(final String name) throws WebWindowNotFoundException {
1274         WebAssert.notNull("name", name);
1275 
1276         for (final WebWindow webWindow : windows_) {
1277             if (name.equals(webWindow.getName())) {
1278                 return webWindow;
1279             }
1280         }
1281 
1282         throw new WebWindowNotFoundException(name);
1283     }
1284 
1285     /**
1286      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1287      *
1288      * Initializes a new web window for JavaScript.
1289      * @param webWindow the new WebWindow
1290      * @param page the page that will become the enclosing page
1291      */
1292     public void initialize(final WebWindow webWindow, final Page page) {
1293         WebAssert.notNull("webWindow", webWindow);
1294 
1295         if (isJavaScriptEngineEnabled()) {
1296             scriptEngine_.initialize(webWindow, page);
1297         }
1298     }
1299 
1300     /**
1301      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1302      *
1303      * Initializes a new empty window for JavaScript.
1304      *
1305      * @param webWindow the new WebWindow
1306      * @param page the page that will become the enclosing page
1307      */
1308     public void initializeEmptyWindow(final WebWindow webWindow, final Page page) {
1309         WebAssert.notNull("webWindow", webWindow);
1310 
1311         if (isJavaScriptEngineEnabled()) {
1312             initialize(webWindow, page);
1313             ((Window) webWindow.getScriptableObject()).initialize();
1314         }
1315     }
1316 
1317     /**
1318      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1319      *
1320      * Adds a new window to the list of available windows.
1321      *
1322      * @param webWindow the new WebWindow
1323      */
1324     public void registerWebWindow(final WebWindow webWindow) {
1325         WebAssert.notNull("webWindow", webWindow);
1326         if (windows_.add(webWindow)) {
1327             fireWindowOpened(new WebWindowEvent(webWindow, WebWindowEvent.OPEN, webWindow.getEnclosedPage(), null));
1328         }
1329         // register JobManager here but don't deregister in deregisterWebWindow as it can live longer
1330         jobManagers_.add(new WeakReference<>(webWindow.getJobManager()));
1331     }
1332 
1333     /**
1334      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1335      *
1336      * Removes a window from the list of available windows.
1337      *
1338      * @param webWindow the window to remove
1339      */
1340     public void deregisterWebWindow(final WebWindow webWindow) {
1341         WebAssert.notNull("webWindow", webWindow);
1342         if (windows_.remove(webWindow)) {
1343             fireWindowClosed(new WebWindowEvent(webWindow, WebWindowEvent.CLOSE, webWindow.getEnclosedPage(), null));
1344         }
1345     }
1346 
1347     /**
1348      * Expands a relative URL relative to the specified base. In most situations
1349      * this is the same as <code>new URL(baseUrl, relativeUrl)</code> but
1350      * there are some cases that URL doesn't handle correctly. See
1351      * <a href="http://www.faqs.org/rfcs/rfc1808.html">RFC1808</a>
1352      * regarding Relative Uniform Resource Locators for more information.
1353      *
1354      * @param baseUrl the base URL
1355      * @param relativeUrl the relative URL
1356      * @return the expansion of the specified base and relative URLs
1357      * @throws MalformedURLException if an error occurred when creating a URL object
1358      */
1359     public static URL expandUrl(final URL baseUrl, final String relativeUrl) throws MalformedURLException {
1360         final String newUrl = UrlUtils.resolveUrl(baseUrl, relativeUrl);
1361         return UrlUtils.toUrlUnsafe(newUrl);
1362     }
1363 
1364     private WebResponse makeWebResponseForDataUrl(final WebRequest webRequest) throws IOException {
1365         final URL url = webRequest.getUrl();
1366 
1367         final DataURLConnection connection = new DataURLConnection(url);
1368 
1369         final List<NameValuePair> responseHeaders = new ArrayList<>();
1370         responseHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE_LC,
1371             connection.getMediaType() + ";charset=" + connection.getCharset()));
1372 
1373         if (HttpMethod.HEAD.equals(webRequest.getHttpMethod())) {
1374             final WebResponseData data = new WebResponseData(200, "OK", responseHeaders);
1375             return new WebResponse(data, url, webRequest.getHttpMethod(), 0);
1376         }
1377 
1378         try (InputStream is = connection.getInputStream()) {
1379             final DownloadedContent downloadedContent =
1380                     HttpWebConnection.downloadContent(is,
1381                             getOptions().getMaxInMemory(),
1382                             getOptions().getTempFileDirectory());
1383             final WebResponseData data = new WebResponseData(downloadedContent, 200, "OK", responseHeaders);
1384             return new WebResponse(data, url, webRequest.getHttpMethod(), 0);
1385         }
1386     }
1387 
1388     private static WebResponse makeWebResponseForAboutUrl(final WebRequest webRequest) throws MalformedURLException {
1389         final URL url = webRequest.getUrl();
1390         final String urlString = url.toExternalForm();
1391         if (UrlUtils.ABOUT_BLANK.equalsIgnoreCase(urlString)) {
1392             return new StringWebResponse("", UrlUtils.URL_ABOUT_BLANK);
1393         }
1394 
1395         final String urlWithoutQuery = org.apache.commons.lang3.StringUtils.substringBefore(urlString, "?");
1396         if (!"blank".equalsIgnoreCase(org.apache.commons.lang3.StringUtils
1397                                         .substringAfter(urlWithoutQuery, UrlUtils.ABOUT_SCHEME))) {
1398             throw new MalformedURLException(url + " is not supported, only about:blank is supported at the moment.");
1399         }
1400         return new StringWebResponse("", url);
1401     }
1402 
1403     /**
1404      * Builds a WebResponse for a file URL.
1405      * This first implementation is basic.
1406      * It assumes that the file contains an HTML page encoded with the specified encoding.
1407      * @param webRequest the request
1408      * @return the web response
1409      * @throws IOException if an IO problem occurs
1410      */
1411     private WebResponse makeWebResponseForFileUrl(final WebRequest webRequest) throws IOException {
1412         URL cleanUrl = webRequest.getUrl();
1413         if (cleanUrl.getQuery() != null) {
1414             // Get rid of the query portion before trying to load the file.
1415             cleanUrl = UrlUtils.getUrlWithNewQuery(cleanUrl, null);
1416         }
1417         if (cleanUrl.getRef() != null) {
1418             // Get rid of the ref portion before trying to load the file.
1419             cleanUrl = UrlUtils.getUrlWithNewRef(cleanUrl, null);
1420         }
1421 
1422         final WebResponse fromCache = getCache().getCachedResponse(webRequest);
1423         if (fromCache != null) {
1424             return new WebResponseFromCache(fromCache, webRequest);
1425         }
1426 
1427         String fileUrl = cleanUrl.toExternalForm();
1428         fileUrl = URLDecoder.decode(fileUrl, UTF_8.name());
1429         final File file = new File(fileUrl.substring(5));
1430         if (!file.exists()) {
1431             // construct 404
1432             final List<NameValuePair> compiledHeaders = new ArrayList<>();
1433             compiledHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE, MimeType.TEXT_HTML));
1434             final WebResponseData responseData =
1435                 new WebResponseData(
1436                         StringUtils
1437                             .toByteArray("File: " + file.getAbsolutePath(), UTF_8),
1438                     404, "Not Found", compiledHeaders);
1439             return new WebResponse(responseData, webRequest, 0);
1440         }
1441 
1442         final String contentType = guessContentType(file);
1443 
1444         final DownloadedContent content = new DownloadedContent.OnFile(file, false);
1445         final List<NameValuePair> compiledHeaders = new ArrayList<>();
1446         compiledHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE, contentType));
1447         compiledHeaders.add(new NameValuePair(HttpHeader.LAST_MODIFIED,
1448                 HttpUtils.formatDate(new Date(file.lastModified()))));
1449         final WebResponseData responseData = new WebResponseData(content, 200, "OK", compiledHeaders);
1450         final WebResponse webResponse = new WebResponse(responseData, webRequest, 0);
1451         getCache().cacheIfPossible(webRequest, webResponse, null);
1452         return webResponse;
1453     }
1454 
1455     private WebResponse makeWebResponseForBlobUrl(final WebRequest webRequest) {
1456         final Window window = getCurrentWindow().getScriptableObject();
1457         final Blob fileOrBlob = window.getDocument().resolveBlobUrl(webRequest.getUrl().toString());
1458         if (fileOrBlob == null) {
1459             throw JavaScriptEngine.typeError("Cannot load data from " + webRequest.getUrl());
1460         }
1461 
1462         final List<NameValuePair> headers = new ArrayList<>();
1463         final String type = fileOrBlob.getType();
1464         if (!StringUtils.isEmptyOrNull(type)) {
1465             headers.add(new NameValuePair(HttpHeader.CONTENT_TYPE, fileOrBlob.getType()));
1466         }
1467         if (fileOrBlob instanceof org.htmlunit.javascript.host.file.File) {
1468             final org.htmlunit.javascript.host.file.File file = (org.htmlunit.javascript.host.file.File) fileOrBlob;
1469             final String fileName = file.getName();
1470             if (!StringUtils.isEmptyOrNull(fileName)) {
1471                 // https://datatracker.ietf.org/doc/html/rfc6266#autoid-10
1472                 headers.add(new NameValuePair(HttpHeader.CONTENT_DISPOSITION, "inline; filename=\"" + fileName + "\""));
1473             }
1474         }
1475 
1476         final DownloadedContent content = new DownloadedContent.InMemory(fileOrBlob.getBytes());
1477         final WebResponseData responseData = new WebResponseData(content, 200, "OK", headers);
1478         return new WebResponse(responseData, webRequest, 0);
1479     }
1480 
1481     /**
1482      * Tries to guess the content type of the file.<br>
1483      * This utility could be located in a helper class but we can compare this functionality
1484      * for instance with the "Helper Applications" settings of Mozilla and therefore see it as a
1485      * property of the "browser".
1486      * @param file the file
1487      * @return "application/octet-stream" if nothing could be guessed
1488      */
1489     public String guessContentType(final File file) {
1490         final String fileName = file.getName();
1491         final String fileNameLC = fileName.toLowerCase(Locale.ROOT);
1492         if (fileNameLC.endsWith(".xhtml")) {
1493             // Java's mime type map returns application/xml in JDK8.
1494             return MimeType.APPLICATION_XHTML;
1495         }
1496 
1497         // Java's mime type map does not know these in JDK8.
1498         if (fileNameLC.endsWith(".js")) {
1499             return MimeType.TEXT_JAVASCRIPT;
1500         }
1501 
1502         if (fileNameLC.endsWith(".css")) {
1503             return MimeType.TEXT_CSS;
1504         }
1505 
1506         String contentType = null;
1507         if (!fileNameLC.endsWith(".php")) {
1508             contentType = URLConnection.guessContentTypeFromName(fileName);
1509         }
1510         if (contentType == null) {
1511             try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
1512                 contentType = URLConnection.guessContentTypeFromStream(inputStream);
1513             }
1514             catch (final IOException ignored) {
1515                 // Ignore silently.
1516             }
1517         }
1518         if (contentType == null) {
1519             contentType = MimeType.APPLICATION_OCTET_STREAM;
1520         }
1521         return contentType;
1522     }
1523 
1524     private WebResponse makeWebResponseForJavaScriptUrl(final WebWindow webWindow, final URL url,
1525         final Charset charset) throws FailingHttpStatusCodeException, IOException {
1526 
1527         HtmlPage page = null;
1528         if (webWindow instanceof FrameWindow) {
1529             final FrameWindow frameWindow = (FrameWindow) webWindow;
1530             page = (HtmlPage) frameWindow.getEnclosedPage();
1531         }
1532         else {
1533             final Page currentPage = webWindow.getEnclosedPage();
1534             if (currentPage instanceof HtmlPage) {
1535                 page = (HtmlPage) currentPage;
1536             }
1537         }
1538 
1539         if (page == null) {
1540             page = getPage(webWindow, WebRequest.newAboutBlankRequest());
1541         }
1542         final ScriptResult r = page.executeJavaScript(url.toExternalForm(), "JavaScript URL", 1);
1543         if (r.getJavaScriptResult() == null || ScriptResult.isUndefined(r)) {
1544             // No new WebResponse to produce.
1545             return webWindow.getEnclosedPage().getWebResponse();
1546         }
1547 
1548         final String contentString = r.getJavaScriptResult().toString();
1549         final StringWebResponse response = new StringWebResponse(contentString, charset, url);
1550         response.setFromJavascript(true);
1551         return response;
1552     }
1553 
1554     /**
1555      * Loads a {@link WebResponse} from the server.
1556      * @param webRequest the request
1557      * @throws IOException if an IO problem occurs
1558      * @return the WebResponse
1559      */
1560     public WebResponse loadWebResponse(final WebRequest webRequest) throws IOException {
1561         final String protocol = webRequest.getUrl().getProtocol();
1562         switch (protocol) {
1563             case UrlUtils.ABOUT:
1564                 return makeWebResponseForAboutUrl(webRequest);
1565 
1566             case "file":
1567                 return makeWebResponseForFileUrl(webRequest);
1568 
1569             case "data":
1570                 return makeWebResponseForDataUrl(webRequest);
1571 
1572             case "blob":
1573                 return makeWebResponseForBlobUrl(webRequest);
1574 
1575             case "http":
1576             case "https":
1577                 return loadWebResponseFromWebConnection(webRequest, ALLOWED_REDIRECTIONS_SAME_URL);
1578 
1579             default:
1580                 throw new IOException("Unsupported protocol '" + protocol + "'");
1581         }
1582     }
1583 
1584     /**
1585      * Loads a {@link WebResponse} from the server through the WebConnection.
1586      * @param webRequest the request
1587      * @param allowedRedirects the number of allowed redirects remaining
1588      * @throws IOException if an IO problem occurs
1589      * @return the resultant {@link WebResponse}
1590      */
1591     private WebResponse loadWebResponseFromWebConnection(final WebRequest webRequest,
1592         final int allowedRedirects) throws IOException {
1593 
1594         URL url = webRequest.getUrl();
1595         final HttpMethod method = webRequest.getHttpMethod();
1596         final List<NameValuePair> parameters = webRequest.getRequestParameters();
1597 
1598         WebAssert.notNull("url", url);
1599         WebAssert.notNull("method", method);
1600         WebAssert.notNull("parameters", parameters);
1601 
1602         url = UrlUtils.encodeUrl(url, webRequest.getCharset());
1603         webRequest.setUrl(url);
1604 
1605         if (LOG.isDebugEnabled()) {
1606             LOG.debug("Load response for " + method + " " + url.toExternalForm());
1607         }
1608 
1609         // If the request settings don't specify a custom proxy, use the default client proxy...
1610         if (webRequest.getProxyHost() == null) {
1611             final ProxyConfig proxyConfig = getOptions().getProxyConfig();
1612             if (proxyConfig.getProxyAutoConfigUrl() != null) {
1613                 if (!UrlUtils.sameFile(new URL(proxyConfig.getProxyAutoConfigUrl()), url)) {
1614                     String content = proxyConfig.getProxyAutoConfigContent();
1615                     if (content == null) {
1616                         content = getPage(proxyConfig.getProxyAutoConfigUrl())
1617                             .getWebResponse().getContentAsString();
1618                         proxyConfig.setProxyAutoConfigContent(content);
1619                     }
1620                     final String allValue = JavaScriptEngine.evaluateProxyAutoConfig(getBrowserVersion(), content, url);
1621                     if (LOG.isDebugEnabled()) {
1622                         LOG.debug("Proxy Auto-Config: value '" + allValue + "' for URL " + url);
1623                     }
1624                     String value = allValue.split(";")[0].trim();
1625                     if (value.startsWith("PROXY")) {
1626                         value = value.substring(6);
1627                         final int colonIndex = value.indexOf(':');
1628                         webRequest.setSocksProxy(false);
1629                         webRequest.setProxyHost(value.substring(0, colonIndex));
1630                         webRequest.setProxyPort(Integer.parseInt(value.substring(colonIndex + 1)));
1631                     }
1632                     else if (value.startsWith("SOCKS")) {
1633                         value = value.substring(6);
1634                         final int colonIndex = value.indexOf(':');
1635                         webRequest.setSocksProxy(true);
1636                         webRequest.setProxyHost(value.substring(0, colonIndex));
1637                         webRequest.setProxyPort(Integer.parseInt(value.substring(colonIndex + 1)));
1638                     }
1639                 }
1640             }
1641             // ...unless the host needs to bypass the configured client proxy!
1642             else if (!proxyConfig.shouldBypassProxy(webRequest.getUrl().getHost())) {
1643                 webRequest.setProxyHost(proxyConfig.getProxyHost());
1644                 webRequest.setProxyPort(proxyConfig.getProxyPort());
1645                 webRequest.setProxyScheme(proxyConfig.getProxyScheme());
1646                 webRequest.setSocksProxy(proxyConfig.isSocksProxy());
1647             }
1648         }
1649 
1650         // Add the headers that are sent with every request.
1651         addDefaultHeaders(webRequest);
1652 
1653         // Retrieve the response, either from the cache or from the server.
1654         final WebResponse fromCache = getCache().getCachedResponse(webRequest);
1655         final WebResponse webResponse = getWebResponseOrUseCached(webRequest, fromCache);
1656 
1657         // Continue according to the HTTP status code.
1658         final int status = webResponse.getStatusCode();
1659         if (status == HttpStatus.USE_PROXY_305) {
1660             getIncorrectnessListener().notify("Ignoring HTTP status code [305] 'Use Proxy'", this);
1661         }
1662         else if (status >= HttpStatus.MOVED_PERMANENTLY_301
1663             && status <= HttpStatus.PERMANENT_REDIRECT_308
1664             && status != HttpStatus.NOT_MODIFIED_304
1665             && getOptions().isRedirectEnabled()) {
1666 
1667             final URL newUrl;
1668             String locationString = null;
1669             try {
1670                 locationString = webResponse.getResponseHeaderValue("Location");
1671                 if (locationString == null) {
1672                     return webResponse;
1673                 }
1674                 locationString = new String(locationString.getBytes(ISO_8859_1), UTF_8);
1675                 newUrl = expandUrl(url, locationString);
1676             }
1677             catch (final MalformedURLException e) {
1678                 getIncorrectnessListener().notify("Got a redirect status code [" + status + " "
1679                     + webResponse.getStatusMessage()
1680                     + "] but the location is not a valid URL [" + locationString
1681                     + "]. Skipping redirection processing.", this);
1682                 return webResponse;
1683             }
1684 
1685             if (LOG.isDebugEnabled()) {
1686                 LOG.debug("Got a redirect status code [" + status + "] new location = [" + locationString + "]");
1687             }
1688 
1689             if (allowedRedirects == 0) {
1690                 throw new FailingHttpStatusCodeException("Too many redirects for "
1691                     + webResponse.getWebRequest().getUrl(), webResponse);
1692             }
1693 
1694             if (status == HttpStatus.MOVED_PERMANENTLY_301
1695                     || status == HttpStatus.FOUND_302
1696                     || status == HttpStatus.SEE_OTHER_303) {
1697                 final WebRequest wrs = new WebRequest(newUrl, HttpMethod.GET);
1698                 wrs.setCharset(webRequest.getCharset());
1699 
1700                 if (HttpMethod.HEAD == webRequest.getHttpMethod()) {
1701                     wrs.setHttpMethod(HttpMethod.HEAD);
1702                 }
1703                 for (final Map.Entry<String, String> entry : webRequest.getAdditionalHeaders().entrySet()) {
1704                     wrs.setAdditionalHeader(entry.getKey(), entry.getValue());
1705                 }
1706                 return loadWebResponseFromWebConnection(wrs, allowedRedirects - 1);
1707             }
1708             else if (status == HttpStatus.TEMPORARY_REDIRECT_307
1709                         || status == HttpStatus.PERMANENT_REDIRECT_308) {
1710                 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307
1711                 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308
1712                 // reuse method and body
1713                 final WebRequest wrs = new WebRequest(newUrl, webRequest.getHttpMethod());
1714                 wrs.setCharset(webRequest.getCharset());
1715                 if (webRequest.getRequestBody() != null) {
1716                     if (HttpMethod.POST == webRequest.getHttpMethod()
1717                             || HttpMethod.PUT == webRequest.getHttpMethod()
1718                             || HttpMethod.PATCH == webRequest.getHttpMethod()) {
1719                         wrs.setRequestBody(webRequest.getRequestBody());
1720                         wrs.setEncodingType(webRequest.getEncodingType());
1721                     }
1722                 }
1723                 else {
1724                     wrs.setRequestParameters(parameters);
1725                 }
1726 
1727                 for (final Map.Entry<String, String> entry : webRequest.getAdditionalHeaders().entrySet()) {
1728                     wrs.setAdditionalHeader(entry.getKey(), entry.getValue());
1729                 }
1730 
1731                 return loadWebResponseFromWebConnection(wrs, allowedRedirects - 1);
1732             }
1733         }
1734 
1735         if (fromCache == null) {
1736             getCache().cacheIfPossible(webRequest, webResponse, null);
1737         }
1738         return webResponse;
1739     }
1740 
1741     /**
1742      * Returns the cached response provided for the request if usable otherwise makes the
1743      * request and returns the response.
1744      * @param webRequest the request
1745      * @param cached a previous cached response for the request, or {@code null}
1746      */
1747     private WebResponse getWebResponseOrUseCached(
1748             final WebRequest webRequest, final WebResponse cached) throws IOException {
1749         if (cached == null) {
1750             return getWebConnection().getResponse(webRequest);
1751         }
1752 
1753         if (!HeaderUtils.containsNoCache(cached)) {
1754             return new WebResponseFromCache(cached, webRequest);
1755         }
1756 
1757         // implementation based on rfc9111 https://www.rfc-editor.org/rfc/rfc9111#name-validation
1758         if (HeaderUtils.containsETag(cached)) {
1759             webRequest.setAdditionalHeader(HttpHeader.IF_NONE_MATCH, cached.getResponseHeaderValue(HttpHeader.ETAG));
1760         }
1761         if (HeaderUtils.containsLastModified(cached)) {
1762             webRequest.setAdditionalHeader(HttpHeader.IF_MODIFIED_SINCE,
1763                     cached.getResponseHeaderValue(HttpHeader.LAST_MODIFIED));
1764         }
1765 
1766         final WebResponse webResponse = getWebConnection().getResponse(webRequest);
1767 
1768         if (webResponse.getStatusCode() >= HttpStatus.INTERNAL_SERVER_ERROR_500) {
1769             return new WebResponseFromCache(cached, webRequest);
1770         }
1771 
1772         if (webResponse.getStatusCode() == HttpStatus.NOT_MODIFIED_304) {
1773             final Map<String, NameValuePair> header2NameValuePair = new LinkedHashMap<>();
1774             for (final NameValuePair pair : cached.getResponseHeaders()) {
1775                 header2NameValuePair.put(pair.getName(), pair);
1776             }
1777             for (final NameValuePair pair : webResponse.getResponseHeaders()) {
1778                 if (preferHeaderFrom304Response(pair.getName())) {
1779                     header2NameValuePair.put(pair.getName(), pair);
1780                 }
1781             }
1782             // WebResponse headers is unmodifiableList so we cannot update it directly
1783             // instead, create a new WebResponseFromCache with updated headers
1784             // then use it to replace the old cached value
1785             final WebResponse updatedCached =
1786                     new WebResponseFromCache(cached, new ArrayList<>(header2NameValuePair.values()), webRequest);
1787             getCache().cacheIfPossible(webRequest, updatedCached, null);
1788             return updatedCached;
1789         }
1790 
1791         getCache().cacheIfPossible(webRequest, webResponse, null);
1792         return webResponse;
1793     }
1794 
1795     /**
1796      * Returns true if the value of the specified header in a 304 Not Modified response should be
1797      * adopted over any previously cached value.
1798      */
1799     private static boolean preferHeaderFrom304Response(final String name) {
1800         final String lcName = name.toLowerCase(Locale.ROOT);
1801         for (final String header : DISCARDING_304_RESPONSE_HEADER_NAMES) {
1802             if (lcName.equals(header)) {
1803                 return false;
1804             }
1805         }
1806         for (final String prefix : DISCARDING_304_HEADER_PREFIXES) {
1807             if (lcName.startsWith(prefix)) {
1808                 return false;
1809             }
1810         }
1811         return true;
1812     }
1813 
1814     /**
1815      * Adds the headers that are sent with every request to the specified {@link WebRequest} instance.
1816      * @param wrs the <code>WebRequestSettings</code> instance to modify
1817      */
1818     private void addDefaultHeaders(final WebRequest wrs) {
1819         // Add user-specified headers to the web request if not present there yet.
1820         requestHeaders_.forEach((name, value) -> {
1821             if (!wrs.isAdditionalHeader(name)) {
1822                 wrs.setAdditionalHeader(name, value);
1823             }
1824         });
1825 
1826         // Add standard HtmlUnit headers to the web request if still not present there yet.
1827         if (!wrs.isAdditionalHeader(HttpHeader.ACCEPT_LANGUAGE)) {
1828             wrs.setAdditionalHeader(HttpHeader.ACCEPT_LANGUAGE, getBrowserVersion().getAcceptLanguageHeader());
1829         }
1830 
1831         if (!wrs.isAdditionalHeader(HttpHeader.SEC_FETCH_DEST)) {
1832             wrs.setAdditionalHeader(HttpHeader.SEC_FETCH_DEST, "document");
1833         }
1834         if (!wrs.isAdditionalHeader(HttpHeader.SEC_FETCH_MODE)) {
1835             wrs.setAdditionalHeader(HttpHeader.SEC_FETCH_MODE, "navigate");
1836         }
1837         if (!wrs.isAdditionalHeader(HttpHeader.SEC_FETCH_SITE)) {
1838             wrs.setAdditionalHeader(HttpHeader.SEC_FETCH_SITE, "same-origin");
1839         }
1840         if (!wrs.isAdditionalHeader(HttpHeader.SEC_FETCH_USER)) {
1841             wrs.setAdditionalHeader(HttpHeader.SEC_FETCH_USER, "?1");
1842         }
1843         if (getBrowserVersion().hasFeature(HTTP_HEADER_PRIORITY)
1844                 && !wrs.isAdditionalHeader(HttpHeader.PRIORITY)) {
1845             wrs.setAdditionalHeader(HttpHeader.PRIORITY, "u=0, i");
1846         }
1847 
1848         if (getBrowserVersion().hasFeature(HTTP_HEADER_CH_UA)
1849                 && !wrs.isAdditionalHeader(HttpHeader.SEC_CH_UA)) {
1850             wrs.setAdditionalHeader(HttpHeader.SEC_CH_UA, getBrowserVersion().getSecClientHintUserAgentHeader());
1851         }
1852         if (getBrowserVersion().hasFeature(HTTP_HEADER_CH_UA)
1853                 && !wrs.isAdditionalHeader(HttpHeader.SEC_CH_UA_MOBILE)) {
1854             wrs.setAdditionalHeader(HttpHeader.SEC_CH_UA_MOBILE, "?0");
1855         }
1856         if (getBrowserVersion().hasFeature(HTTP_HEADER_CH_UA)
1857                 && !wrs.isAdditionalHeader(HttpHeader.SEC_CH_UA_PLATFORM)) {
1858             wrs.setAdditionalHeader(HttpHeader.SEC_CH_UA_PLATFORM,
1859                     getBrowserVersion().getSecClientHintUserAgentPlatformHeader());
1860         }
1861 
1862         if (!wrs.isAdditionalHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS)) {
1863             wrs.setAdditionalHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS, "1");
1864         }
1865     }
1866 
1867     /**
1868      * Returns an immutable list of open web windows (whether they are top level windows or not).
1869      * This is a snapshot; future changes are not reflected by this list.
1870      * <p>
1871      * The list is ordered by age, the oldest one first.
1872      *
1873      * @return an immutable list of open web windows (whether they are top level windows or not)
1874      * @see #getWebWindowByName(String)
1875      * @see #getTopLevelWindows()
1876      */
1877     public List<WebWindow> getWebWindows() {
1878         return Collections.unmodifiableList(new ArrayList<>(windows_));
1879     }
1880 
1881     /**
1882      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1883      *
1884      * Returns true if the list of WebWindows contains the provided one.
1885      * This method is there to improve the performance of some internal checks because
1886      * calling getWebWindows().contains(.) creates some objects without any need.
1887      *
1888      * @param webWindow the window to check
1889      * @return true or false
1890      */
1891     public boolean containsWebWindow(final WebWindow webWindow) {
1892         return windows_.contains(webWindow);
1893     }
1894 
1895     /**
1896      * Returns an immutable list of open top level windows.
1897      * This is a snapshot; future changes are not reflected by this list.
1898      * <p>
1899      * The list is ordered by age, the oldest one first.
1900      *
1901      * @return an immutable list of open top level windows
1902      * @see #getWebWindowByName(String)
1903      * @see #getWebWindows()
1904      */
1905     public List<TopLevelWindow> getTopLevelWindows() {
1906         return Collections.unmodifiableList(new ArrayList<>(topLevelWindows_));
1907     }
1908 
1909     /**
1910      * Sets the handler to be used whenever a refresh is triggered. Refer
1911      * to the documentation for {@link RefreshHandler} for more details.
1912      * @param handler the new handler
1913      */
1914     public void setRefreshHandler(final RefreshHandler handler) {
1915         if (handler == null) {
1916             refreshHandler_ = new NiceRefreshHandler(2);
1917         }
1918         else {
1919             refreshHandler_ = handler;
1920         }
1921     }
1922 
1923     /**
1924      * Returns the current refresh handler.
1925      * The default refresh handler is a {@link NiceRefreshHandler NiceRefreshHandler(2)}.
1926      * @return the current RefreshHandler
1927      */
1928     public RefreshHandler getRefreshHandler() {
1929         return refreshHandler_;
1930     }
1931 
1932     /**
1933      * Sets the script pre processor for this {@link WebClient}.
1934      * @param scriptPreProcessor the new preprocessor or null if none is specified
1935      */
1936     public void setScriptPreProcessor(final ScriptPreProcessor scriptPreProcessor) {
1937         scriptPreProcessor_ = scriptPreProcessor;
1938     }
1939 
1940     /**
1941      * Returns the script pre processor for this {@link WebClient}.
1942      * @return the pre processor or null of one hasn't been set
1943      */
1944     public ScriptPreProcessor getScriptPreProcessor() {
1945         return scriptPreProcessor_;
1946     }
1947 
1948     /**
1949      * Sets the listener for messages generated by the HTML parser.
1950      * @param listener the new listener, {@code null} if messages should be totally ignored
1951      */
1952     public void setHTMLParserListener(final HTMLParserListener listener) {
1953         htmlParserListener_ = listener;
1954     }
1955 
1956     /**
1957      * Gets the configured listener for messages generated by the HTML parser.
1958      * @return {@code null} if no listener is defined (default value)
1959      */
1960     public HTMLParserListener getHTMLParserListener() {
1961         return htmlParserListener_;
1962     }
1963 
1964     /**
1965      * Returns the CSS error handler used by this web client when CSS problems are encountered.
1966      * @return the CSS error handler used by this web client when CSS problems are encountered
1967      * @see DefaultCssErrorHandler
1968      * @see SilentCssErrorHandler
1969      */
1970     public CSSErrorHandler getCssErrorHandler() {
1971         return cssErrorHandler_;
1972     }
1973 
1974     /**
1975      * Sets the CSS error handler used by this web client when CSS problems are encountered.
1976      * @param cssErrorHandler the CSS error handler used by this web client when CSS problems are encountered
1977      * @see DefaultCssErrorHandler
1978      * @see SilentCssErrorHandler
1979      */
1980     public void setCssErrorHandler(final CSSErrorHandler cssErrorHandler) {
1981         WebAssert.notNull("cssErrorHandler", cssErrorHandler);
1982         cssErrorHandler_ = cssErrorHandler;
1983     }
1984 
1985     /**
1986      * Sets the number of milliseconds that a script is allowed to execute before being terminated.
1987      * A value of 0 or less means no timeout.
1988      *
1989      * @param timeout the timeout value, in milliseconds
1990      */
1991     public void setJavaScriptTimeout(final long timeout) {
1992         scriptEngine_.setJavaScriptTimeout(timeout);
1993     }
1994 
1995     /**
1996      * Returns the number of milliseconds that a script is allowed to execute before being terminated.
1997      * A value of 0 or less means no timeout.
1998      *
1999      * @return the timeout value, in milliseconds
2000      */
2001     public long getJavaScriptTimeout() {
2002         return scriptEngine_.getJavaScriptTimeout();
2003     }
2004 
2005     /**
2006      * Gets the current listener for encountered incorrectness (except HTML parsing messages that
2007      * are handled by the HTML parser listener). Default value is an instance of
2008      * {@link IncorrectnessListenerImpl}.
2009      * @return the current listener (not {@code null})
2010      */
2011     public IncorrectnessListener getIncorrectnessListener() {
2012         return incorrectnessListener_;
2013     }
2014 
2015     /**
2016      * Returns the current HTML incorrectness listener.
2017      * @param listener the new value (not {@code null})
2018      */
2019     public void setIncorrectnessListener(final IncorrectnessListener listener) {
2020         if (listener == null) {
2021             throw new IllegalArgumentException("Null is not a valid IncorrectnessListener");
2022         }
2023         incorrectnessListener_ = listener;
2024     }
2025 
2026     /**
2027      * Returns the WebConsole.
2028      * @return the web console
2029      */
2030     public WebConsole getWebConsole() {
2031         if (webConsole_ == null) {
2032             webConsole_ = new WebConsole();
2033         }
2034         return webConsole_;
2035     }
2036 
2037     /**
2038      * Gets the current AJAX controller.
2039      * @return the controller
2040      */
2041     public AjaxController getAjaxController() {
2042         return ajaxController_;
2043     }
2044 
2045     /**
2046      * Sets the current AJAX controller.
2047      * @param newValue the controller
2048      */
2049     public void setAjaxController(final AjaxController newValue) {
2050         if (newValue == null) {
2051             throw new IllegalArgumentException("Null is not a valid AjaxController");
2052         }
2053         ajaxController_ = newValue;
2054     }
2055 
2056     /**
2057      * Sets the attachment handler.
2058      * @param handler the new attachment handler
2059      */
2060     public void setAttachmentHandler(final AttachmentHandler handler) {
2061         attachmentHandler_ = handler;
2062     }
2063 
2064     /**
2065      * Returns the current attachment handler.
2066      * @return the current attachment handler
2067      */
2068     public AttachmentHandler getAttachmentHandler() {
2069         return attachmentHandler_;
2070     }
2071 
2072     /**
2073      * Sets the WebStart handler.
2074      * @param handler the new WebStart handler
2075      */
2076     public void setWebStartHandler(final WebStartHandler handler) {
2077         webStartHandler_ = handler;
2078     }
2079 
2080     /**
2081      * Returns the current WebStart handler.
2082      * @return the current WebStart handler
2083      */
2084     public WebStartHandler getWebStartHandler() {
2085         return webStartHandler_;
2086     }
2087 
2088     /**
2089      * Returns the current clipboard handler.
2090      * @return the current clipboard handler
2091      */
2092     public ClipboardHandler getClipboardHandler() {
2093         return clipboardHandler_;
2094     }
2095 
2096     /**
2097      * Sets the clipboard handler.
2098      * @param handler the new clipboard handler
2099      */
2100     public void setClipboardHandler(final ClipboardHandler handler) {
2101         clipboardHandler_ = handler;
2102     }
2103 
2104     /**
2105      * Returns the current {@link PrintHandler}.
2106      * @return the current {@link PrintHandler} or null if print
2107      *         requests are ignored
2108      */
2109     public PrintHandler getPrintHandler() {
2110         return printHandler_;
2111     }
2112 
2113     /**
2114      * Sets the {@link PrintHandler} to be used if Windoe.print() is called
2115      * (<a href="https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#printing">Printing Spec</a>).
2116      *
2117      * @param handler the new {@link PrintHandler} or null if you like to
2118      *        ignore print requests (default is null)
2119      */
2120     public void setPrintHandler(final PrintHandler handler) {
2121         printHandler_ = handler;
2122     }
2123 
2124     /**
2125      * Returns the current FrameContent handler.
2126      * @return the current FrameContent handler
2127      */
2128     public FrameContentHandler getFrameContentHandler() {
2129         return frameContentHandler_;
2130     }
2131 
2132     /**
2133      * Sets the FrameContent handler.
2134      * @param handler the new FrameContent handler
2135      */
2136     public void setFrameContentHandler(final FrameContentHandler handler) {
2137         frameContentHandler_ = handler;
2138     }
2139 
2140     /**
2141      * Sets the onbeforeunload handler for this {@link WebClient}.
2142      * @param onbeforeunloadHandler the new onbeforeunloadHandler or null if none is specified
2143      */
2144     public void setOnbeforeunloadHandler(final OnbeforeunloadHandler onbeforeunloadHandler) {
2145         onbeforeunloadHandler_ = onbeforeunloadHandler;
2146     }
2147 
2148     /**
2149      * Returns the onbeforeunload handler for this {@link WebClient}.
2150      * @return the onbeforeunload handler or null if one hasn't been set
2151      */
2152     public OnbeforeunloadHandler getOnbeforeunloadHandler() {
2153         return onbeforeunloadHandler_;
2154     }
2155 
2156     /**
2157      * Gets the cache currently being used.
2158      * @return the cache (may not be null)
2159      */
2160     public Cache getCache() {
2161         return cache_;
2162     }
2163 
2164     /**
2165      * Sets the cache to use.
2166      * @param cache the new cache (must not be {@code null})
2167      */
2168     public void setCache(final Cache cache) {
2169         if (cache == null) {
2170             throw new IllegalArgumentException("cache should not be null!");
2171         }
2172         cache_ = cache;
2173     }
2174 
2175     /**
2176      * Keeps track of the current window. Inspired by WebTest's logic to track the current response.
2177      */
2178     private static final class CurrentWindowTracker implements WebWindowListener, Serializable {
2179         private final WebClient webClient_;
2180         private final boolean ensureOneTopLevelWindow_;
2181 
2182         CurrentWindowTracker(final WebClient webClient, final boolean ensureOneTopLevelWindow) {
2183             webClient_ = webClient;
2184             ensureOneTopLevelWindow_ = ensureOneTopLevelWindow;
2185         }
2186 
2187         /**
2188          * {@inheritDoc}
2189          */
2190         @Override
2191         public void webWindowClosed(final WebWindowEvent event) {
2192             final WebWindow window = event.getWebWindow();
2193             if (window instanceof TopLevelWindow) {
2194                 webClient_.topLevelWindows_.remove(window);
2195                 if (window == webClient_.getCurrentWindow()) {
2196                     if (!webClient_.topLevelWindows_.isEmpty()) {
2197                         // The current window is now the previous top-level window.
2198                         webClient_.setCurrentWindow(
2199                                 webClient_.topLevelWindows_.get(webClient_.topLevelWindows_.size() - 1));
2200                     }
2201                 }
2202             }
2203             else if (window == webClient_.getCurrentWindow()) {
2204                 // The current window is now the last top-level window.
2205                 if (webClient_.topLevelWindows_.isEmpty()) {
2206                     webClient_.setCurrentWindow(null);
2207                 }
2208                 else {
2209                     webClient_.setCurrentWindow(
2210                             webClient_.topLevelWindows_.get(webClient_.topLevelWindows_.size() - 1));
2211                 }
2212             }
2213         }
2214 
2215         /**
2216          * Postprocessing to make sure we have always one top level window open.
2217          */
2218         public void afterWebWindowClosedListenersProcessed(final WebWindowEvent event) {
2219             if (!ensureOneTopLevelWindow_) {
2220                 return;
2221             }
2222 
2223             if (webClient_.topLevelWindows_.isEmpty()) {
2224                 // Must always have at least window, and there are no top-level windows left; must create one.
2225                 final TopLevelWindow newWindow = new TopLevelWindow("", webClient_);
2226                 webClient_.setCurrentWindow(newWindow);
2227             }
2228         }
2229 
2230         /**
2231          * {@inheritDoc}
2232          */
2233         @Override
2234         public void webWindowContentChanged(final WebWindowEvent event) {
2235             final WebWindow window = event.getWebWindow();
2236             boolean use = false;
2237             if (window instanceof DialogWindow) {
2238                 use = true;
2239             }
2240             else if (window instanceof TopLevelWindow) {
2241                 use = event.getOldPage() == null;
2242             }
2243             else if (window instanceof FrameWindow) {
2244                 final FrameWindow fw = (FrameWindow) window;
2245                 final String enclosingPageState = fw.getEnclosingPage().getDocumentElement().getReadyState();
2246                 final URL frameUrl = fw.getEnclosedPage().getUrl();
2247                 if (!DomNode.READY_STATE_COMPLETE.equals(enclosingPageState) || frameUrl == UrlUtils.URL_ABOUT_BLANK) {
2248                     return;
2249                 }
2250 
2251                 // now looks at the visibility of the frame window
2252                 final BaseFrameElement frameElement = fw.getFrameElement();
2253                 if (webClient_.isJavaScriptEnabled() && frameElement.isDisplayed()) {
2254                     final ComputedCssStyleDeclaration style = fw.getComputedStyle(frameElement, null);
2255                     use = style.getCalculatedWidth(false, false) != 0
2256                             && style.getCalculatedHeight(false, false) != 0;
2257                 }
2258             }
2259             if (use) {
2260                 webClient_.setCurrentWindow(window);
2261             }
2262         }
2263 
2264         /**
2265          * {@inheritDoc}
2266          */
2267         @Override
2268         public void webWindowOpened(final WebWindowEvent event) {
2269             final WebWindow window = event.getWebWindow();
2270             if (window instanceof TopLevelWindow) {
2271                 final TopLevelWindow tlw = (TopLevelWindow) window;
2272                 webClient_.topLevelWindows_.add(tlw);
2273             }
2274             // Page is not loaded yet, don't set it now as current window.
2275         }
2276     }
2277 
2278     /**
2279      * Closes all opened windows, stopping all background JavaScript processing.
2280      * The WebClient is not really usable after this - you have to create a new one or
2281      * use WebClient.reset() instead.
2282      * <p>
2283      * {@inheritDoc}
2284      */
2285     @Override
2286     public void close() {
2287         // avoid attaching new windows to the js engine
2288         if (scriptEngine_ != null) {
2289             scriptEngine_.prepareShutdown();
2290         }
2291 
2292         // stop the CurrentWindowTracker from making sure there is still one window available
2293         currentWindowTracker_ = new CurrentWindowTracker(this, false);
2294 
2295         // Hint: a new TopLevelWindow may be opened by some JS script while we are closing the others
2296         // but the prepareShutdown() call will prevent the new window form getting js support
2297         List<WebWindow> windows = new ArrayList<>(windows_);
2298         for (final WebWindow window : windows) {
2299             if (window instanceof TopLevelWindow) {
2300                 final TopLevelWindow topLevelWindow = (TopLevelWindow) window;
2301 
2302                 try {
2303                     topLevelWindow.close(true);
2304                 }
2305                 catch (final Exception e) {
2306                     LOG.error("Exception while closing a TopLevelWindow", e);
2307                 }
2308             }
2309             else if (window instanceof DialogWindow) {
2310                 final DialogWindow dialogWindow = (DialogWindow) window;
2311 
2312                 try {
2313                     dialogWindow.close();
2314                 }
2315                 catch (final Exception e) {
2316                     LOG.error("Exception while closing a DialogWindow", e);
2317                 }
2318             }
2319         }
2320 
2321         // second round, none of the remaining windows should be registered to
2322         // the js engine because of prepareShutdown()
2323         windows = new ArrayList<>(windows_);
2324         for (final WebWindow window : windows) {
2325             if (window instanceof TopLevelWindow) {
2326                 final TopLevelWindow topLevelWindow = (TopLevelWindow) window;
2327 
2328                 try {
2329                     topLevelWindow.close(true);
2330                 }
2331                 catch (final Exception e) {
2332                     LOG.error("Exception while closing a TopLevelWindow", e);
2333                 }
2334             }
2335             else if (window instanceof DialogWindow) {
2336                 final DialogWindow dialogWindow = (DialogWindow) window;
2337 
2338                 try {
2339                     dialogWindow.close();
2340                 }
2341                 catch (final Exception e) {
2342                     LOG.error("Exception while closing a DialogWindow", e);
2343                 }
2344             }
2345         }
2346 
2347         // now both lists have to be empty
2348         if (!topLevelWindows_.isEmpty()) {
2349             LOG.error("Sill " + topLevelWindows_.size() + " top level windows are open. Please report this error!");
2350             topLevelWindows_.clear();
2351         }
2352 
2353         if (!windows_.isEmpty()) {
2354             LOG.error("Sill " + windows_.size() + " windows are open. Please report this error!");
2355             windows_.clear();
2356         }
2357         currentWindow_ = null;
2358 
2359         ThreadDeath toThrow = null;
2360         if (scriptEngine_ != null) {
2361             try {
2362                 scriptEngine_.shutdown();
2363             }
2364             catch (final ThreadDeath ex) {
2365                 // make sure the following cleanup is performed to avoid resource leaks
2366                 toThrow = ex;
2367             }
2368             catch (final Exception e) {
2369                 LOG.error("Exception while shutdown the scriptEngine", e);
2370             }
2371         }
2372         scriptEngine_ = null;
2373 
2374         if (webConnection_ != null) {
2375             try {
2376                 webConnection_.close();
2377             }
2378             catch (final Exception e) {
2379                 LOG.error("Exception while closing the connection", e);
2380             }
2381         }
2382         webConnection_ = null;
2383 
2384         synchronized (this) {
2385             if (executor_ != null) {
2386                 try {
2387                     executor_.shutdownNow();
2388                 }
2389                 catch (final Exception e) {
2390                     LOG.error("Exception while shutdown the executor service", e);
2391                 }
2392             }
2393         }
2394         executor_ = null;
2395 
2396         cache_.clear();
2397         if (toThrow != null) {
2398             throw toThrow;
2399         }
2400     }
2401 
2402     /**
2403      * <p><span style="color:red">Experimental API: May be changed in next release
2404      * and may not yet work perfectly!</span></p>
2405      *
2406      * <p>This shuts down the whole client and restarts with a new empty window.
2407      * Cookies and other states are preserved.
2408      */
2409     public void reset() {
2410         close();
2411 
2412         // this has to be done after the browser version was set
2413         webConnection_ = new HttpWebConnection(this);
2414         if (javaScriptEngineEnabled_) {
2415             scriptEngine_ = new JavaScriptEngine(this);
2416         }
2417 
2418         // The window must be constructed AFTER the script engine.
2419         currentWindowTracker_ = new CurrentWindowTracker(this, true);
2420         currentWindow_ = new TopLevelWindow("", this);
2421     }
2422 
2423     /**
2424      * <p>Blocks until all background JavaScript tasks have finished executing or until the specified
2425      * timeout is reached, whichever occurs first. Background JavaScript tasks include:</p>
2426      * <ul>
2427      *   <li>JavaScript scheduled via <code>window.setTimeout()</code></li>
2428      *   <li>JavaScript scheduled via <code>window.setInterval()</code></li>
2429      *   <li>Asynchronous <code>XMLHttpRequest</code> operations</li>
2430      *   <li>Other asynchronous JavaScript operations across all windows managed by this WebClient</li>
2431      * </ul>
2432      *
2433      * <p><strong>Timeout Behavior:</strong> If background tasks are scheduled to execute after
2434      * <code>(now + timeoutMillis)</code>, this method will wait for the full timeout duration
2435      * and then return the number of remaining jobs. The method guarantees it will never block
2436      * longer than the specified timeout.</p>
2437      *
2438      * <p><strong>Use Case:</strong> Use this method when you don't know the exact timing of when
2439      * background JavaScript will start, but you have a reasonable estimate of how long all
2440      * tasks should take to complete. For scenarios where you know when tasks should start
2441      * executing, consider using {@link #waitForBackgroundJavaScriptStartingBefore(long)} instead.</p>
2442      *
2443      * <p><strong>Thread Safety:</strong> This method is thread-safe and handles concurrent
2444      * modifications to the internal job manager list gracefully.</p>
2445      *
2446      * <p><strong>Example Usage:</strong></p>
2447      * <pre><code>
2448      * // Wait up to 5 seconds for all background JavaScript to complete
2449      * int remainingJobs = webClient.waitForBackgroundJavaScript(5000);
2450      * if (remainingJobs == 0) {
2451      *     log("All background JavaScript completed");
2452      * } else {
2453      *     log("Timeout reached, " + remainingJobs + " jobs still pending");
2454      * }
2455      * </code></pre>
2456      *
2457      * @param timeoutMillis the maximum amount of time to wait in milliseconds; must be positive
2458      * @return the number of background JavaScript jobs still executing or waiting to be executed
2459      *         when this method returns; returns <code>0</code> if all jobs completed successfully
2460      *         within the timeout period
2461      * @throws IllegalArgumentException if timeoutMillis is negative
2462      * @see #waitForBackgroundJavaScriptStartingBefore(long)
2463      * @see #waitForBackgroundJavaScriptStartingBefore(long, long)
2464      */
2465     public int waitForBackgroundJavaScript(final long timeoutMillis) {
2466         int count = 0;
2467         final long endTime = System.currentTimeMillis() + timeoutMillis;
2468         for (Iterator<WeakReference<JavaScriptJobManager>> i = jobManagers_.iterator(); i.hasNext();) {
2469             final JavaScriptJobManager jobManager;
2470             final WeakReference<JavaScriptJobManager> reference;
2471             try {
2472                 reference = i.next();
2473                 jobManager = reference.get();
2474                 if (jobManager == null) {
2475                     i.remove();
2476                     continue;
2477                 }
2478             }
2479             catch (final ConcurrentModificationException e) {
2480                 i = jobManagers_.iterator();
2481                 count = 0;
2482                 continue;
2483             }
2484 
2485             final long newTimeout = endTime - System.currentTimeMillis();
2486             count += jobManager.waitForJobs(newTimeout);
2487         }
2488         if (count != getAggregateJobCount()) {
2489             final long newTimeout = endTime - System.currentTimeMillis();
2490             return waitForBackgroundJavaScript(newTimeout);
2491         }
2492         return count;
2493     }
2494 
2495     /**
2496      * <p>Blocks until all background JavaScript tasks scheduled to start executing before
2497      * <code>(now + delayMillis)</code> have finished executing. Background JavaScript tasks include:</p>
2498      * <ul>
2499      *   <li>JavaScript scheduled via <code>window.setTimeout()</code></li>
2500      *   <li>JavaScript scheduled via <code>window.setInterval()</code></li>
2501      *   <li>Asynchronous <code>XMLHttpRequest</code> operations</li>
2502      *   <li>Other asynchronous JavaScript operations across all windows managed by this WebClient</li>
2503      * </ul>
2504      *
2505      * <p><strong>Method Behavior:</strong></p>
2506      * <ul>
2507      *   <li>If no background JavaScript tasks are currently executing and none are scheduled
2508      *       to start within <code>delayMillis</code>, this method returns immediately</li>
2509      *   <li>Tasks scheduled to execute after <code>(now + delayMillis)</code> are ignored
2510      *       and do not affect the waiting behavior</li>
2511      *   <li>The method waits for tasks to complete execution, not just to start</li>
2512      *   <li>This method waits indefinitely for qualifying tasks to complete (no timeout)</li>
2513      * </ul>
2514      *
2515      * <p><strong>Use Case:</strong> This method is ideal when you know approximately when
2516      * background JavaScript should start executing but are uncertain about execution duration.
2517      * Use this when you don't need a timeout and want to ensure all relevant tasks complete.
2518      * For scenarios where you need to wait for all background tasks regardless of timing,
2519      * use {@link #waitForBackgroundJavaScript(long)} instead. For timeout control, use
2520      * {@link #waitForBackgroundJavaScriptStartingBefore(long, long)} instead.</p>
2521      *
2522      * <p><strong>Thread Safety:</strong> This method is thread-safe and handles concurrent
2523      * modifications to the internal job manager list gracefully.</p>
2524      *
2525      * <p><strong>Example Usage:</strong></p>
2526      * <pre><code>
2527      * // Wait indefinitely for JavaScript tasks starting within 1 second
2528      * int remainingJobs = webClient.waitForBackgroundJavaScriptStartingBefore(1000);
2529      * if (remainingJobs == 0) {
2530      *     log("All relevant background JavaScript completed");
2531      * } else {
2532      *     log("Some tasks may still be pending: " + remainingJobs + " jobs");
2533      * }
2534      *
2535      * // Common pattern: wait for tasks that should start soon
2536      * // (useful after triggering an action that schedules JavaScript)
2537      * webClient.waitForBackgroundJavaScriptStartingBefore(500);
2538      * </code></pre>
2539      *
2540      * @param delayMillis the delay which determines the background tasks to wait for (in milliseconds);
2541      *                   must be non-negative
2542      * @return the number of background JavaScript jobs still executing or waiting to be executed
2543      *         when this method returns; returns <code>0</code> if all qualifying jobs completed
2544      *         successfully
2545      * @see #waitForBackgroundJavaScript(long)
2546      * @see #waitForBackgroundJavaScriptStartingBefore(long, long)
2547      */
2548     public int waitForBackgroundJavaScriptStartingBefore(final long delayMillis) {
2549         return waitForBackgroundJavaScriptStartingBefore(delayMillis, -1);
2550     }
2551 
2552     /**
2553      * <p>Blocks until all background JavaScript tasks scheduled to start executing before
2554      * <code>(now + delayMillis)</code> have finished executing, or until the specified timeout
2555      * is reached, whichever occurs first. Background JavaScript tasks include:</p>
2556      * <ul>
2557      *   <li>JavaScript scheduled via <code>window.setTimeout()</code></li>
2558      *   <li>JavaScript scheduled via <code>window.setInterval()</code></li>
2559      *   <li>Asynchronous <code>XMLHttpRequest</code> operations</li>
2560      *   <li>Other asynchronous JavaScript operations across all windows managed by this WebClient</li>
2561      * </ul>
2562      *
2563      * <p><strong>Method Behavior:</strong></p>
2564      * <ul>
2565      *   <li>If no background JavaScript tasks are currently executing and none are scheduled
2566      *       to start within <code>delayMillis</code>, this method returns immediately</li>
2567      *   <li>Tasks scheduled to execute after <code>(now + delayMillis)</code> are ignored
2568      *       and do not affect the waiting behavior</li>
2569      *   <li>The method waits for tasks to complete execution, not just to start</li>
2570      * </ul>
2571      *
2572      * <p><strong>Timeout Behavior:</strong></p>
2573      * <ul>
2574      *   <li>If <code>timeoutMillis</code> is negative or less than <code>delayMillis</code>,
2575      *       the timeout is ignored and the method waits indefinitely</li>
2576      *   <li>When a valid timeout is specified, the method will never block longer than
2577      *       <code>timeoutMillis</code> milliseconds</li>
2578      *   <li>The timeout applies to the total waiting time, not per task</li>
2579      * </ul>
2580      *
2581      * <p><strong>Use Case:</strong> This method is ideal when you know approximately when
2582      * background JavaScript should start executing but are uncertain about execution duration.
2583      * For scenarios where you need to wait for all background tasks regardless of timing,
2584      * use {@link #waitForBackgroundJavaScript(long)} instead.</p>
2585      *
2586      * <p><strong>Thread Safety:</strong> This method is thread-safe and handles concurrent
2587      * modifications to the internal job manager list gracefully.</p>
2588      *
2589      * <p><strong>Example Usage:</strong></p>
2590      * <pre><code>
2591      * // Wait for JavaScript tasks starting within 1 second, with 10 second max timeout
2592      * int remainingJobs = webClient.waitForBackgroundJavaScriptStartingBefore(1000, 10000);
2593      * if (remainingJobs == 0) {
2594      *     log("All relevant background JavaScript completed");
2595      * } else {
2596      *     log("Timeout reached or tasks still pending: " + remainingJobs + " jobs");
2597      * }
2598      *
2599      * // Wait indefinitely for tasks starting within 500ms (timeout ignored)
2600      * webClient.waitForBackgroundJavaScriptStartingBefore(500, 100); // timeout &lt; delay
2601      * </code></pre>
2602      *
2603      * @param delayMillis the delay which determines the background tasks to wait for (in milliseconds);
2604      *                   must be non-negative
2605      * @param timeoutMillis the maximum amount of time to wait (in milliseconds); if negative or
2606      *                     less than <code>delayMillis</code>, the timeout is ignored and the method
2607      *                     waits indefinitely for qualifying tasks to complete
2608      * @return the number of background JavaScript jobs still executing or waiting to be executed
2609      *         when this method returns; returns <code>0</code> if all qualifying jobs completed
2610      *         successfully within the specified constraints
2611      * @see #waitForBackgroundJavaScript(long)
2612      * @see #waitForBackgroundJavaScriptStartingBefore(long)
2613      */
2614     public int waitForBackgroundJavaScriptStartingBefore(final long delayMillis, final long timeoutMillis) {
2615         int count = 0;
2616         long now = System.currentTimeMillis();
2617         final long endTime = now + delayMillis;
2618         long endTimeout = now + timeoutMillis;
2619         if (timeoutMillis < 0 || timeoutMillis < delayMillis) {
2620             endTimeout = -1;
2621         }
2622 
2623         for (Iterator<WeakReference<JavaScriptJobManager>> i = jobManagers_.iterator(); i.hasNext();) {
2624             final JavaScriptJobManager jobManager;
2625             final WeakReference<JavaScriptJobManager> reference;
2626             try {
2627                 reference = i.next();
2628                 jobManager = reference.get();
2629                 if (jobManager == null) {
2630                     i.remove();
2631                     continue;
2632                 }
2633             }
2634             catch (final ConcurrentModificationException e) {
2635                 i = jobManagers_.iterator();
2636                 count = 0;
2637                 continue;
2638             }
2639             now = System.currentTimeMillis();
2640             final long newDelay = endTime - now;
2641             final long newTimeout = (endTimeout == -1) ? -1 : endTimeout - now;
2642             count += jobManager.waitForJobsStartingBefore(newDelay, newTimeout);
2643         }
2644         if (count != getAggregateJobCount()) {
2645             now = System.currentTimeMillis();
2646             final long newDelay = endTime - now;
2647             final long newTimeout = (endTimeout == -1) ? -1 : endTimeout - now;
2648             return waitForBackgroundJavaScriptStartingBefore(newDelay, newTimeout);
2649         }
2650         return count;
2651     }
2652 
2653     /**
2654      * Returns the aggregate background JavaScript job count across all windows.
2655      * @return the aggregate background JavaScript job count across all windows
2656      */
2657     private int getAggregateJobCount() {
2658         int count = 0;
2659         for (Iterator<WeakReference<JavaScriptJobManager>> i = jobManagers_.iterator(); i.hasNext();) {
2660             final JavaScriptJobManager jobManager;
2661             final WeakReference<JavaScriptJobManager> reference;
2662             try {
2663                 reference = i.next();
2664                 jobManager = reference.get();
2665                 if (jobManager == null) {
2666                     i.remove();
2667                     continue;
2668                 }
2669             }
2670             catch (final ConcurrentModificationException e) {
2671                 i = jobManagers_.iterator();
2672                 count = 0;
2673                 continue;
2674             }
2675             final int jobCount = jobManager.getJobCount();
2676             count += jobCount;
2677         }
2678         return count;
2679     }
2680 
2681     /**
2682      * When we deserialize, re-initializie transient fields.
2683      * @param in the object input stream
2684      * @throws IOException if an error occurs
2685      * @throws ClassNotFoundException if an error occurs
2686      */
2687     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
2688         in.defaultReadObject();
2689 
2690         webConnection_ = new HttpWebConnection(this);
2691         scriptEngine_ = new JavaScriptEngine(this);
2692         jobManagers_ = Collections.synchronizedList(new ArrayList<>());
2693         loadQueue_ = new ArrayList<>();
2694         css3ParserPool_ = new CSS3ParserPool();
2695         broadcastChannel_ = new HashSet<>();
2696     }
2697 
2698     private static class LoadJob {
2699         private final WebWindow requestingWindow_;
2700         private final String target_;
2701         private final WebResponse response_;
2702         private final WeakReference<Page> originalPage_;
2703         private final WebRequest request_;
2704         private final String forceAttachmentWithFilename_;
2705 
2706         // we can't us the WebRequest from the WebResponse because
2707         // we need the original request e.g. after a redirect
2708         LoadJob(final WebRequest request, final WebResponse response,
2709                 final WebWindow requestingWindow, final String target, final String forceAttachmentWithFilename) {
2710             request_ = request;
2711             requestingWindow_ = requestingWindow;
2712             target_ = target;
2713             response_ = response;
2714             originalPage_ = new WeakReference<>(requestingWindow.getEnclosedPage());
2715             forceAttachmentWithFilename_ = forceAttachmentWithFilename;
2716         }
2717 
2718         public boolean isOutdated() {
2719             if (target_ != null && !target_.isEmpty()) {
2720                 return false;
2721             }
2722 
2723             if (requestingWindow_.isClosed()) {
2724                 return true;
2725             }
2726 
2727             if (requestingWindow_.getEnclosedPage() != originalPage_.get()) {
2728                 return true;
2729             }
2730 
2731             return false;
2732         }
2733     }
2734 
2735     /**
2736      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2737      *
2738      * Perform the downloads and stores it for loading later into a window.
2739      * In the future downloads should be performed in parallel in separated threads.
2740      * TODO: refactor it before next release.
2741      * @param requestingWindow the window from which the request comes
2742      * @param target the name of the target window
2743      * @param request the request to perform
2744      * @param checkHash if true check for hashChenage
2745      * @param forceAttachmentWithFilename if not {@code null} the AttachmentHandler isAttachment() method is not called,
2746      *        the response has to be handled as attachment in any case
2747      * @param description information about the origin of the request. Useful for debugging.
2748      */
2749     public void download(final WebWindow requestingWindow, final String target,
2750         final WebRequest request, final boolean checkHash,
2751         final String forceAttachmentWithFilename, final String description) {
2752 
2753         final WebWindow targetWindow = resolveWindow(requestingWindow, target);
2754         final URL url = request.getUrl();
2755 
2756         if (targetWindow != null && HttpMethod.POST != request.getHttpMethod()) {
2757             final Page page = targetWindow.getEnclosedPage();
2758             if (page != null) {
2759                 if (page.isHtmlPage() && !((HtmlPage) page).isOnbeforeunloadAccepted()) {
2760                     return;
2761                 }
2762 
2763                 if (checkHash) {
2764                     final URL current = page.getUrl();
2765                     final boolean justHashJump =
2766                             HttpMethod.GET == request.getHttpMethod()
2767                             && UrlUtils.sameFile(url, current)
2768                             && null != url.getRef();
2769 
2770                     if (justHashJump) {
2771                         processOnlyHashChange(targetWindow, url);
2772                         return;
2773                     }
2774                 }
2775             }
2776         }
2777 
2778         synchronized (loadQueue_) {
2779             // verify if this load job doesn't already exist
2780             for (final LoadJob otherLoadJob : loadQueue_) {
2781                 if (otherLoadJob.response_ == null) {
2782                     continue;
2783                 }
2784                 final WebRequest otherRequest = otherLoadJob.request_;
2785                 final URL otherUrl = otherRequest.getUrl();
2786 
2787                 if (url.getPath().equals(otherUrl.getPath()) // fail fast
2788                     && url.toString().equals(otherUrl.toString())
2789                     && request.getRequestParameters().equals(otherRequest.getRequestParameters())
2790                     && Objects.equals(request.getRequestBody(), otherRequest.getRequestBody())) {
2791                     return; // skip it;
2792                 }
2793             }
2794         }
2795 
2796         final LoadJob loadJob;
2797         try {
2798             WebResponse response;
2799             try {
2800                 response = loadWebResponse(request);
2801             }
2802             catch (final NoHttpResponseException e) {
2803                 LOG.error("NoHttpResponseException while downloading; generating a NoHttpResponse", e);
2804                 response = new WebResponse(RESPONSE_DATA_NO_HTTP_RESPONSE, request, 0);
2805             }
2806             loadJob = new LoadJob(request, response, requestingWindow, target, forceAttachmentWithFilename);
2807         }
2808         catch (final IOException e) {
2809             throw new RuntimeException(e);
2810         }
2811 
2812         synchronized (loadQueue_) {
2813             loadQueue_.add(loadJob);
2814         }
2815     }
2816 
2817     /**
2818      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2819      *
2820      * Loads downloaded responses into the corresponding windows.
2821      * TODO: refactor it before next release.
2822      * @throws IOException in case of exception
2823      * @throws FailingHttpStatusCodeException in case of exception
2824      */
2825     public void loadDownloadedResponses() throws FailingHttpStatusCodeException, IOException {
2826         final List<LoadJob> queue;
2827 
2828         // synchronize access to the loadQueue_,
2829         // to be sure no job is ignored
2830         synchronized (loadQueue_) {
2831             if (loadQueue_.isEmpty()) {
2832                 return;
2833             }
2834             queue = new ArrayList<>(loadQueue_);
2835             loadQueue_.clear();
2836         }
2837 
2838         final HashSet<WebWindow> updatedWindows = new HashSet<>();
2839         for (int i = queue.size() - 1; i >= 0; --i) {
2840             final LoadJob loadJob = queue.get(i);
2841             if (loadJob.isOutdated()) {
2842                 if (LOG.isInfoEnabled()) {
2843                     LOG.info("No usage of download: " + loadJob);
2844                 }
2845                 continue;
2846             }
2847 
2848             final WebWindow window = resolveWindow(loadJob.requestingWindow_, loadJob.target_);
2849             if (updatedWindows.contains(window)) {
2850                 if (LOG.isInfoEnabled()) {
2851                     LOG.info("No usage of download: " + loadJob);
2852                 }
2853                 continue;
2854             }
2855 
2856             final WebWindow win = openTargetWindow(loadJob.requestingWindow_, loadJob.target_, TARGET_SELF);
2857             final Page pageBeforeLoad = win.getEnclosedPage();
2858             loadWebResponseInto(loadJob.response_, win, loadJob.forceAttachmentWithFilename_);
2859 
2860             // start execution here.
2861             if (scriptEngine_ != null) {
2862                 scriptEngine_.registerWindowAndMaybeStartEventLoop(win);
2863             }
2864 
2865             if (pageBeforeLoad != win.getEnclosedPage()) {
2866                 updatedWindows.add(win);
2867             }
2868 
2869             // check and report problems if needed
2870             throwFailingHttpStatusCodeExceptionIfNecessary(loadJob.response_);
2871         }
2872     }
2873 
2874     private static void processOnlyHashChange(final WebWindow window, final URL urlWithOnlyHashChange) {
2875         final Page page = window.getEnclosedPage();
2876         final String oldURL = page.getUrl().toExternalForm();
2877 
2878         // update request url
2879         final WebRequest req = page.getWebResponse().getWebRequest();
2880         req.setUrl(urlWithOnlyHashChange);
2881 
2882         // update location.hash
2883         final Window jsWindow = window.getScriptableObject();
2884         if (null != jsWindow) {
2885             final Location location = jsWindow.getLocation();
2886             location.setHash(oldURL, urlWithOnlyHashChange.getRef());
2887         }
2888 
2889         // add to history
2890         window.getHistory().addPage(page);
2891     }
2892 
2893     /**
2894      * Returns the options object of this WebClient.
2895      * @return the options object
2896      */
2897     public WebClientOptions getOptions() {
2898         return options_;
2899     }
2900 
2901     /**
2902      * Gets the holder for the different storages.
2903      * <p><span style="color:red">Experimental API: May be changed in next release!</span></p>
2904      * @return the holder
2905      */
2906     public StorageHolder getStorageHolder() {
2907         return storageHolder_;
2908     }
2909 
2910     /**
2911      * Returns the currently configured cookies applicable to the specified URL, in an unmodifiable set.
2912      * If disabled, this returns an empty set.
2913      * @param url the URL on which to filter the returned cookies
2914      * @return the currently configured cookies applicable to the specified URL, in an unmodifiable set
2915      */
2916     public synchronized Set<Cookie> getCookies(final URL url) {
2917         final CookieManager cookieManager = getCookieManager();
2918 
2919         if (!cookieManager.isCookiesEnabled()) {
2920             return Collections.emptySet();
2921         }
2922 
2923         final URL normalizedUrl = HttpClientConverter.replaceForCookieIfNecessary(url);
2924 
2925         final String host = normalizedUrl.getHost();
2926         // URLs like "about:blank" don't have cookies and we need to catch these
2927         // cases here before HttpClient complains
2928         if (host.isEmpty()) {
2929             return Collections.emptySet();
2930         }
2931 
2932         // discard expired cookies
2933         cookieManager.clearExpired(new Date());
2934 
2935         final Set<Cookie> matchingCookies = new LinkedHashSet<>();
2936         HttpClientConverter.addMatching(cookieManager.getCookies(), normalizedUrl,
2937                 getBrowserVersion(), matchingCookies);
2938         return Collections.unmodifiableSet(matchingCookies);
2939     }
2940 
2941     /**
2942      * Parses the given cookie and adds this to our cookie store.
2943      * @param cookieString the string to parse
2944      * @param pageUrl the url of the page that likes to set the cookie
2945      * @param origin the requester
2946      */
2947     public void addCookie(final String cookieString, final URL pageUrl, final Object origin) {
2948         final CookieManager cookieManager = getCookieManager();
2949         if (!cookieManager.isCookiesEnabled()) {
2950             if (LOG.isDebugEnabled()) {
2951                 LOG.debug("Skipped adding cookie: '" + cookieString
2952                         + "' because cookies are not enabled for the CookieManager.");
2953             }
2954             return;
2955         }
2956 
2957         try {
2958             final List<Cookie> cookies = HttpClientConverter.parseCookie(cookieString, pageUrl, getBrowserVersion());
2959 
2960             for (final Cookie cookie : cookies) {
2961                 cookieManager.addCookie(cookie);
2962 
2963                 if (LOG.isDebugEnabled()) {
2964                     LOG.debug("Added cookie: '" + cookieString + "'");
2965                 }
2966             }
2967         }
2968         catch (final MalformedCookieException e) {
2969             if (LOG.isDebugEnabled()) {
2970                 LOG.warn("Adding cookie '" + cookieString + "' failed.", e);
2971             }
2972             getIncorrectnessListener().notify("Adding cookie '" + cookieString
2973                         + "' failed; reason: '" + e.getMessage() + "'.", origin);
2974         }
2975     }
2976 
2977     /**
2978      * Returns true if the javaScript support is enabled.
2979      * To disable the javascript support (eg. temporary)
2980      * you have to use the {@link WebClientOptions#setJavaScriptEnabled(boolean)} setter.
2981      * @see #isJavaScriptEngineEnabled()
2982      * @see WebClientOptions#isJavaScriptEnabled()
2983      * @return true if the javaScript engine and the javaScript support is enabled.
2984      */
2985     public boolean isJavaScriptEnabled() {
2986         return javaScriptEngineEnabled_ && getOptions().isJavaScriptEnabled();
2987     }
2988 
2989     /**
2990      * Returns true if the javaScript engine is enabled.
2991      * To disable the javascript engine you have to use the
2992      * {@link WebClient#WebClient(BrowserVersion, boolean, String, int)} constructor.
2993      * @return true if the javaScript engine is enabled.
2994      */
2995     public boolean isJavaScriptEngineEnabled() {
2996         return javaScriptEngineEnabled_;
2997     }
2998 
2999     /**
3000      * Parses the given XHtml code string and loads the resulting XHtmlPage into
3001      * the current window.
3002      *
3003      * @param htmlCode the html code as string
3004      * @return the HtmlPage
3005      * @throws IOException in case of error
3006      */
3007     public HtmlPage loadHtmlCodeIntoCurrentWindow(final String htmlCode) throws IOException {
3008         final HTMLParser htmlParser = getPageCreator().getHtmlParser();
3009         final WebWindow webWindow = getCurrentWindow();
3010 
3011         final StringWebResponse webResponse =
3012                 new StringWebResponse(htmlCode, new URL("https://www.htmlunit.org/dummy.html"));
3013         final HtmlPage page = new HtmlPage(webResponse, webWindow);
3014         webWindow.setEnclosedPage(page);
3015 
3016         htmlParser.parse(this, webResponse, page, false, false);
3017         return page;
3018     }
3019 
3020     /**
3021      * Parses the given XHtml code string and loads the resulting XHtmlPage into
3022      * the current window.
3023      *
3024      * @param xhtmlCode the xhtml code as string
3025      * @return the XHtmlPage
3026      * @throws IOException in case of error
3027      */
3028     public XHtmlPage loadXHtmlCodeIntoCurrentWindow(final String xhtmlCode) throws IOException {
3029         final HTMLParser htmlParser = getPageCreator().getHtmlParser();
3030         final WebWindow webWindow = getCurrentWindow();
3031 
3032         final StringWebResponse webResponse =
3033                 new StringWebResponse(xhtmlCode, new URL("https://www.htmlunit.org/dummy.html"));
3034         final XHtmlPage page = new XHtmlPage(webResponse, webWindow);
3035         webWindow.setEnclosedPage(page);
3036 
3037         htmlParser.parse(this, webResponse, page, true, false);
3038         return page;
3039     }
3040 
3041     /**
3042      * Creates a new {@link WebSocketAdapter}.
3043      *
3044      * @param webSocketListener the {@link WebSocketListener}
3045      * @return a new {@link WebSocketAdapter}
3046      */
3047     public WebSocketAdapter buildWebSocketAdapter(final WebSocketListener webSocketListener) {
3048         return webSocketAdapterFactory_.buildWebSocketAdapter(this, webSocketListener);
3049     }
3050 
3051     /**
3052      * Defines a new factory method to create a new WebSocketAdapter.
3053      *
3054      * @param factory a {@link WebSocketAdapterFactory}
3055      */
3056     public void setWebSocketAdapter(final WebSocketAdapterFactory factory) {
3057         webSocketAdapterFactory_ = factory;
3058     }
3059 
3060     /**
3061      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3062      *
3063      * @return a CSS3Parser that will return to an internal pool for reuse if closed using the
3064      *         try-with-resource concept
3065      */
3066     public PooledCSS3Parser getCSS3Parser() {
3067         return this.css3ParserPool_.get();
3068     }
3069 
3070     /**
3071      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3072      *
3073      * @return the set of known {@link BroadcastChannel}s
3074      */
3075     public Set<BroadcastChannel> getBroadcastChannels() {
3076         return broadcastChannel_;
3077     }
3078 
3079     /**
3080      * Our pool of CSS3Parsers. If you need a parser, get it from here and use the AutoCloseable
3081      * functionality with a try-with-resource block. If you don't want to do that at all, continue
3082      * to build CSS3Parsers the old fashioned way.
3083      * <p>
3084      * Fetching a parser is thread safe. This API is built to minimize synchronization overhead,
3085      * hence it is possible to miss a returned parser from another thread under heavy pressure,
3086      * but because that is unlikely, we keep it simple and efficient. Caches are not supposed
3087      * to give cutting-edge guarantees.
3088      * <p>
3089      * This concept avoids a resource leak when someone does not close the fetched
3090      * parser because the pool does not know anything about the parser unless
3091      * it returns. We are not running a checkout-checkin concept.
3092      * <p>
3093      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3094      */
3095     static class CSS3ParserPool {
3096         /*
3097          * Our pool. We only hold data when it is available. In addition, synchronization against
3098          * this deque is cheap.
3099          */
3100         private final ConcurrentLinkedDeque<PooledCSS3Parser> parsers_ = new ConcurrentLinkedDeque<>();
3101 
3102         /**
3103          * Fetch a new or recycled CSS3parser. Make sure you use the try-with-resource concept
3104          * to automatically return it after use because a parser creation is expensive.
3105          * We won't get a leak, if you don't do so, but that will remove the advantage.
3106          *
3107          * @return a parser
3108          */
3109         public PooledCSS3Parser get() {
3110             // see if we have one, LIFO
3111             final PooledCSS3Parser parser = parsers_.pollLast();
3112 
3113             // if we don't have one, get us one
3114             return parser != null ? parser.markInUse(this) : new PooledCSS3Parser(this);
3115         }
3116 
3117         /**
3118          * Return a parser. Normally you don't have to use that method explicitly.
3119          * Prefer to user the AutoCloseable interface of the PooledParser by
3120          * using a try-with-resource statement.
3121          *
3122          * @param parser the parser to recycle
3123          */
3124         protected void recycle(final PooledCSS3Parser parser) {
3125             parsers_.addLast(parser);
3126         }
3127     }
3128 
3129     /**
3130      * This is a poolable CSS3Parser which can be reused automatically when closed.
3131      * A regular CSS3Parser is not thread-safe, hence also our pooled parser
3132      * is not thread-safe.
3133      * <p>
3134      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
3135      */
3136     public static class PooledCSS3Parser extends CSS3Parser implements AutoCloseable {
3137         /**
3138          * The pool we want to return us to. Because multiple threads can use this, we
3139          * have to ensure that we see the action here.
3140          */
3141         private CSS3ParserPool pool_;
3142 
3143         /**
3144          * Create a new poolable parser.
3145          *
3146          * @param pool the pool the parser should return to when it is closed
3147          */
3148         protected PooledCSS3Parser(final CSS3ParserPool pool) {
3149             super();
3150             this.pool_ = pool;
3151         }
3152 
3153         /**
3154          * Resets the parser's pool state so it can be safely returned again.
3155          *
3156          * @param pool the pool the parser should return to when it is closed
3157          * @return this parser for fluid programming
3158          */
3159         protected PooledCSS3Parser markInUse(final CSS3ParserPool pool) {
3160             // ensure we detect programming mistakes
3161             if (this.pool_ == null) {
3162                 this.pool_ = pool;
3163             }
3164             else {
3165                 throw new IllegalStateException("This PooledParser was not returned to the pool properly");
3166             }
3167 
3168             return this;
3169         }
3170 
3171         /**
3172          * Implements the AutoClosable interface. The return method ensures that
3173          * we are notified when we incorrectly close it twice which indicates a
3174          * programming flow defect.
3175          *
3176          * @throws IllegalStateException in case the parser is closed several times
3177          */
3178         @Override
3179         public void close() {
3180             if (this.pool_ != null) {
3181                 final CSS3ParserPool oldPool = this.pool_;
3182                 // set null first and recycle later to avoid exposing a broken state
3183                 // volatile guarantees visibility
3184                 this.pool_ = null;
3185 
3186                 // return
3187                 oldPool.recycle(this);
3188             }
3189             else {
3190                 throw new IllegalStateException("This PooledParser was returned already");
3191             }
3192         }
3193     }
3194 }