001: /**********************************************************************************
002: *
003: * Copyright (c) 2003, 2004 The Regents of the University of Michigan, Trustees of Indiana University,
004: * Board of Trustees of the Leland Stanford, Jr., University, and The MIT Corporation
005: *
006: * Licensed under the Educational Community License Version 1.0 (the "License");
007: * By obtaining, using and/or copying this Original Work, you agree that you have read,
008: * understand, and will comply with the terms and conditions of the Educational Community License.
009: * You may obtain a copy of the License at:
010: *
011: * http://cvs.sakaiproject.org/licenses/license_1_0.html
012: *
013: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
014: * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
015: * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
016: * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
017: * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
018: *
019: **********************************************************************************/package edu.indiana.lib.twinpeaks.net;
020:
021: import edu.indiana.lib.twinpeaks.util.*;
022:
023: import java.io.*;
024: import java.net.*;
025: import java.util.*;
026:
027: import javax.xml.parsers.*;
028:
029: import org.w3c.dom.*;
030: import org.xml.sax.*;
031:
032: /**
033: * Handle HTTP based search operations. Send (POST or GET) a query to the
034: * server, read up the response.
035: *
036: * The response text and HTTP details (status, character set, etc) are made
037: * available to caller.
038: */
039: public class HttpTransaction {
040:
041: private static org.apache.commons.logging.Log _log = LogUtils
042: .getLog(HttpTransaction.class);
043:
044: /**
045: * Agent identification, HTTP form submission types
046: */
047: private final static String AGENT = "TwinPeaksAgent/1.0";
048: public final static String METHOD_GET = "GET";
049: public final static String METHOD_POST = "POST";
050: /*
051: * Character set constants
052: */
053: private static final String CHARSETEQ = "charset=";
054: public static final String DEFAULTCS = HttpTransactionUtils.DEFAULTCS;
055:
056: private URL url;
057: private ParameterMap parameters;
058: private HttpURLConnection connection;
059:
060: private String username;
061: private String password;
062:
063: private int responseCode;
064: private byte[] responseRaw;
065: private String responseString;
066: private CaseBlindHashMap responseHeaders;
067: private List responseCookies;
068:
069: private String inputCharacterSet;
070: private String defaultCharacterSet;
071:
072: private boolean doPost;
073: private boolean doRedirects;
074: private boolean transactionDone;
075: private boolean preserveBaseUrlFile;
076:
077: /**
078: * Default constructor
079: */
080: public HttpTransaction() {
081: this .responseHeaders = new CaseBlindHashMap();
082: }
083:
084: /**
085: * Initialize.
086: */
087: public void initialize(URL url, List cookieList) {
088:
089: this .url = url;
090: this .responseCookies = cookieList;
091: this .parameters = null;
092: this .doPost = true;
093: this .doRedirects = false;
094: this .preserveBaseUrlFile = false;
095: this .responseCode = 0;
096: this .transactionDone = false;
097: this .defaultCharacterSet = DEFAULTCS;
098: this .inputCharacterSet = DEFAULTCS;
099: }
100:
101: /**
102: * Initialize
103: */
104: public void initialize(List cookieList) {
105: initialize(null, cookieList);
106: }
107:
108: /**
109: * Set the transaction type
110: * @param type (GET or POST)
111: */
112: public void setTransactionType(String type) {
113:
114: doPost = true;
115:
116: if (METHOD_GET.equalsIgnoreCase(type)) {
117: doPost = false;
118: return;
119: }
120:
121: if (!METHOD_POST.equalsIgnoreCase(type)) {
122: throw new IllegalArgumentException(
123: "Unsupported transaction: " + type);
124: }
125: }
126:
127: /**
128: * Honor redirects?
129: * @param follow Set as true to follow any redirects suggested
130: */
131: public void setFollowRedirects(boolean follow) {
132: doRedirects = follow;
133: }
134:
135: /**
136: * Set up a name=value pair
137: * @param name Parameter name
138: * @param value Parameter value
139: */
140: public void setParameter(String name, String value) {
141: addParameter(name, value);
142: }
143:
144: /**
145: * Get a named parameter
146: * @param name Parameter name
147: * @return Parameter value
148: */
149: public String getParameter(String name) {
150: return parameters.getParameterMapValue(name);
151: }
152:
153: /**
154: * Get parameter the name of first occurance of the supplied value
155: * @param value Parameter value
156: * @return Parameter name
157: */
158: public String getParameterName(String value) {
159: return parameters.getParameterMapName(value);
160: }
161:
162: /**
163: * Empty the parameter list
164: */
165: public void clearParameters() {
166:
167: if (parameters != null) {
168: parameters.clear();
169: }
170: }
171:
172: /**
173: * Initialize for a new transaction
174: */
175: private void reset() throws DomException {
176:
177: connection = null;
178: responseString = null;
179: responseRaw = null;
180: transactionDone = false;
181:
182: responseHeaders.clear();
183: }
184:
185: /**
186: * Get the response status
187: * @return The HTTP response code
188: */
189: public int getResponseCode() {
190: verifyServerResponseSeen();
191: return responseCode;
192: }
193:
194: /**
195: * Get the URL text sent to the server
196: * @return URL string
197: */
198: public String getUrl() {
199: verifyServerResponseSeen();
200: return connection.getURL().toString();
201: }
202:
203: /**
204: * Set the "preserve URL file" flag
205: * @param state true to preserve the URL file portion (default is false)
206: */
207: public void setPreserveBaseUrlFile(boolean state) {
208: preserveBaseUrlFile = state;
209: }
210:
211: /**
212: * Get the basic url specification for this connection
213: * @return protocol://hostname[:port][/file]
214: */
215: public String getBaseUrlSpecification()
216: throws MalformedURLException {
217: verifyServerResponseSeen();
218: return HttpTransactionUtils.formatUrl(connection.getURL(),
219: preserveBaseUrlFile);
220: }
221:
222: /**
223: * Get the unfiltered response as sent by the server (debug)
224: * @return Response text as recieved
225: */
226: public byte[] getResponseBytes() {
227: verifyServerResponseSeen();
228: return responseRaw;
229: }
230:
231: /**
232: * Get the character-set-encoded String rendition of the server response
233: * @return Response text as recieved
234: */
235: public String getResponseString() {
236: verifyServerResponseSeen();
237: return responseString;
238: }
239:
240: /**
241: * Get all HTTP response headers
242: * @return CaseBlindHashMap of response-field/value pairs
243: */
244: public CaseBlindHashMap getResponseHeaders() {
245: verifyServerResponseSeen();
246: return responseHeaders;
247: }
248:
249: /**
250: * Get a named HTTP response
251: * @return Response value
252: */
253: public String getResponseHeader(String key) {
254: verifyServerResponseSeen();
255: return (String) responseHeaders.get(key);
256: }
257:
258: /**
259: * Get all provided cookies
260: * @return CaseBlindHashMap of response-field/value pairs
261: */
262: public List getResponseCookies() {
263: verifyServerResponseSeen();
264: return responseCookies;
265: }
266:
267: /**
268: * Get the response document character set (supplied by the server)
269: * @return The character set (as a String, default to iso-8859-1)
270: */
271: public String getResponseCharacterSet() {
272: return getResponseCharacterSet(false);
273: }
274:
275: /**
276: * Set the default character set
277: * @param cs Character set (utf-8, etc)
278: * Note: the character set defaults to DEFAULTCS (above) if not overridden
279: */
280: public void setDefaultCharacterSet(String cs) {
281: defaultCharacterSet = cs;
282: }
283:
284: /**
285: * Get the default character set (use if none supplied by server)
286: * @return The character set (iso-8859-1, utf-8, etc)
287: */
288: public String getDefaultCharacterSet() {
289: return defaultCharacterSet;
290: }
291:
292: /**
293: * Set the input character set
294: * @param cs Character set (utf-8, etc)
295: * Note: the character set defaults to DEFAULTCS (above) if not overridden
296: */
297: public void setInputCharacterSet(String cs) {
298: inputCharacterSet = cs;
299: }
300:
301: /**
302: * Get the default character set (use if none supplied by server)
303: * @return The character set (iso-8859-1, utf-8, etc)
304: */
305: public String getInputCharacterSet() {
306: return inputCharacterSet;
307: }
308:
309: /**
310: * Get the response document character set (supplied by the server)
311: * @param verify Validate server state (transaction complete)?
312: * @return The character set (as a String, default to iso-8859-1)
313: */
314: private String getResponseCharacterSet(boolean verify) {
315: String contentType;
316: StringBuffer buffer;
317: int index;
318:
319: if (verify) {
320: verifyServerResponseSeen();
321: }
322:
323: contentType = connection.getContentType();
324: _log.debug("ContentType = " + contentType);
325:
326: index = (contentType == null) ? -1 : contentType.toLowerCase()
327: .indexOf(CHARSETEQ);
328:
329: if (index == -1) {
330: _log.debug("return default character set: "
331: + getDefaultCharacterSet());
332: return getDefaultCharacterSet();
333: }
334:
335: buffer = new StringBuffer();
336: for (int i = (index + CHARSETEQ.length()); i < contentType
337: .length(); i++) {
338:
339: switch (contentType.charAt(i)) {
340: case ' ':
341: case '\t':
342: case ';':
343: break;
344:
345: default:
346: buffer.append(contentType.charAt(i));
347: break;
348: }
349: }
350: _log.debug("character set = "
351: + ((buffer.length() == 0) ? getDefaultCharacterSet()
352: : buffer.toString()));
353: return (buffer.length() == 0) ? getDefaultCharacterSet()
354: : buffer.toString();
355: }
356:
357: /**
358: * Add a <code>name=value</code> pair to the parameter list
359: *
360: * @param name Parameter name
361: * @param value Parameter content
362: */
363: private void addParameter(String name, String value) {
364:
365: if ((name != null) && (value != null)) {
366:
367: if (parameters == null) {
368: parameters = new ParameterMap();
369: }
370: parameters.setParameterMapValue(name, value);
371: }
372: }
373:
374: /**
375: * Create a URL object from the provided url text. If this is a GET operation
376: * and parameters have been set up, add them to the url text first.
377: * @param url URL text (eg http://xx/yy/zz)
378: */
379: private URL addParametersAndCreateUrl(String url)
380: throws MalformedURLException, UnsupportedEncodingException {
381: StringBuffer urlBuffer = new StringBuffer(url);
382:
383: if ((!doPost) && (parameters != null)) {
384: String separator = "?";
385: String cs = getInputCharacterSet();
386: Iterator it;
387:
388: if (url.indexOf('?') != -1) {
389: separator = "&";
390: }
391:
392: it = parameters.getParameterMapIterator();
393: while (parameters.nextParameterMapEntry(it)) {
394:
395: urlBuffer.append(HttpTransactionUtils.formatParameter(
396: parameters.getParameterNameFromIterator(),
397: parameters.getParameterValueFromIterator(),
398: separator, cs));
399:
400: if (separator.equals("?")) {
401: separator = "&";
402: }
403: }
404: }
405: return new URL(urlBuffer.toString());
406: }
407:
408: /**
409: * POST provided parameters
410: */
411: private void postParameters() throws IOException {
412:
413: Writer writer = null;
414: String cs = getInputCharacterSet();
415:
416: connection.setDoOutput(true);
417: try {
418: writer = new OutputStreamWriter(connection
419: .getOutputStream(), cs);
420:
421: if (parameters != null) {
422: String separator = "";
423: Iterator it;
424:
425: it = parameters.getParameterMapIterator();
426: while (parameters.nextParameterMapEntry(it)) {
427:
428: writer.write(separator
429: + parameters.getParameterNameFromIterator()
430: + "="
431: + URLEncoder.encode(parameters
432: .getParameterValueFromIterator(),
433: cs));
434:
435: if (separator.equals("")) {
436: separator = "&";
437: }
438: }
439: }
440:
441: } finally {
442: try {
443: if (writer != null)
444: writer.close();
445: } catch (Exception ignore) {
446: }
447: }
448: }
449:
450: /**
451: * Read the server response
452: */
453: private void readResponse() throws IOException, DomException,
454: UnsupportedEncodingException {
455:
456: ByteArrayOutputStream content = new ByteArrayOutputStream();
457: BufferedInputStream input = null;
458:
459: byte[] buffer = new byte[1024 * 8];
460: int count;
461:
462: /*
463: * Read the entire response
464: */
465: try {
466: input = new BufferedInputStream(connection.getInputStream());
467:
468: while ((count = input.read(buffer, 0, buffer.length)) != -1) {
469: content.write(buffer, 0, count);
470: }
471:
472: } finally {
473: try {
474: if (input != null)
475: input.close();
476: } catch (Exception ignore) {
477: }
478: }
479: /*
480: * Save the response text
481: */
482: responseString = content
483: .toString(getResponseCharacterSet(false));
484: responseRaw = content.toByteArray();
485: /*
486: * Pick up the HTTP status, headers, cookies
487: */
488: responseCode = connection.getResponseCode();
489:
490: responseHeaders.clear();
491: for (int i = 0;; i++) {
492: CookieData cookie;
493: String key, value;
494:
495: key = connection.getHeaderFieldKey(i);
496: value = connection.getHeaderField(i);
497:
498: if ((key == null) && (value == null)) {
499: break;
500: }
501:
502: if (!"Set-Cookie".equalsIgnoreCase(key)) {
503: responseHeaders.put(key, value);
504: continue;
505: }
506:
507: cookie = CookieUtils.parseCookie(url, value);
508: CookieUtils.storeCookie(responseCookies, cookie);
509: continue;
510: }
511: }
512:
513: /**
514: * Append a cookie attribute to the current cookie text
515: * @param sb Cookie text (StringBuffer)
516: * @param attribute Attribute name
517: * @param value Attribute value
518: */
519: private void append(StringBuffer sb, String attribute,
520: String value, boolean writeSeperator) {
521: if (value != null) {
522:
523: sb.append(attribute);
524: sb.append("=");
525: sb.append(value);
526:
527: if (writeSeperator) {
528: sb.append("; ");
529: }
530: }
531: }
532:
533: /**
534: * Set request (client-side) cookies
535: */
536: private String setRequestCookies() {
537: List cookieList;
538: StringBuffer cookieValues;
539: Iterator iterator;
540: String value;
541:
542: cookieValues = new StringBuffer();
543: cookieList = CookieUtils.findCookiesForServer(responseCookies,
544: url);
545: iterator = cookieList.iterator();
546:
547: while (iterator.hasNext()) {
548: CookieData cookie = (CookieData) iterator.next();
549:
550: append(cookieValues, cookie.getName(), cookie.getValue(),
551: iterator.hasNext());
552: }
553: return cookieValues.toString();
554: }
555:
556: /**
557: * Get an HttpURLConnection for this transaction
558: */
559: public HttpURLConnection getConnection() throws IOException {
560: HttpURLConnection urlConnection = (HttpURLConnection) url
561: .openConnection();
562: /*
563: * Set up the target URL (once)
564: */
565: return urlConnection;
566: }
567:
568: /**
569: * Perform one command transaction - add parameters and build the URL
570: * @param url URL string to server-side resource
571: * @return HTTP response code
572: */
573: public int doTransaction(String url) throws IOException,
574: DomException {
575:
576: this .url = addParametersAndCreateUrl(url);
577: return doTransaction();
578: }
579:
580: /**
581: * Perform one command transaction
582: * @param url URL for server-side
583: * @return HTTP response code
584: */
585: public int doTransaction(URL url) throws IOException, DomException {
586: this .url = url;
587: return doTransaction();
588: }
589:
590: /**
591: * Perform one command transaction
592: * @return HTTP response code
593: */
594: public int doTransaction() throws IOException, DomException {
595: String clientCookie;
596: /*
597: * Get connection, set transaction characteristics
598: */
599: _log.debug("*** CONNECTING to URL: " + this .url.toString());
600:
601: reset();
602: connection = getConnection();
603:
604: connection.setRequestProperty("User-Agent", AGENT);
605: connection.setRequestProperty("Accept",
606: "text/xml, text/html, text/*;q=0.5");
607: connection.setRequestProperty("Accept-Charset",
608: "iso-8859-1, utf-8, *;q=0.5");
609: /*
610: * Send along any appropriate cookies
611: */
612: clientCookie = setRequestCookies();
613: if (clientCookie.length() > 0) {
614: _log.debug("Cookie: " + clientCookie);
615: connection.setRequestProperty("Cookie", clientCookie);
616: }
617: /*
618: * Handle HTTP redirects as requested
619: */
620: connection.setInstanceFollowRedirects(doRedirects);
621: /*
622: * POST or GET?
623: *
624: * Should GET build the URL from the parameter list? We don't at present.
625: */
626: connection.setDoInput(true);
627: if (doPost) {
628: postParameters();
629: }
630: /*
631: * Get the server response, "close" the connection, return HTTP status
632: */
633: readResponse();
634:
635: connection.disconnect();
636: transactionDone = true;
637:
638: return getResponseCode();
639: }
640:
641: /**
642: * Verify we've recieved some sort of server response, even if invalid
643: */
644: private void verifyServerResponseSeen() {
645:
646: if (!transactionDone) {
647: String message = "The server transaction is not yet complete";
648:
649: throw new IllegalStateException(message);
650: }
651: }
652: }
|