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