001: package com.meterware.httpunit;
002:
003: /********************************************************************************************************************
004: * $Id: WebRequest.java,v 1.63 2004/09/09 22:32:41 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.File;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.io.OutputStream;
027:
028: import java.net.*;
029:
030: import java.util.*;
031:
032: /**
033: * A request sent to a web server.
034: **/
035: abstract public class WebRequest {
036:
037: static final String REFERER_HEADER_NAME = "Referer";
038:
039: private static URLStreamHandler JAVASCRIPT_STREAM_HANDLER = new JavascriptURLStreamHandler();
040: private static URLStreamHandler HTTPS_STREAM_HANDLER = new HttpsURLStreamHandler();
041:
042: private final ParameterHolder _parameterHolder;
043:
044: private URL _urlBase;
045: private FrameSelector _sourceFrame;
046: private String _requestTarget;
047: private String _urlString;
048: private Hashtable _headers;
049: private WebRequestSource _webRequestSource;
050:
051: private SubmitButton _button;
052:
053: /**
054: * Sets the value of a header to be sent with this request. A header set here will override any matching header set
055: * in the WebClient when the request is actually sent.
056: */
057: public void setHeaderField(String headerName, String headerValue) {
058: getHeaderDictionary().put(headerName, headerValue);
059: }
060:
061: /**
062: * Returns a copy of the headers to be sent with this request.
063: **/
064: public Dictionary getHeaders() {
065: return (Dictionary) getHeaderDictionary().clone();
066: }
067:
068: /**
069: * Returns the final URL associated with this web request.
070: **/
071: public URL getURL() throws MalformedURLException {
072: if (getURLBase() == null
073: || getURLBase().toString().indexOf("?") < 0) {
074: return newURL(getURLBase(), getURLString());
075: } else {
076: final String urlBaseString = getURLBase().toString();
077: URL newurlbase = new URL(urlBaseString.substring(0,
078: urlBaseString.indexOf("?")));
079: return newURL(newurlbase, getURLString());
080: }
081: }
082:
083: /**
084: * Creates a new URL, handling the case where the relative URL begins with a '?'
085: */
086: private URL newURL(final URL base, final String spec)
087: throws MalformedURLException {
088: if (spec.toLowerCase().startsWith("javascript:")) {
089: return new URL("javascript", null, -1, spec
090: .substring("javascript:".length()),
091: JAVASCRIPT_STREAM_HANDLER);
092: } else if (spec.toLowerCase().startsWith("https:")
093: && !HttpsProtocolSupport.hasHttpsSupport()) {
094: return new URL("https", null, -1, spec.substring("https:"
095: .length()), HTTPS_STREAM_HANDLER);
096: } else {
097: if (getURLBase() == null || getURLString().indexOf(':') > 0) {
098: if (getURLString().indexOf(':') <= 0) {
099: throw new RuntimeException(
100: "No protocol specified in URL '"
101: + getURLString() + "'");
102: }
103: HttpsProtocolSupport
104: .verifyProtocolSupport(getURLString()
105: .substring(0,
106: getURLString().indexOf(':')));
107: }
108: return spec.startsWith("?") ? new URL(base + spec)
109: : newCombinedURL(base, spec);
110: }
111: }
112:
113: private URL newCombinedURL(final URL base, final String spec)
114: throws MalformedURLException {
115: if (base == null)
116: return new URL(getNormalizedURL(spec));
117: else if (spec.startsWith(".."))
118: return new URL(getNormalizedURL(getURLDirectory(base)
119: + spec));
120: else
121: return new URL(base, getNormalizedURL(spec));
122: }
123:
124: private String getURLDirectory(final URL base) {
125: String url = base.toExternalForm();
126: int i = url.lastIndexOf('/');
127: return url.substring(0, i + 1);
128: }
129:
130: private String getNormalizedURL(String url) {
131: int questionIndex = url.indexOf('?');
132: if (questionIndex < 0)
133: return getNormalizedPath(url);
134: return getNormalizedPath(url.substring(0, questionIndex))
135: + url.substring(questionIndex);
136: }
137:
138: private String getNormalizedPath(String path) {
139: if (path.lastIndexOf("//") > path.lastIndexOf("://") + 1)
140: return getNormalizedPath(stripDoubleSlashes(path));
141: if (path.indexOf("/..") > 0)
142: return getNormalizedPath(stripUpNavigation(path));
143: if (path.indexOf("/./") > 0)
144: return getNormalizedPath(stripInPlaceNavigation(path));
145: return path;
146: }
147:
148: private String stripInPlaceNavigation(String url) {
149: int i = url.lastIndexOf("/./");
150: return url.substring(0, i + 1) + url.substring(i + 2);
151: }
152:
153: private String stripUpNavigation(String url) {
154: int i = url.indexOf("/..");
155: int j = url.lastIndexOf("/", i - 1);
156: return url.substring(0, j + 1) + url.substring(i + 3);
157: }
158:
159: private String stripDoubleSlashes(String url) {
160: int i = url.lastIndexOf("//");
161: return url.substring(0, i) + url.substring(i + 1);
162: }
163:
164: /**
165: * Returns the target for this web request.
166: */
167: public String getTarget() {
168: return _requestTarget;
169: }
170:
171: /**
172: * Returns the frame from which this request originates.
173: */
174: FrameSelector getSourceFrame() {
175: return _sourceFrame;
176: }
177:
178: /**
179: * Returns the HTTP method defined for this request.
180: **/
181: abstract public String getMethod();
182:
183: /**
184: * Returns the query string defined for this request. The query string is sent to the HTTP server as part of
185: * the request header. This default implementation returns an empty string.
186: **/
187: public String getQueryString() {
188: return "";
189: }
190:
191: //------------------------------------- ParameterCollection methods ------------------------------------
192:
193: /**
194: * Sets the value of a parameter in a web request.
195: **/
196: public void setParameter(String name, String value) {
197: _parameterHolder.setParameter(name, value);
198: }
199:
200: /**
201: * Sets the multiple values of a parameter in a web request.
202: **/
203: public void setParameter(String name, String[] values) {
204: _parameterHolder.setParameter(name, values);
205: }
206:
207: /**
208: * Sets the multiple values of a file upload parameter in a web request.
209: **/
210: public void setParameter(String parameterName,
211: UploadFileSpec[] files) {
212: if (!maySelectFile(parameterName))
213: throw new IllegalNonFileParameterException(parameterName);
214: if (!isMimeEncoded())
215: throw new MultipartFormRequiredException();
216: _parameterHolder.setParameter(parameterName, files);
217: }
218:
219: /**
220: * Specifies the click position for the submit button. When a user clioks on an image button, not only the name
221: * and value of the button, but also the position of the mouse at the time of the click is submitted with the form.
222: * This method allows the caller to override the position selected when this request was created.
223: *
224: * @exception IllegalRequestParameterException thrown if the request was not created from a form with an image button.
225: **/
226: public void setImageButtonClickPosition(int x, int y)
227: throws IllegalRequestParameterException {
228: if (_button == null)
229: throw new IllegalButtonPositionException();
230: _parameterHolder.selectImageButtonPosition(_button, x, y);
231: }
232:
233: /**
234: * Returns true if the specified parameter is a file field.
235: **/
236: public boolean isFileParameter(String name) {
237: return _parameterHolder.isFileParameter(name);
238: }
239:
240: /**
241: * Sets the file for a parameter upload in a web request.
242: **/
243: public void selectFile(String parameterName, File file) {
244: setParameter(parameterName,
245: new UploadFileSpec[] { new UploadFileSpec(file) });
246: }
247:
248: /**
249: * Sets the file for a parameter upload in a web request.
250: **/
251: public void selectFile(String parameterName, File file,
252: String contentType) {
253: setParameter(parameterName,
254: new UploadFileSpec[] { new UploadFileSpec(file,
255: contentType) });
256: }
257:
258: /**
259: * Sets the file for a parameter upload in a web request.
260: **/
261: public void selectFile(String parameterName, String fileName,
262: InputStream inputStream, String contentType) {
263: setParameter(parameterName,
264: new UploadFileSpec[] { new UploadFileSpec(fileName,
265: inputStream, contentType) });
266: }
267:
268: /**
269: * Returns an array of all parameter names defined as part of this web request.
270: * @since 1.3.1
271: **/
272: public String[] getRequestParameterNames() {
273: final HashSet names = new HashSet();
274: ParameterProcessor pp = new ParameterProcessor() {
275: public void addParameter(String name, String value,
276: String characterSet) throws IOException {
277: names.add(name);
278: }
279:
280: public void addFile(String parameterName,
281: UploadFileSpec fileSpec) throws IOException {
282: names.add(parameterName);
283: }
284: };
285:
286: try {
287: _parameterHolder.recordPredefinedParameters(pp);
288: _parameterHolder.recordParameters(pp);
289: } catch (IOException e) {
290: }
291:
292: return (String[]) names.toArray(new String[names.size()]);
293: }
294:
295: /**
296: * Returns the value of a parameter in this web request.
297: * @return the value of the named parameter, or empty string
298: * if it is not set.
299: **/
300: public String getParameter(String name) {
301: String[] values = getParameterValues(name);
302: return values.length == 0 ? "" : values[0];
303: }
304:
305: /**
306: * Returns the multiple default values of the named parameter.
307: **/
308: public String[] getParameterValues(String name) {
309: return _parameterHolder.getParameterValues(name);
310: }
311:
312: /**
313: * Removes a parameter from this web request.
314: **/
315: public void removeParameter(String name) {
316: _parameterHolder.removeParameter(name);
317: }
318:
319: //------------------------------------- Object methods ------------------------------------
320:
321: public String toString() {
322: return getMethod() + " request for (" + getURLBase() + ") "
323: + getURLString();
324: }
325:
326: //------------------------------------- protected members ------------------------------------
327:
328: /**
329: * Constructs a web request using an absolute URL string.
330: **/
331: protected WebRequest(String urlString) {
332: this (null, urlString);
333: }
334:
335: /**
336: * Constructs a web request using a base URL and a relative URL string.
337: **/
338: protected WebRequest(URL urlBase, String urlString) {
339: this (urlBase, urlString, TOP_FRAME);
340: }
341:
342: /**
343: * Constructs a web request using a base request and a relative URL string.
344: **/
345: protected WebRequest(WebRequest baseRequest, String urlString,
346: String target) throws MalformedURLException {
347: this (baseRequest.getURL(), urlString, target);
348: }
349:
350: /**
351: * Constructs a web request using a base URL, a relative URL string, and a target.
352: **/
353: protected WebRequest(URL urlBase, String urlString, String target) {
354: this (urlBase, urlString, FrameSelector.TOP_FRAME, target);
355: }
356:
357: /**
358: * Constructs a web request using a base URL, a relative URL string, and a target.
359: **/
360: protected WebRequest(URL urlBase, String urlString,
361: FrameSelector frame, String target) {
362: this (urlBase, urlString, frame, target,
363: new UncheckedParameterHolder());
364: }
365:
366: /**
367: * Constructs a web request from a form.
368: **/
369: protected WebRequest(WebForm sourceForm,
370: ParameterHolder parameterHolder, SubmitButton button,
371: int x, int y) {
372: this (sourceForm, parameterHolder);
373: if (button != null && button.isImageButton()
374: && button.getName().length() > 0) {
375: _button = button;
376: _parameterHolder.selectImageButtonPosition(_button, x, y);
377: }
378: }
379:
380: protected WebRequest(WebRequestSource requestSource,
381: ParameterHolder parameterHolder) {
382: this (requestSource.getBaseURL(), requestSource
383: .getRelativePage(), requestSource.getFrame(),
384: requestSource.getTarget(), parameterHolder);
385: _webRequestSource = requestSource;
386: setHeaderField(REFERER_HEADER_NAME, requestSource.getBaseURL()
387: .toExternalForm());
388: }
389:
390: static ParameterHolder newParameterHolder(
391: WebRequestSource requestSource) {
392: if (HttpUnitOptions.getParameterValuesValidated()) {
393: return requestSource;
394: } else {
395: return new UncheckedParameterHolder(requestSource);
396: }
397: }
398:
399: /**
400: * Constructs a web request using a base URL, a relative URL string, and a target.
401: **/
402: private WebRequest(URL urlBase, String urlString,
403: FrameSelector sourceFrame, String requestTarget,
404: ParameterHolder parameterHolder) {
405: _urlBase = urlBase;
406: _sourceFrame = sourceFrame;
407: _requestTarget = requestTarget;
408: _urlString = urlString.toLowerCase().startsWith("http") ? escape(urlString)
409: : urlString;
410: _parameterHolder = parameterHolder;
411: }
412:
413: private static String escape(String urlString) {
414: if (urlString.indexOf(' ') < 0)
415: return urlString;
416: StringBuffer sb = new StringBuffer();
417:
418: int start = 0;
419: do {
420: int index = urlString.indexOf(' ', start);
421: if (index < 0) {
422: sb.append(urlString.substring(start));
423: break;
424: } else {
425: sb.append(urlString.substring(start, index)).append(
426: "%20");
427: start = index + 1;
428: }
429: } while (true);
430: return sb.toString();
431: }
432:
433: /**
434: * Returns true if selectFile may be called with this parameter.
435: */
436: protected boolean maySelectFile(String parameterName) {
437: return isFileParameter(parameterName);
438: }
439:
440: /**
441: * Selects whether MIME-encoding will be used for this request. MIME-encoding changes the way the request is sent
442: * and is required for requests which include file parameters. This method may only be called for a POST request
443: * which was not created from a form.
444: **/
445: protected void setMimeEncoded(boolean mimeEncoded) {
446: _parameterHolder.setSubmitAsMime(mimeEncoded);
447: }
448:
449: /**
450: * Returns true if this request is to be MIME-encoded.
451: **/
452: protected boolean isMimeEncoded() {
453: return _parameterHolder.isSubmitAsMime();
454: }
455:
456: /**
457: * Returns the content type of this request. If null, no content is specified.
458: */
459: protected String getContentType() {
460: return null;
461: }
462:
463: /**
464: * Returns the character set required for this request.
465: **/
466: final protected String getCharacterSet() {
467: return _parameterHolder.getCharacterSet();
468: }
469:
470: /**
471: * Performs any additional processing necessary to complete the request.
472: **/
473: protected void completeRequest(URLConnection connection)
474: throws IOException {
475: if (connection instanceof HttpURLConnection)
476: ((HttpURLConnection) connection)
477: .setRequestMethod(getMethod());
478: }
479:
480: /**
481: * Writes the contents of the message body to the specified stream.
482: */
483: protected void writeMessageBody(OutputStream stream)
484: throws IOException {
485: }
486:
487: final protected URL getURLBase() {
488: return _urlBase;
489: }
490:
491: //------------------------------------- protected members ---------------------------------------------
492:
493: protected String getURLString() {
494: final String queryString = getQueryString();
495: if (queryString.length() == 0) {
496: return _urlString;
497: } else {
498: return _urlString + "?" + queryString;
499: }
500: }
501:
502: final protected ParameterHolder getParameterHolder() {
503: return _parameterHolder;
504: }
505:
506: //---------------------------------- package members --------------------------------
507:
508: /** The target indicating the topmost frame of a window. **/
509: static final String TOP_FRAME = "_top";
510:
511: /** The target indicating the parent of a frame. **/
512: static final String PARENT_FRAME = "_parent";
513:
514: /** The target indicating a new, empty window. **/
515: static final String NEW_WINDOW = "_blank";
516:
517: /** The target indicating the same frame. **/
518: static final String SAME_FRAME = "_self";
519:
520: WebRequestSource getWebRequestSource() {
521: return _webRequestSource;
522: }
523:
524: Hashtable getHeaderDictionary() {
525: if (_headers == null) {
526: _headers = new Hashtable();
527: if (getContentType() != null)
528: _headers.put("Content-Type", getContentType());
529: }
530: return _headers;
531: }
532:
533: String getReferer() {
534: return _headers == null ? null : (String) _headers
535: .get(REFERER_HEADER_NAME);
536: }
537:
538: }
539:
540: class URLEncodedString implements ParameterProcessor {
541:
542: private StringBuffer _buffer = new StringBuffer(
543: HttpUnitUtils.DEFAULT_BUFFER_SIZE);
544:
545: private boolean _haveParameters = false;
546:
547: public String getString() {
548: return _buffer.toString();
549: }
550:
551: public void addParameter(String name, String value,
552: String characterSet) {
553: if (_haveParameters)
554: _buffer.append('&');
555: _buffer.append(encode(name, characterSet));
556: if (value != null)
557: _buffer.append('=').append(encode(value, characterSet));
558: _haveParameters = true;
559: }
560:
561: public void addFile(String parameterName, UploadFileSpec fileSpec) {
562: throw new RuntimeException(
563: "May not URL-encode a file upload request");
564: }
565:
566: /**
567: * Returns a URL-encoded version of the string, including all eight bits, unlike URLEncoder, which strips the high bit.
568: **/
569: private String encode(String source, String characterSet) {
570: if (characterSet
571: .equalsIgnoreCase(HttpUnitUtils.DEFAULT_CHARACTER_SET)) {
572: return URLEncoder.encode(source);
573: } else {
574: try {
575: byte[] rawBytes = source.getBytes(characterSet);
576: StringBuffer result = new StringBuffer(
577: 3 * rawBytes.length);
578: for (int i = 0; i < rawBytes.length; i++) {
579: int candidate = rawBytes[i] & 0xff;
580: if (candidate == ' ') {
581: result.append('+');
582: } else if ((candidate >= 'A' && candidate <= 'Z')
583: || (candidate >= 'a' && candidate <= 'z')
584: || (candidate == '.') || (candidate == '-')
585: || (candidate == '*') || (candidate == '_')
586: || (candidate >= '0' && candidate <= '9')) {
587: result.append((char) rawBytes[i]);
588: } else if (candidate < 16) {
589: result.append("%0").append(
590: Integer.toHexString(candidate)
591: .toUpperCase());
592: } else {
593: result.append('%').append(
594: Integer.toHexString(candidate)
595: .toUpperCase());
596: }
597: }
598: return result.toString();
599: } catch (java.io.UnsupportedEncodingException e) {
600: return "???"; // XXX should pass the exception through as IOException ultimately
601: }
602: }
603: }
604:
605: }
606:
607: //======================================== class JavaScriptURLStreamHandler ============================================
608:
609: class JavascriptURLStreamHandler extends URLStreamHandler {
610:
611: protected URLConnection openConnection(URL u) throws IOException {
612: return null;
613: }
614: }
615:
616: //======================================== class HttpsURLStreamHandler ============================================
617:
618: class HttpsURLStreamHandler extends URLStreamHandler {
619:
620: protected URLConnection openConnection(URL u) throws IOException {
621: throw new RuntimeException(
622: "https support requires the Java Secure Sockets Extension. See http://java.sun.com/products/jsse");
623: }
624: }
625:
626: //============================= exception class IllegalNonFileParameterException ======================================
627:
628: /**
629: * This exception is thrown on an attempt to set a non-file parameter to a file value.
630: **/
631: class IllegalNonFileParameterException extends
632: IllegalRequestParameterException {
633:
634: IllegalNonFileParameterException(String parameterName) {
635: _parameterName = parameterName;
636: }
637:
638: public String getMessage() {
639: return "Parameter '"
640: + _parameterName
641: + "' is not a file parameter and may not be set to a file value.";
642: }
643:
644: private String _parameterName;
645:
646: }
647:
648: //============================= exception class MultipartFormRequiredException ======================================
649:
650: /**
651: * This exception is thrown on an attempt to set a file parameter in a form that does not specify MIME encoding.
652: **/
653: class MultipartFormRequiredException extends
654: IllegalRequestParameterException {
655:
656: MultipartFormRequiredException() {
657: }
658:
659: public String getMessage() {
660: return "The request does not use multipart/form-data encoding, and cannot be used to upload files ";
661: }
662:
663: }
664:
665: //============================= exception class IllegalButtonPositionException ======================================
666:
667: /**
668: * This exception is thrown on an attempt to set a file parameter in a form that does not specify MIME encoding.
669: **/
670: class IllegalButtonPositionException extends
671: IllegalRequestParameterException {
672:
673: IllegalButtonPositionException() {
674: }
675:
676: public String getMessage() {
677: return "The request was not created with an image button, and cannot accept an image button click position";
678: }
679:
680: }
|