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  
19  import java.io.IOException;
20  import java.net.URL;
21  import java.nio.charset.Charset;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.htmlunit.util.MimeType;
31  import org.htmlunit.util.NameValuePair;
32  
33  /**
34   * A fake {@link WebConnection} designed to mock out the actual HTTP connections.
35   *
36   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
37   * @author Noboru Sinohara
38   * @author Marc Guillemot
39   * @author Brad Clarke
40   * @author Ahmed Ashour
41   * @author Ronald Brill
42   */
43  public class MockWebConnection implements WebConnection {
44  
45      private static final Log LOG = LogFactory.getLog(MockWebConnection.class);
46  
47      private final Map<String, IOException> throwableMap_ = new HashMap<>();
48      private final Map<String, RawResponseData> responseMap_ = new HashMap<>();
49      private RawResponseData defaultResponse_;
50      private WebRequest lastRequest_;
51      private int requestCount_;
52      private final List<URL> requestedUrls_ = Collections.synchronizedList(new ArrayList<>());
53  
54      /**
55       * Contains the raw data configured for a response.
56       */
57      public static class RawResponseData {
58          private final List<NameValuePair> headers_;
59          private final byte[] byteContent_;
60          private final String stringContent_;
61          private final int statusCode_;
62          private final String statusMessage_;
63          private Charset charset_;
64  
65          RawResponseData(final byte[] byteContent, final int statusCode, final String statusMessage,
66                  final String contentType, final List<NameValuePair> headers) {
67              byteContent_ = byteContent;
68              stringContent_ = null;
69              statusCode_ = statusCode;
70              statusMessage_ = statusMessage;
71              headers_ = compileHeaders(headers, contentType);
72          }
73  
74          RawResponseData(final String stringContent, final Charset charset, final int statusCode,
75                  final String statusMessage, final String contentType, final List<NameValuePair> headers) {
76              byteContent_ = null;
77              charset_ = charset;
78              stringContent_ = stringContent;
79              statusCode_ = statusCode;
80              statusMessage_ = statusMessage;
81              headers_ = compileHeaders(headers, contentType);
82          }
83  
84          private static List<NameValuePair> compileHeaders(final List<NameValuePair> headers, final String contentType) {
85              final List<NameValuePair> compiledHeaders = new ArrayList<>();
86              if (headers != null) {
87                  compiledHeaders.addAll(headers);
88              }
89              if (contentType != null) {
90                  compiledHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE, contentType));
91              }
92              return compiledHeaders;
93          }
94  
95          WebResponseData asWebResponseData() {
96              final byte[] content;
97              if (byteContent_ != null) {
98                  content = byteContent_;
99              }
100             else if (stringContent_ == null) {
101                 content = new byte[] {};
102             }
103             else {
104                 content = stringContent_.getBytes(charset_);
105             }
106             return new WebResponseData(content, statusCode_, statusMessage_, headers_);
107         }
108 
109         /**
110          * Gets the configured headers.
111          * @return the headers
112          */
113         public List<NameValuePair> getHeaders() {
114             return headers_;
115         }
116 
117         /**
118          * Gets the configured content bytes.
119          * @return {@code null} if a String content has been configured
120          */
121         public byte[] getByteContent() {
122             return byteContent_;
123         }
124 
125         /**
126          * Gets the configured content String.
127          * @return {@code null} if a byte content has been configured
128          */
129         public String getStringContent() {
130             return stringContent_;
131         }
132 
133         /**
134          * Gets the configured status code.
135          * @return the status code
136          */
137         public int getStatusCode() {
138             return statusCode_;
139         }
140 
141         /**
142          * Gets the configured status message.
143          * @return the message
144          */
145         public String getStatusMessage() {
146             return statusMessage_;
147         }
148 
149         /**
150          * Gets the configured charset.
151          * @return {@code null} for byte content
152          */
153         public Charset getCharset() {
154             return charset_;
155         }
156     }
157 
158     /**
159      * {@inheritDoc}
160      */
161     @Override
162     public WebResponse getResponse(final WebRequest request) throws IOException {
163         final RawResponseData rawResponse = getRawResponse(request);
164         return new WebResponse(rawResponse.asWebResponseData(), request, 0);
165     }
166 
167     /**
168      * Gets the raw response configured for the request.
169      * @param request the request
170      * @return the raw response
171      * @throws IOException if defined
172      */
173     public RawResponseData getRawResponse(final WebRequest request) throws IOException {
174         final URL url = request.getUrl();
175 
176         if (LOG.isDebugEnabled()) {
177             LOG.debug("Getting response for " + url.toExternalForm());
178         }
179 
180         lastRequest_ = request;
181         requestCount_++;
182         requestedUrls_.add(url);
183 
184         String urlString = url.toExternalForm();
185         final IOException throwable = throwableMap_.get(urlString);
186         if (throwable != null) {
187             throw throwable;
188         }
189 
190         RawResponseData rawResponse = responseMap_.get(urlString);
191         if (rawResponse == null) {
192             // try to find without query params
193             final int queryStart = urlString.lastIndexOf('?');
194             if (queryStart > -1) {
195                 urlString = urlString.substring(0, queryStart);
196                 rawResponse = responseMap_.get(urlString);
197             }
198 
199             // fall back to default
200             if (rawResponse == null) {
201                 rawResponse = defaultResponse_;
202                 if (rawResponse == null) {
203                     throw new IllegalStateException("No response specified that can handle URL "
204                          + request.getHttpMethod()
205                          + " [" + urlString + "]");
206                 }
207             }
208         }
209 
210         return rawResponse;
211     }
212 
213     /**
214      * Gets the list of requested URLs.
215      * @return the list of relative URLs
216      */
217     public List<URL> getRequestedUrls() {
218         return Collections.unmodifiableList(requestedUrls_);
219     }
220 
221     /**
222      * Gets the list of requested URLs relative to the provided URL.
223      * @param relativeTo what should be removed from the requested URLs.
224      * @return the list of relative URLs
225      */
226     public List<String> getRequestedUrls(final URL relativeTo) {
227         final String baseUrl = relativeTo.toString();
228         final List<String> response = new ArrayList<>();
229         for (final URL url : requestedUrls_) {
230             String s = url.toString();
231             if (s.startsWith(baseUrl)) {
232                 s = s.substring(baseUrl.length());
233             }
234             response.add(s);
235         }
236 
237         return Collections.unmodifiableList(response);
238     }
239 
240     /**
241      * Returns the method that was used in the last call to submitRequest().
242      *
243      * @return the method that was used in the last call to submitRequest()
244      */
245     public HttpMethod getLastMethod() {
246         return lastRequest_.getHttpMethod();
247     }
248 
249     /**
250      * Returns the parameters that were used in the last call to submitRequest().
251      *
252      * @return the parameters that were used in the last call to submitRequest()
253      */
254     public List<NameValuePair> getLastParameters() {
255         return lastRequest_.getRequestParameters();
256     }
257 
258     /**
259      * Sets the response that will be returned when the specified URL is requested.
260      * @param url the URL that will return the given response
261      * @param content the content to return
262      * @param statusCode the status code to return
263      * @param statusMessage the status message to return
264      * @param contentType the content type to return
265      * @param headers the response headers to return
266      */
267     public void setResponse(final URL url, final String content, final int statusCode,
268             final String statusMessage, final String contentType,
269             final List<NameValuePair> headers) {
270 
271         setResponse(
272                 url,
273                 content,
274                 statusCode,
275                 statusMessage,
276                 contentType,
277                 ISO_8859_1,
278                 headers);
279     }
280 
281     /**
282      * Sets the response that will be returned when the specified URL is requested.
283      * @param url the URL that will return the given response
284      * @param content the content to return
285      * @param statusCode the status code to return
286      * @param statusMessage the status message to return
287      * @param contentType the content type to return
288      * @param charset the charset
289      * @param headers the response headers to return
290      */
291     public void setResponse(final URL url, final String content, final int statusCode,
292             final String statusMessage, final String contentType, final Charset charset,
293             final List<NameValuePair> headers) {
294 
295         final RawResponseData responseEntry = buildRawResponseData(content, charset, statusCode, statusMessage,
296                 contentType, headers);
297         responseMap_.put(url.toExternalForm(), responseEntry);
298     }
299 
300     /**
301      * Sets the exception that will be thrown when the specified URL is requested.
302      * @param url the URL that will force the exception
303      * @param throwable the Throwable
304      */
305     public void setThrowable(final URL url, final IOException throwable) {
306         throwableMap_.put(url.toExternalForm(), throwable);
307     }
308 
309     /**
310      * Sets the response that will be returned when the specified URL is requested.
311      * @param url the URL that will return the given response
312      * @param content the content to return
313      * @param statusCode the status code to return
314      * @param statusMessage the status message to return
315      * @param contentType the content type to return
316      * @param headers the response headers to return
317      */
318     public void setResponse(final URL url, final byte[] content, final int statusCode,
319             final String statusMessage, final String contentType,
320             final List<NameValuePair> headers) {
321 
322         final RawResponseData responseEntry = buildRawResponseData(content, statusCode, statusMessage, contentType,
323             headers);
324         responseMap_.put(url.toExternalForm(), responseEntry);
325     }
326 
327     private static RawResponseData buildRawResponseData(final byte[] content, final int statusCode,
328             final String statusMessage, final String contentType, final List<NameValuePair> headers) {
329         return new RawResponseData(content, statusCode, statusMessage, contentType, headers);
330     }
331 
332     private static RawResponseData buildRawResponseData(final String content, Charset charset, final int statusCode,
333             final String statusMessage, final String contentType, final List<NameValuePair> headers) {
334 
335         if (charset == null) {
336             charset = ISO_8859_1;
337         }
338         return new RawResponseData(content, charset, statusCode, statusMessage, contentType, headers);
339     }
340 
341     /**
342      * Convenient method that is the same as calling
343      * {@link #setResponse(URL,String,int,String,String,List)} with a status
344      * of "200 OK", a content type of "text/html" and no additional headers.
345      *
346      * @param url the URL that will return the given response
347      * @param content the content to return
348      */
349     public void setResponse(final URL url, final String content) {
350         setResponse(url, content, 200, "OK", MimeType.TEXT_HTML, null);
351     }
352 
353     /**
354      * Convenient method that is the same as calling
355      * {@link #setResponse(URL,String,int,String,String,List)} with a status
356      * of "200 OK" and no additional headers.
357      *
358      * @param url the URL that will return the given response
359      * @param content the content to return
360      * @param contentType the content type to return
361      */
362     public void setResponse(final URL url, final String content, final String contentType) {
363         setResponse(url, content, 200, "OK", contentType, null);
364     }
365 
366     /**
367      * Convenient method that is the same as calling
368      * {@link #setResponse(URL, String, int, String, String, Charset, List)} with a status
369      * of "200 OK" and no additional headers.
370      *
371      * @param url the URL that will return the given response
372      * @param content the content to return
373      * @param contentType the content type to return
374      * @param charset the charset
375      */
376     public void setResponse(final URL url, final String content, final String contentType, final Charset charset) {
377         setResponse(url, content, 200, "OK", contentType, charset, null);
378     }
379 
380     /**
381      * Specify a generic HTML page that will be returned when the given URL is specified.
382      * The page will contain only minimal HTML to satisfy the HTML parser but will contain
383      * the specified title so that tests can check for titleText.
384      *
385      * @param url the URL that will return the given response
386      * @param title the title of the page
387      */
388     public void setResponseAsGenericHtml(final URL url, final String title) {
389         final String content = "<!DOCTYPE html><html><head><title>" + title + "</title></head><body></body></html>";
390         setResponse(url, content);
391     }
392 
393     /**
394      * Sets the response that will be returned when a URL is requested that does
395      * not have a specific content set for it.
396      *
397      * @param content the content to return
398      * @param statusCode the status code to return
399      * @param statusMessage the status message to return
400      * @param contentType the content type to return
401      */
402     public void setDefaultResponse(final String content, final int statusCode,
403             final String statusMessage, final String contentType) {
404 
405         defaultResponse_ = buildRawResponseData(content, null, statusCode, statusMessage, contentType, null);
406     }
407 
408     /**
409      * Sets the response that will be returned when a URL is requested that does
410      * not have a specific content set for it.
411      *
412      * @param content the content to return
413      * @param statusCode the status code to return
414      * @param statusMessage the status message to return
415      * @param contentType the content type to return
416      */
417     public void setDefaultResponse(final byte[] content, final int statusCode,
418             final String statusMessage, final String contentType) {
419 
420         defaultResponse_ = buildRawResponseData(content, statusCode, statusMessage, contentType, null);
421     }
422 
423     /**
424      * Sets the response that will be returned when a URL is requested that does
425      * not have a specific content set for it.
426      *
427      * @param content the content to return
428      */
429     public void setDefaultResponse(final String content) {
430         setDefaultResponse(content, 200, "OK", MimeType.TEXT_HTML);
431     }
432 
433     /**
434      * Sets the response that will be returned when a URL is requested that does
435      * not have a specific content set for it.
436      *
437      * @param content the content to return
438      * @param contentType the content type to return
439      */
440     public void setDefaultResponse(final String content, final String contentType) {
441         setDefaultResponse(content, 200, "OK", contentType, null);
442     }
443 
444     /**
445      * Sets the response that will be returned when a URL is requested that does
446      * not have a specific content set for it.
447      *
448      * @param content the content to return
449      * @param contentType the content type to return
450      * @param charset the charset
451      */
452     public void setDefaultResponse(final String content, final String contentType, final Charset charset) {
453         setDefaultResponse(content, 200, "OK", contentType, charset, null);
454     }
455 
456     /**
457      * Sets the response that will be returned when the specified URL is requested.
458      * @param content the content to return
459      * @param statusCode the status code to return
460      * @param statusMessage the status message to return
461      * @param contentType the content type to return
462      * @param headers the response headers to return
463      */
464     public void setDefaultResponse(final String content, final int statusCode,
465             final String statusMessage, final String contentType,
466             final List<NameValuePair> headers) {
467 
468         defaultResponse_ = buildRawResponseData(content, null, statusCode, statusMessage, contentType, headers);
469     }
470 
471     /**
472      * Sets the response that will be returned when the specified URL is requested.
473      * @param content the content to return
474      * @param statusCode the status code to return
475      * @param statusMessage the status message to return
476      * @param contentType the content type to return
477      * @param charset the charset
478      * @param headers the response headers to return
479      */
480     public void setDefaultResponse(final String content, final int statusCode,
481             final String statusMessage, final String contentType, final Charset charset,
482             final List<NameValuePair> headers) {
483 
484         defaultResponse_ = buildRawResponseData(content, charset, statusCode, statusMessage, contentType, headers);
485     }
486 
487     /**
488      * Returns the additional headers that were used in the in the last call
489      * to {@link #getResponse(WebRequest)}.
490      * @return the additional headers that were used in the in the last call
491      *         to {@link #getResponse(WebRequest)}
492      */
493     public Map<String, String> getLastAdditionalHeaders() {
494         return lastRequest_.getAdditionalHeaders();
495     }
496 
497     /**
498      * Returns the {@link WebRequest} that was used in the in the last call
499      * to {@link #getResponse(WebRequest)}.
500      * @return the {@link WebRequest} that was used in the in the last call
501      *         to {@link #getResponse(WebRequest)}
502      */
503     public WebRequest getLastWebRequest() {
504         return lastRequest_;
505     }
506 
507     /**
508      * Returns the number of requests made to this mock web connection.
509      * @return the number of requests made to this mock web connection
510      */
511     public int getRequestCount() {
512         return requestCount_;
513     }
514 
515     /**
516      * Indicates if a response has already been configured for this URL.
517      * @param url the url
518      * @return {@code false} if no response has been configured
519      */
520     public boolean hasResponse(final URL url) {
521         return responseMap_.containsKey(url.toExternalForm());
522     }
523 
524     /**
525      * {@inheritDoc}
526      */
527     @Override
528     public void close() {
529         clear();
530     }
531 
532     /**
533      * Resets this.
534      */
535     public void clear() {
536         throwableMap_.clear();
537         responseMap_.clear();
538         defaultResponse_ = null;
539         lastRequest_ = null;
540         requestCount_ = 0;
541         requestedUrls_.clear();
542     }
543 }