001: /*
002: * @(#)HttpURLConnection.java 0.3-2 18/06/1999
003: *
004: * This file is part of the HTTPClient package
005: * Copyright (C) 1996-1999 Ronald Tschalär
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this library; if not, write to the Free
019: * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
020: * MA 02111-1307, USA
021: *
022: * For questions, suggestions, bug-reports, enhancement-requests etc.
023: * I may be contacted at:
024: *
025: * ronald@innovation.ch
026: *
027: */
028:
029: package HTTPClient;
030:
031: import java.net.URL;
032: import java.net.ProtocolException;
033: import java.net.UnknownHostException;
034: import java.io.IOException;
035: import java.io.InterruptedIOException;
036: import java.io.InputStream;
037: import java.io.OutputStream;
038: import java.io.BufferedInputStream;
039: import java.io.ByteArrayOutputStream;
040: import java.util.Date;
041: import java.util.Hashtable;
042: import java.util.Enumeration;
043:
044: /**
045: * This class is a wrapper around HTTPConnection providing the interface
046: * defined by java.net.URLConnection and java.net.HttpURLConnection.
047: *
048: * <P>This class can be used to replace the HttpClient in the JDK with this
049: * HTTPClient by defining the property
050: * <code>java.protocol.handler.pkgs=HTTPClient</code>.
051: *
052: * <P>One difference between Sun's HttpClient and this one is that this
053: * one will provide you with a real output stream if possible. This leads
054: * to two changes: you should set the request property "Content-Length",
055: * if possible, before invoking getOutputStream(); and in many cases
056: * getOutputStream() implies connect(). This should be transparent, though,
057: * apart from the fact that you can't change any headers or other settings
058: * anymore once you've gotten the output stream.
059: * So, for large data do:
060: * <PRE>
061: * HttpURLConnection con = (HttpURLConnection) url.openConnection();
062: *
063: * con.setDoOutput(true);
064: * con.setRequestProperty("Content-Length", ...);
065: * OutputStream out = con.getOutputStream();
066: *
067: * out.write(...);
068: * out.close();
069: *
070: * if (con.getResponseCode() != 200)
071: * ...
072: * </PRE>
073: *
074: * <P>The HTTPClient will send the request data using the chunked transfer
075: * encoding when no Content-Length is specified and the server is HTTP/1.1
076: * compatible. Because cgi-scripts can't usually handle this, you may
077: * experience problems trying to POST data. For this reason, whenever
078: * the Content-Type is application/x-www-form-urlencoded getOutputStream()
079: * will buffer the data before sending it so as prevent chunking. If you
080: * are sending requests with a different Content-Type and are experiencing
081: * problems then you may want to try setting the system property
082: * <var>HTTPClient.dontChunkRequests</var> to <var>true</var> (this needs
083: * to be done either on the command line or somewhere in the code before
084: * the first URLConnection.openConnection() is invoked).
085: *
086: * @version 0.3-2 18/06/1999
087: * @author Ronald Tschalär
088: * @since V0.3
089: */
090:
091: public class HttpURLConnection extends java.net.HttpURLConnection
092: implements GlobalConstants {
093: /** a list of HTTPConnections */
094: private static Hashtable connections = new Hashtable();
095:
096: /** the current connection */
097: private HTTPConnection con;
098:
099: /** the resource */
100: private String resource;
101:
102: /** the current method */
103: private String method;
104:
105: /** has the current method been set via setRequestMethod()? */
106: private boolean method_set;
107:
108: /** the default request headers */
109: private static NVPair[] default_headers = new NVPair[0];
110:
111: /** the request headers */
112: private NVPair[] headers;
113:
114: /** the response */
115: private HTTPResponse resp;
116:
117: /** is the redirection module activated for this instance? */
118: private boolean do_redir;
119:
120: /** the RedirectionModule class */
121: private static Class redir_mod;
122:
123: /** the output stream used for POST and PUT */
124: private OutputStream output_stream;
125:
126: static {
127: // The default allowUserAction in java.net.URLConnection is
128: // false.
129: try {
130: if (Boolean
131: .getBoolean("HTTPClient.HttpURLConnection.AllowUI"))
132: setDefaultAllowUserInteraction(true);
133: } catch (SecurityException se) {
134: }
135:
136: // get the RedirectionModule class
137: try {
138: redir_mod = Class.forName("HTTPClient.RedirectionModule");
139: } catch (ClassNotFoundException cnfe) {
140: throw new NoClassDefFoundError(cnfe.getMessage());
141: }
142:
143: // Set the User-Agent if the http.agent property is set
144: try {
145: String agent = System.getProperty("http.agent");
146: if (agent != null)
147: setDefaultRequestProperty("User-Agent", agent);
148: } catch (SecurityException se) {
149: }
150: }
151:
152: // Constructors
153:
154: private static String non_proxy_hosts = "";
155: private static String proxy_host = "";
156: private static int proxy_port = -1;
157:
158: /**
159: * Construct a connection to the specified url. A cache of
160: * HTTPConnections is used to maximize the reuse of these across
161: * multiple HttpURLConnections.
162: *
163: * <BR>The default method is "GET".
164: *
165: * @param url the url of the request
166: * @exception ProtocolNotSuppException if the protocol is not supported
167: */
168: public HttpURLConnection(URL url) throws ProtocolNotSuppException,
169: IOException {
170: super (url);
171:
172: // first read proxy properties and set
173: try {
174: String hosts = System.getProperty("http.nonProxyHosts", "");
175: if (!hosts.equalsIgnoreCase(non_proxy_hosts)) {
176: connections.clear();
177: non_proxy_hosts = hosts;
178: String[] list = Util.splitProperty(hosts);
179: for (int idx = 0; idx < list.length; idx++)
180: HTTPConnection.dontProxyFor(list[idx]);
181: }
182: } catch (ParseException pe) {
183: throw new IOException(pe.toString());
184: } catch (SecurityException se) {
185: }
186:
187: try {
188: String host = System.getProperty("http.proxyHost", "");
189: int port = Integer.getInteger("http.proxyPort", -1)
190: .intValue();
191: if (!host.equalsIgnoreCase(proxy_host)
192: || port != proxy_port) {
193: connections.clear();
194: proxy_host = host;
195: proxy_port = port;
196: HTTPConnection.setProxyServer(host, port);
197: }
198: } catch (SecurityException se) {
199: }
200:
201: // now setup stuff
202: con = getConnection(url);
203: method = "GET";
204: method_set = false;
205: resource = url.getFile();
206: headers = default_headers;
207: do_redir = getFollowRedirects();
208: output_stream = null;
209: }
210:
211: /**
212: * Returns an HTTPConnection. A cache of connections is kept and first
213: * consulted; only when the cache lookup fails is a new one created
214: * and added to the cache.
215: *
216: * @param url the url
217: * @return an HTTPConnection
218: * @exception ProtocolNotSuppException if the protocol is not supported
219: */
220: private HTTPConnection getConnection(URL url)
221: throws ProtocolNotSuppException {
222: // try the cache, using the host name
223:
224: String php = url.getProtocol()
225: + ":"
226: + url.getHost()
227: + ":"
228: + ((url.getPort() != -1) ? url.getPort() : URI
229: .defaultPort(url.getProtocol()));
230: php = php.toLowerCase();
231:
232: HTTPConnection con = (HTTPConnection) connections.get(php);
233: if (con != null)
234: return con;
235:
236: // Not in cache, so create new one and cache it
237:
238: con = new HTTPConnection(url);
239: connections.put(php, con);
240:
241: return con;
242: }
243:
244: // Methods
245:
246: /**
247: * Sets the request method (e.g. "PUT" or "HEAD"). Can only be set
248: * before connect() is called.
249: *
250: * @param method the http method.
251: * @exception ProtocolException if already connected.
252: */
253: public void setRequestMethod(String method)
254: throws ProtocolException {
255: if (connected)
256: throw new ProtocolException("Already connected!");
257:
258: if (DebugURLC)
259: System.err.println("URLC: (" + url
260: + ") Setting request method: " + method);
261:
262: this .method = method.trim().toUpperCase();
263: method_set = true;
264: }
265:
266: /**
267: * Return the request method used.
268: *
269: * @return the http method.
270: */
271: public String getRequestMethod() {
272: return method;
273: }
274:
275: /**
276: * Get the response code. Calls connect() if not connected.
277: *
278: * @return the http response code returned.
279: */
280: public int getResponseCode() throws IOException {
281: if (!connected)
282: connect();
283:
284: try {
285: return resp.getStatusCode();
286: } catch (ModuleException me) {
287: throw new IOException(me.toString());
288: }
289: }
290:
291: /**
292: * Get the response message describing the response code. Calls connect()
293: * if not connected.
294: *
295: * @return the http response message returned with the response code.
296: */
297: public String getResponseMessage() throws IOException {
298: if (!connected)
299: connect();
300:
301: try {
302: return resp.getReasonLine();
303: } catch (ModuleException me) {
304: throw new IOException(me.toString());
305: }
306: }
307:
308: /**
309: * Get the value part of a header. Calls connect() if not connected.
310: *
311: * @param name the of the header.
312: * @return the value of the header, or null if no such header was returned.
313: */
314: public String getHeaderField(String name) {
315: try {
316: if (!connected)
317: connect();
318: return resp.getHeader(name);
319: } catch (Exception e) {
320: return null;
321: }
322: }
323:
324: /**
325: * Get the value part of a header and converts it to an int. If the
326: * header does not exist or if its value could not be converted to an
327: * int then the default is returned. Calls connect() if not connected.
328: *
329: * @param name the of the header.
330: * @param def the default value to return in case of an error.
331: * @return the value of the header, or null if no such header was returned.
332: */
333: public int getHeaderFieldInt(String name, int def) {
334: try {
335: if (!connected)
336: connect();
337: return resp.getHeaderAsInt(name);
338: } catch (Exception e) {
339: return def;
340: }
341: }
342:
343: /**
344: * Get the value part of a header, interprets it as a date and converts
345: * it to a long representing the number of milliseconds since 1970. If
346: * the header does not exist or if its value could not be converted to a
347: * date then the default is returned. Calls connect() if not connected.
348: *
349: * @param name the of the header.
350: * @param def the default value to return in case of an error.
351: * @return the value of the header, or def in case of an error.
352: */
353: public long getHeaderFieldDate(String name, long def) {
354: try {
355: if (!connected)
356: connect();
357: return resp.getHeaderAsDate(name).getTime();
358: } catch (Exception e) {
359: return def;
360: }
361: }
362:
363: private String[] hdr_keys, hdr_values;
364:
365: /**
366: * Gets header name of the n-th header. Calls connect() if not connected.
367: * The name of the 0-th header is <var>null</var>, even though it the
368: * 0-th header has a value.
369: *
370: * @param n which header to return.
371: * @return the header name, or null if not that many headers.
372: */
373: public String getHeaderFieldKey(int n) {
374: if (hdr_keys == null)
375: fill_hdr_arrays();
376:
377: if (n >= 0 && n < hdr_keys.length)
378: return hdr_keys[n];
379: else
380: return null;
381: }
382:
383: /**
384: * Gets header value of the n-th header. Calls connect() if not connected.
385: * The value of 0-th header is the Status-Line (e.g. "HTTP/1.1 200 Ok").
386: *
387: * @param n which header to return.
388: * @return the header value, or null if not that many headers.
389: */
390: public String getHeaderField(int n) {
391: if (hdr_values == null)
392: fill_hdr_arrays();
393:
394: if (n >= 0 && n < hdr_values.length)
395: return hdr_values[n];
396: else
397: return null;
398: }
399:
400: /**
401: * Cache the list of headers.
402: */
403: private void fill_hdr_arrays()
404: {
405: try
406: {
407: if (!connected) connect();
408:
409: // count number of headers
410: int num = 1;
411: Enumeration enum = resp.listHeaders();
412: while (enum.hasMoreElements())
413: {
414: num++;
415: enum.nextElement();
416: }
417:
418: // allocate arrays
419: hdr_keys = new String[num];
420: hdr_values = new String[num];
421:
422: // fill arrays
423: enum = resp.listHeaders();
424: for (int idx=1; idx<num; idx++)
425: {
426: hdr_keys[idx] = (String) enum.nextElement();
427: hdr_values[idx] = resp.getHeader(hdr_keys[idx]);
428: }
429:
430: // the 0'th field is special
431: hdr_values[0] = resp.getVersion() + " " + resp.getStatusCode() +
432: " " + resp.getReasonLine();
433: }
434: catch (Exception e)
435: { hdr_keys = hdr_values = new String[0]; }
436: }
437:
438: /**
439: * Gets an input stream from which the data in the response may be read.
440: * Calls connect() if not connected.
441: *
442: * @return an InputStream
443: * @exception ProtocolException if input not enabled.
444: * @see java.net.URLConnection#setDoInput(boolean)
445: */
446: public InputStream getInputStream() throws IOException {
447: if (!doInput)
448: throw new ProtocolException(
449: "Input not enabled! (use setDoInput(true))");
450:
451: if (!connected)
452: connect();
453:
454: InputStream stream;
455: try {
456: stream = resp.getInputStream();
457: } catch (ModuleException e) {
458: throw new IOException(e.toString());
459: }
460:
461: return stream;
462: }
463:
464: /**
465: * Returns the error stream if the connection failed
466: * but the server sent useful data nonetheless.
467: *
468: * <P>This method will not cause a connection to be initiated.
469: *
470: * @return an InputStream, or null if either the connection hasn't
471: * been established yet or no error occured
472: * @see java.net.HttpURLConnection#getErrorStream()
473: * @since V0.3-1
474: */
475: public InputStream getErrorStream() {
476: try {
477: if (!doInput || !connected || resp.getStatusCode() < 300
478: || resp.getHeaderAsInt("Content-length") <= 0)
479: return null;
480:
481: return resp.getInputStream();
482: } catch (Exception e) {
483: return null;
484: }
485: }
486:
487: /**
488: * Gets an output stream which can be used send an entity with the
489: * request. Can be called multiple times, in which case always the
490: * same stream is returned.
491: *
492: * <P>The default request method changes to "POST" when this method is
493: * called. Cannot be called after connect().
494: *
495: * <P>If no Content-type has been set it defaults to
496: * <var>application/x-www-form-urlencoded</var>. Furthermore, if the
497: * Content-type is <var>application/x-www-form-urlencoded</var> then all
498: * output will be collected in a buffer before sending it to the server;
499: * otherwise an HttpOutputStream is used.
500: *
501: * @return an OutputStream
502: * @exception ProtocolException if already connect()'ed, if output is not
503: * enabled or if the request method does not
504: * support output.
505: * @see java.net.URLConnection#setDoOutput(boolean)
506: * @see HTTPClient.HttpOutputStream
507: */
508: public synchronized OutputStream getOutputStream()
509: throws IOException {
510: if (connected)
511: throw new ProtocolException("Already connected!");
512:
513: if (!doOutput)
514: throw new ProtocolException(
515: "Output not enabled! (use setDoOutput(true))");
516: if (!method_set)
517: method = "POST";
518: else if (method.equals("HEAD") || method.equals("GET")
519: || method.equals("TRACE"))
520: throw new ProtocolException("Method " + method
521: + " does not support output!");
522:
523: if (getRequestProperty("Content-type") == null)
524: setRequestProperty("Content-type",
525: "application/x-www-form-urlencoded");
526:
527: if (output_stream == null) {
528: if (DebugURLC)
529: System.err.println("URLC: (" + url
530: + ") creating output stream");
531:
532: String cl = getRequestProperty("Content-Length");
533: if (cl != null)
534: output_stream = new HttpOutputStream(Integer
535: .parseInt(cl));
536: else {
537: // Hack: because of restrictions when using true output streams
538: // and because form-data is usually quite limited in size, we
539: // first collect all data before sending it if this is
540: // form-data.
541: if (getRequestProperty("Content-type").equals(
542: "application/x-www-form-urlencoded"))
543: output_stream = new ByteArrayOutputStream(300);
544: else
545: output_stream = new HttpOutputStream();
546: }
547:
548: if (output_stream instanceof HttpOutputStream)
549: connect();
550: }
551:
552: return output_stream;
553: }
554:
555: /**
556: * Gets the url for this connection. If we're connect()'d and the request
557: * was redirected then the url returned is that of the final request.
558: *
559: * @return the final url, or null if any exception occured.
560: */
561: public URL getURL() {
562: if (connected) {
563: try {
564: if (resp.getEffectiveURL() != null)
565: return resp.getEffectiveURL();
566: } catch (Exception e) {
567: return null;
568: }
569: }
570:
571: return url;
572: }
573:
574: /**
575: * Sets the <var>If-Modified-Since</var> header.
576: *
577: * @param time the number of milliseconds since 1970.
578: */
579: public void setIfModifiedSince(long time) {
580: super .setIfModifiedSince(time);
581: setRequestProperty("If-Modified-Since", Util.httpDate(new Date(
582: time)));
583: }
584:
585: /**
586: * Sets an arbitrary request header.
587: *
588: * @param name the name of the header.
589: * @param value the value for the header.
590: */
591: public void setRequestProperty(String name, String value) {
592: if (DebugURLC)
593: System.err.println("URLC: (" + url
594: + ") Setting request property: " + name + " : "
595: + value);
596:
597: int idx;
598: for (idx = 0; idx < headers.length; idx++) {
599: if (headers[idx].getName().equalsIgnoreCase(name))
600: break;
601: }
602:
603: if (idx == headers.length)
604: headers = Util.resizeArray(headers, idx + 1);
605:
606: headers[idx] = new NVPair(name, value);
607: }
608:
609: /**
610: * Gets the value of a given request header.
611: *
612: * @param name the name of the header.
613: * @return the value part of the header, or null if no such header.
614: */
615: public String getRequestProperty(String name) {
616: for (int idx = 0; idx < headers.length; idx++) {
617: if (headers[idx].getName().equalsIgnoreCase(name))
618: return headers[idx].getValue();
619: }
620:
621: return null;
622: }
623:
624: /**
625: * Sets an arbitrary default request header. All headers set here are
626: * automatically sent with each request.
627: *
628: * @param name the name of the header.
629: * @param value the value for the header.
630: */
631: public static void setDefaultRequestProperty(String name,
632: String value) {
633: if (DebugURLC)
634: System.err
635: .println("URLC: Setting default request property: "
636: + name + " : " + value);
637:
638: int idx;
639: for (idx = 0; idx < default_headers.length; idx++) {
640: if (default_headers[idx].getName().equalsIgnoreCase(name))
641: break;
642: }
643:
644: if (idx == default_headers.length)
645: default_headers = Util
646: .resizeArray(default_headers, idx + 1);
647:
648: default_headers[idx] = new NVPair(name, value);
649: }
650:
651: /**
652: * Gets the value for a given default request header.
653: *
654: * @param name the name of the header.
655: * @return the value part of the header, or null if no such header.
656: */
657: public static String getDefaultRequestProperty(String name) {
658: for (int idx = 0; idx < default_headers.length; idx++) {
659: if (default_headers[idx].getName().equalsIgnoreCase(name))
660: return default_headers[idx].getValue();
661: }
662:
663: return null;
664: }
665:
666: /**
667: * Enables or disables the automatic handling of redirection responses
668: * for this instance only. Cannot be called after <code>connect()</code>.
669: *
670: * @param set enables automatic redirection handling if true.
671: */
672: public void setInstanceFollowRedirects(boolean set) {
673: if (connected)
674: throw new IllegalStateException("Already connected!");
675:
676: do_redir = set;
677: }
678:
679: /**
680: * @return true if automatic redirection handling for this instance is
681: * enabled.
682: */
683: public boolean getInstanceFollowRedirects() {
684: return do_redir;
685: }
686:
687: /**
688: * Connects to the server (if connection not still kept alive) and
689: * issues the request.
690: */
691: public synchronized void connect() throws IOException {
692: if (connected)
693: return;
694:
695: if (DebugURLC)
696: System.err.println("URLC: (" + url + ") Connecting ...");
697:
698: // useCaches TBD!!!
699:
700: synchronized (con) {
701: con.setAllowUserInteraction(allowUserInteraction);
702: if (do_redir)
703: con.addModule(redir_mod, 2);
704: else
705: con.removeModule(redir_mod);
706:
707: try {
708: if (output_stream instanceof ByteArrayOutputStream)
709: resp = con.ExtensionMethod(method, resource,
710: ((ByteArrayOutputStream) output_stream)
711: .toByteArray(), headers);
712: else
713: resp = con.ExtensionMethod(method, resource,
714: (HttpOutputStream) output_stream, headers);
715: } catch (ModuleException e) {
716: throw new IOException(e.toString());
717: }
718: }
719:
720: connected = true;
721: }
722:
723: /**
724: * Closes all the connections to this server.
725: */
726: public void disconnect() {
727: if (DebugURLC)
728: System.err
729: .println("URLC: (" + url + ") Disconnecting ...");
730:
731: con.stop();
732: }
733:
734: /**
735: * Shows if request are being made through an http proxy or directly.
736: *
737: * @return true if an http proxy is being used.
738: */
739: public boolean usingProxy() {
740: return (con.getProxyHost() != null);
741: }
742:
743: /**
744: * produces a string.
745: * @return a string containing the HttpURLConnection
746: */
747: public String toString() {
748: return getClass().getName() + "[" + url + "]";
749: }
750: }
|