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