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 }