Source code: com/meterware/httpunit/WebClient.java
1 package com.meterware.httpunit;
2 /********************************************************************************************************************
3 * $Id: WebClient.java,v 1.62 2004/09/29 17:15:24 russgold Exp $
4 *
5 * Copyright (c) 2000-2004, Russell Gold
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
8 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
10 * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in all copies or substantial portions
13 * of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
16 * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
18 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 * DEALINGS IN THE SOFTWARE.
20 *
21 *******************************************************************************************************************/
22 import java.io.IOException;
23 import java.io.OutputStream;
24
25 import java.net.HttpURLConnection;
26 import java.net.MalformedURLException;
27 import java.net.URL;
28
29 import java.util.*;
30
31 import org.xml.sax.SAXException;
32 import com.meterware.httpunit.cookies.CookieJar;
33
34
35 /**
36 * The context for a series of web requests. This class manages cookies used to maintain
37 * session context, computes relative URLs, and generally emulates the browser behavior
38 * needed to build an automated test of a web site.
39 *
40 * @author <a href="mailto:russgold@httpunit.org">Russell Gold</a>
41 * @author Jan Ohrstrom
42 * @author Seth Ladd
43 * @author Oliver Imbusch
44 **/
45 abstract
46 public class WebClient {
47
48 private ArrayList _openWindows = new ArrayList();
49
50 /** The current main window. **/
51 private WebWindow _mainWindow = new WebWindow( this );
52 private String _authorizationString;
53 private String _proxyAuthorizationString;
54
55
56 public WebWindow getMainWindow() {
57 return _mainWindow;
58 }
59
60
61 public void setMainWindow( WebWindow mainWindow ) {
62 if (!_openWindows.contains( mainWindow )) throw new IllegalArgumentException( "May only select an open window owned by this client" );
63 _mainWindow = mainWindow;
64 }
65
66
67 public WebWindow[] getOpenWindows() {
68 return (WebWindow[]) _openWindows.toArray( new WebWindow[ _openWindows.size() ] );
69 }
70
71
72 public WebWindow getOpenWindow( String name ) {
73 if (name == null || name.length() == 0) return null;
74 for (Iterator i = _openWindows.iterator(); i.hasNext();) {
75 WebWindow window = (WebWindow) i.next();
76 if (name.equals( window.getName() )) return window;
77 }
78 return null;
79 }
80
81
82 /**
83 * Submits a GET method request and returns a response.
84 * @exception SAXException thrown if there is an error parsing the retrieved page
85 **/
86 public WebResponse getResponse( String urlString ) throws MalformedURLException, IOException, SAXException {
87 return _mainWindow.getResponse( urlString );
88 }
89
90
91 /**
92 * Submits a web request and returns a response. This is an alternate name for the getResponse method.
93 */
94 public WebResponse sendRequest( WebRequest request ) throws MalformedURLException, IOException, SAXException {
95 return _mainWindow.sendRequest( request );
96 }
97
98
99 /**
100 * Returns the response representing the current top page in the main window.
101 */
102 public WebResponse getCurrentPage() {
103 return _mainWindow.getCurrentPage();
104 }
105
106
107 /**
108 * Submits a web request and returns a response, using all state developed so far as stored in
109 * cookies as requested by the server.
110 * @exception SAXException thrown if there is an error parsing the retrieved page
111 **/
112 public WebResponse getResponse( WebRequest request ) throws MalformedURLException, IOException, SAXException {
113 return _mainWindow.getResponse( request );
114 }
115
116
117 /**
118 * Returns the name of the currently active frames in the main window.
119 **/
120 public String[] getFrameNames() {
121 return _mainWindow.getFrameNames();
122 }
123
124
125 /**
126 * Returns the response associated with the specified frame name in the main window.
127 * Throws a runtime exception if no matching frame is defined.
128 **/
129 public WebResponse getFrameContents( String frameName ) {
130 return _mainWindow.getFrameContents( frameName );
131 }
132
133
134 /**
135 * Returns the response associated with the specified frame name in the main window.
136 * Throws a runtime exception if no matching frame is defined.
137 *
138 * @since 1.6
139 **/
140 public WebResponse getFrameContents( FrameSelector targetFrame ) {
141 return _mainWindow.getFrameContents( targetFrame );
142 }
143
144
145 /**
146 * Returns the resource specified by the request. Does not update the client or load included framesets or scripts.
147 * May return null if the resource is a JavaScript URL which would normally leave the client unchanged.
148 */
149 public WebResponse getResource( WebRequest request ) throws IOException {
150 return _mainWindow.getResource( request );
151 }
152
153
154 /**
155 * Resets the state of this client, removing all cookies, frames, and per-client headers. This does not affect
156 * any listeners or preferences which may have been set.
157 **/
158 public void clearContents() {
159 _mainWindow = new WebWindow( this );
160 _cookieJar.clear();
161 _headers = new HeaderDictionary();
162 }
163
164
165 /**
166 * Defines a cookie to be sent to the server on every request.
167 * @deprecated as of 1.6, use #putCookie instead.
168 **/
169 public void addCookie( String name, String value ) {
170 _cookieJar.addCookie( name, value );
171 }
172
173
174 /**
175 * Defines a cookie to be sent to the server on every request. This overrides any previous setting for this cookie name.
176 **/
177 public void putCookie( String name, String value ) {
178 _cookieJar.putCookie( name, value );
179 }
180
181
182 /**
183 * Returns the name of all the active cookies which will be sent to the server.
184 **/
185 public String[] getCookieNames() {
186 return _cookieJar.getCookieNames();
187 }
188
189
190 /**
191 * Returns the value of the specified cookie.
192 **/
193 public String getCookieValue( String name ) {
194 return _cookieJar.getCookieValue( name );
195 }
196
197
198 /**
199 * Returns the properties associated with this client.
200 */
201 public ClientProperties getClientProperties() {
202 return _clientProperties;
203 }
204
205
206 /**
207 * Specifies the user agent identification. Used to trigger browser-specific server behavior.
208 * @deprecated as of 1.4.6. Use ClientProperties#setUserAgent instead.
209 **/
210 public void setUserAgent( String userAgent ) {
211 getClientProperties().setUserAgent( userAgent );
212 }
213
214
215 /**
216 * Returns the current user agent setting.
217 * @deprecated as of 1.4.6. Use ClientProperties#getUserAgent instead.
218 **/
219 public String getUserAgent() {
220 return getClientProperties().getUserAgent();
221 }
222
223
224 /**
225 * Sets a username and password for a basic authentication scheme.
226 **/
227 public void setAuthorization( String userName, String password ) {
228 _authorizationString = "Basic " + Base64.encode( userName + ':' + password );
229 }
230
231
232 /**
233 * Specifies a proxy server to use for requests from this client.
234 */
235 public void setProxyServer( String proxyHost, int proxyPort ) {
236 }
237
238
239 /**
240 * Specifies a proxy server to use, along with a user and password for authentication.
241 *
242 * @since 1.6
243 */
244 public void setProxyServer( String proxyHost, int proxyPort, String userName, String password ) {
245 setProxyServer( proxyHost, proxyPort );
246 _proxyAuthorizationString = "Basic " + Base64.encode( userName + ':' + password );
247 }
248
249
250 /**
251 * Clears the proxy server settings.
252 */
253 public void clearProxyServer() {
254 }
255
256
257 /**
258 * Returns the name of the active proxy server.
259 */
260 public String getProxyHost() {
261 return System.getProperty( "proxyHost" );
262 }
263
264
265 /**
266 * Returns the number of the active proxy port, or 0 is none is specified.
267 */
268 public int getProxyPort() {
269 try {
270 return Integer.parseInt( System.getProperty( "proxyPort" ) );
271 } catch (NumberFormatException e) {
272 return 0;
273 }
274 }
275
276
277
278
279 /**
280 * Sets the value for a header field to be sent with all requests. If the value set is null,
281 * removes the header from those to be sent.
282 **/
283 public void setHeaderField( String fieldName, String fieldValue ) {
284 _headers.put( fieldName, fieldValue );
285 }
286
287
288 /**
289 * Returns the value for the header field with the specified name. This method will ignore the case of the field name.
290 */
291 public String getHeaderField( String fieldName ) {
292 return (String) _headers.get( fieldName );
293 }
294
295
296 /**
297 * Specifies whether an exception will be thrown when an error status (4xx or 5xx) is detected on a response.
298 * Defaults to the value returned by HttpUnitOptions.getExceptionsThrownOnErrorStatus.
299 **/
300 public void setExceptionsThrownOnErrorStatus( boolean throwExceptions ) {
301 _exceptionsThrownOnErrorStatus = throwExceptions;
302 }
303
304
305 /**
306 * Returns true if an exception will be thrown when an error status (4xx or 5xx) is detected on a response.
307 **/
308 public boolean getExceptionsThrownOnErrorStatus() {
309 return _exceptionsThrownOnErrorStatus;
310 }
311
312
313 /**
314 * Adds a listener to watch for requests and responses.
315 */
316 public void addClientListener( WebClientListener listener ) {
317 synchronized (_clientListeners) {
318 if (listener != null && !_clientListeners.contains( listener )) _clientListeners.add( listener );
319 }
320 }
321
322
323 /**
324 * Removes a listener to watch for requests and responses.
325 */
326 public void removeClientListener( WebClientListener listener ) {
327 synchronized (_clientListeners) {
328 _clientListeners.remove( listener );
329 }
330 }
331
332
333 /**
334 * Adds a listener to watch for window openings and closings.
335 */
336 public void addWindowListener( WebWindowListener listener ) {
337 synchronized (_windowListeners) {
338 if (listener != null && !_windowListeners.contains( listener )) _windowListeners.add( listener );
339 }
340 }
341
342
343 /**
344 * Removes a listener to watch for window openings and closings.
345 */
346 public void removeWindowListener( WebWindowListener listener ) {
347 synchronized (_windowListeners) {
348 _windowListeners.remove( listener );
349 }
350 }
351
352
353 /**
354 * Returns the next javascript alert without removing it from the queue.
355 */
356 public String getNextAlert() {
357 return _alerts.isEmpty() ? null : (String) _alerts.getFirst();
358 }
359
360
361 /**
362 * Returns the next javascript alert and removes it from the queue. If the queue is empty,
363 * will return an empty string.
364 */
365 public String popNextAlert() {
366 if (_alerts.isEmpty()) return "";
367 return (String) _alerts.removeFirst();
368 }
369
370
371 /**
372 * Specifies the object which will respond to all dialogs.
373 **/
374 public void setDialogResponder( DialogResponder responder ) {
375 _dialogResponder = responder;
376 }
377
378
379 //------------------------------------------ protected members -----------------------------------
380
381
382 protected WebClient() {
383 _openWindows.add( _mainWindow );
384 }
385
386
387 /**
388 * Creates a web response object which represents the response to the specified web request.
389 * @param request the request to which the response should be generated
390 * @param targetFrame the frame in which the response should be stored
391 **/
392 abstract
393 protected WebResponse newResponse( WebRequest request, FrameSelector targetFrame ) throws MalformedURLException, IOException;
394
395
396 /**
397 * Writes the message body for the request.
398 **/
399 final protected void writeMessageBody( WebRequest request, OutputStream stream ) throws IOException {
400 request.writeMessageBody( stream );
401 }
402
403
404 /**
405 * Returns the value of all current header fields.
406 **/
407 protected Dictionary getHeaderFields( URL targetURL ) {
408 Hashtable result = (Hashtable) _headers.clone();
409 result.put( "User-Agent", getClientProperties().getUserAgent() );
410 if (getClientProperties().isAcceptGzip()) result.put( "Accept-Encoding", "gzip" );
411 AddHeaderIfNotNull( result, "Cookie", _cookieJar.getCookieHeaderField( targetURL ) );
412 AddHeaderIfNotNull( result, "Authorization", _authorizationString );
413 AddHeaderIfNotNull( result, "Proxy-Authorization", _proxyAuthorizationString );
414 return result;
415 }
416
417
418 private void AddHeaderIfNotNull( Hashtable result, final String headerName, final String headerValue ) {
419 if (headerValue != null) result.put( headerName, headerValue );
420 }
421
422
423 /**
424 * Updates this web client based on a received response. This includes updating
425 * cookies and frames. This method is required by ServletUnit, which cannot call the updateWindow method directly.
426 **/
427 final
428 protected void updateMainWindow( FrameSelector frame, WebResponse response ) throws MalformedURLException, IOException, SAXException {
429 getMainWindow().updateWindow( frame.getName(), response, new RequestContext() );
430 }
431
432
433 //------------------------------------------------- package members ----------------------------------------------------
434
435
436 void tellListeners( WebRequest request ) {
437 List listeners;
438
439 synchronized (_clientListeners) {
440 listeners = new ArrayList( _clientListeners );
441 }
442
443 for (Iterator i = listeners.iterator(); i.hasNext();) {
444 ((WebClientListener) i.next()).requestSent( this, request );
445 }
446 }
447
448
449 void tellListeners( WebResponse response ) {
450 List listeners;
451
452 synchronized (_clientListeners) {
453 listeners = new ArrayList( _clientListeners );
454 }
455
456 for (Iterator i = listeners.iterator(); i.hasNext();) {
457 ((WebClientListener) i.next()).responseReceived( this, response );
458 }
459 }
460
461
462 void updateClient( WebResponse response ) throws IOException {
463 if (getClientProperties().isAcceptCookies()) _cookieJar.updateCookies( response.getCookieJar() );
464 validateHeaders( response );
465 }
466
467
468 CookieJar getCookieJar() {
469 return _cookieJar;
470 }
471
472
473 void updateFrameContents( WebWindow requestWindow, String requestTarget, WebResponse response, RequestContext requestContext ) throws IOException, SAXException {
474 if (response.getFrame() == FrameSelector.NEW_FRAME) {
475 WebWindow window = new WebWindow( this, requestWindow.getCurrentPage() );
476 if (!WebRequest.NEW_WINDOW.equalsIgnoreCase( requestTarget )) window.setName( requestTarget );
477 response.setFrame( window.getTopFrame() );
478 window.updateFrameContents( response, requestContext );
479 _openWindows.add( window );
480 reportWindowOpened( window );
481 } else if (response.getFrame().getWindow() != null && response.getFrame().getWindow() != requestWindow) {
482 response.getFrame().getWindow().updateFrameContents( response, requestContext );
483 } else {
484 if (response.getFrame() == FrameSelector.TOP_FRAME) response.setFrame( requestWindow.getTopFrame() );
485 requestWindow.updateFrameContents( response, requestContext );
486 }
487 }
488
489
490 void close( WebWindow window ) {
491 if (!_openWindows.contains( window )) throw new IllegalStateException( "Window is already closed" );
492 _openWindows.remove( window );
493 if (_openWindows.isEmpty()) _openWindows.add( new WebWindow( this ) );
494 if (window.equals( _mainWindow )) _mainWindow = (WebWindow) _openWindows.get(0);
495 reportWindowClosed( window );
496 }
497
498
499 private void reportWindowOpened( WebWindow window ) {
500 List listeners;
501
502 synchronized (_windowListeners) {
503 listeners = new ArrayList( _windowListeners );
504 }
505
506 for (Iterator i = listeners.iterator(); i.hasNext();) {
507 ((WebWindowListener) i.next()).windowOpened( this, window );
508 }
509 }
510
511
512 private void reportWindowClosed( WebWindow window ) {
513 List listeners;
514
515 synchronized (_windowListeners) {
516 listeners = new ArrayList( _windowListeners );
517 }
518
519 for (Iterator i = listeners.iterator(); i.hasNext();) {
520 ((WebWindowListener) i.next()).windowClosed( this, window );
521 }
522 }
523
524 //------------------------------------------ package members ------------------------------------
525
526
527 boolean getConfirmationResponse( String message ) {
528 return _dialogResponder.getConfirmation( message );
529 }
530
531
532 String getUserResponse( String message, String defaultResponse ) {
533 return _dialogResponder.getUserResponse( message, defaultResponse );
534 }
535
536
537 void postAlert( String message ) {
538 _alerts.addLast( message );
539 }
540
541 //------------------------------------------ private members -------------------------------------
542
543 /** The list of alerts generated by JavaScript. **/
544 private LinkedList _alerts = new LinkedList();
545
546 /** The currently defined cookies. **/
547 private CookieJar _cookieJar = new CookieJar();
548
549
550 /** A map of header names to values. **/
551 private HeaderDictionary _headers = new HeaderDictionary();
552
553 private boolean _exceptionsThrownOnErrorStatus = HttpUnitOptions.getExceptionsThrownOnErrorStatus();
554
555 private List _clientListeners = new ArrayList();
556
557 private List _windowListeners = new ArrayList();
558
559 private DialogResponder _dialogResponder = new DialogAdapter();
560
561 private ClientProperties _clientProperties = ClientProperties.getDefaultProperties().cloneProperties();
562
563
564 /**
565 * Examines the headers in the response and throws an exception if appropriate.
566 **/
567 private void validateHeaders( WebResponse response ) throws HttpException {
568 if (!getExceptionsThrownOnErrorStatus()) return;
569
570 if (response.getHeaderField( "WWW-Authenticate" ) != null) {
571 throw new AuthorizationRequiredException( response.getHeaderField( "WWW-Authenticate" ) );
572 } else if (response.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) {
573 throw new HttpInternalErrorException( response.getURL() );
574 } else if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
575 throw new HttpNotFoundException( response.getResponseMessage(), response.getURL() );
576 } else if (response.getResponseCode() >= HttpURLConnection.HTTP_BAD_REQUEST) {
577 throw new HttpException( response.getResponseCode(), response.getResponseMessage(), response.getURL() );
578 }
579 }
580
581
582 FrameSelector findFrame( String target ) {
583 for (int i = 0; i < _openWindows.size(); i++) {
584 WebWindow webWindow = (WebWindow) _openWindows.get( i );
585 FrameSelector frame = webWindow.getFrame( target );
586 if (frame != null) return frame;
587 }
588 return null;
589 }
590
591
592 //==================================================================================================
593
594
595 static public class HeaderDictionary extends Hashtable {
596
597 public void addEntries( Dictionary source ) {
598 for (Enumeration e = source.keys(); e.hasMoreElements(); ) {
599 Object key = e.nextElement();
600 put( key, source.get( key ) );
601 }
602 }
603
604
605 public boolean containsKey( Object key ) {
606 return super.containsKey( matchPreviousFieldName( key.toString() ) );
607 }
608
609
610 public Object get( Object fieldName ) {
611 return (String) super.get( matchPreviousFieldName( fieldName.toString() ) );
612 }
613
614
615 public Object put( Object fieldName, Object fieldValue ) {
616 fieldName = matchPreviousFieldName( fieldName.toString() );
617 Object oldValue = super.get( fieldName );
618 if (fieldValue == null) {
619 remove( fieldName );
620 } else {
621 super.put( fieldName, fieldValue );
622 }
623 return oldValue;
624 }
625
626
627 /**
628 * If a matching field name with different case is already known, returns the older name.
629 * Otherwise, returns the specified name.
630 **/
631 private String matchPreviousFieldName( String fieldName ) {
632 for (Enumeration e = keys(); e.hasMoreElements(); ) {
633 String key = (String) e.nextElement();
634 if (key.equalsIgnoreCase( fieldName )) return key;
635 }
636 return fieldName;
637 }
638
639
640 }
641
642 }
643
644
645 //==================================================================================================
646
647
648 class RedirectWebRequest extends WebRequest {
649
650
651 RedirectWebRequest( WebResponse response ) throws MalformedURLException {
652 super( response.getURL(), response.getHeaderField( "Location" ), response.getFrame(), response.getFrameName() );
653 if (response.getReferer() != null) setHeaderField( "Referer", response.getReferer() );
654 }
655
656
657 /**
658 * Returns the HTTP method defined for this request.
659 **/
660 public String getMethod() {
661 return "GET";
662 }
663 }
664
665
666
667
668
669