/*
* @(#)HttpURLConnection.java 0.3-3 06/05/2001
*
* This file is part of the HTTPClient package
* Copyright (C) 1996-2001 Ronald Tschal�r
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free
* Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307, USA
*
* For questions, suggestions, bug-reports, enhancement-requests etc.
* I may be contacted at:
*
* ronald@innovation.ch
*
* The HTTPClient's home page is located at:
*
* http://www.innovation.ch/java/HTTPClient/
*
*/
package org.exoplatform.common.http.client;
import org.exoplatform.commons.utils.PrivilegedSystemHelper;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URL;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
/**
* This class is a wrapper around HTTPConnection providing the interface defined
* by java.net.URLConnection and java.net.HttpURLConnection.
* <P>
* This class can be used to replace the HttpClient in the JDK with this
* HTTPClient by defining the property
* <code>java.protocol.handler.pkgs=HTTPClient</code>.
* <P>
* One difference between Sun's HttpClient and this one is that this one will
* provide you with a real output stream if possible. This leads to two changes:
* you should set the request property "Content-Length", if possible, before
* invoking getOutputStream(); and in many cases getOutputStream() implies
* connect(). This should be transparent, though, apart from the fact that you
* can't change any headers or other settings anymore once you've gotten the
* output stream. So, for large data do:
*
* <PRE>
* HttpURLConnection con = (HttpURLConnection) url.openConnection();
* con.setDoOutput(true);
* con.setRequestProperty("Content-Length", ...);
* OutputStream out = con.getOutputStream();
* out.write(...);
* out.close();
* if (con.getResponseCode() != 200)
* ...
* </PRE>
* <P>
* The HTTPClient will send the request data using the chunked transfer encoding
* when no Content-Length is specified and the server is HTTP/1.1 compatible.
* Because cgi-scripts can't usually handle this, you may experience problems
* trying to POST data. For this reason, whenever the Content-Type is
* application/x-www-form-urlencoded getOutputStream() will buffer the data
* before sending it so as prevent chunking. If you are sending requests with a
* different Content-Type and are experiencing problems then you may want to try
* setting the system property <var>HTTPClient.dontChunkRequests</var> to
* <var>true</var> (this needs to be done either on the command line or
* somewhere in the code before the first URLConnection.openConnection() is
* invoked).
* <P>
* A second potential incompatibility is that the HTTPClient aggresively resuses
* connections, and can do so more often that Sun's client. This can cause
* problems if you send multiple requests, and the first one has a long
* response. In this case (assuming the server allows the connection to be used
* for multiple requests) the responses to second, third, etc request won't be
* received until the first response has been completely read. With Sun's client
* on the other hand you may not experience this, as it may not be able to keep
* the connection open and there may create multiple connections for the
* requests. This allows the responses to the second, third, etc requests to be
* read before the first response has completed. <strong>Note:</strong> whether
* this will happen depends on details of the resource being requested and the
* server. In many cases the HTTPClient and Sun's client will exhibit the same
* behaviour. Also, applications which depend on being able to read the second
* response before the first one has completed must be considered broken,
* because A) this behaviour cannot be relied upon even in Sun's current client,
* and B) Sun's implementation will exhibit the same problem if they ever switch
* to HTTP/1.1.
* @version 0.3-3 06/05/2001
* @author Ronald Tschal�r
* @since V0.3
*/
public class HttpURLConnection extends java.net.HttpURLConnection
{
/** the cache of HTTPConnections */
protected static Hashtable connections = new Hashtable();
/** the current connection */
protected HTTPConnection con;
/** the cached url.toString() */
private String urlString;
/** the resource */
private String resource;
/** the current method */
private String method;
/** has the current method been set via setRequestMethod()? */
private boolean method_set;
/** the default request headers */
private static NVPair[] default_headers = new NVPair[0];
/** the request headers */
private NVPair[] headers;
/** the response */
protected HTTPResponse resp;
/** is the redirection module activated for this instance? */
private boolean do_redir;
/** the RedirectionModule class */
private static Class redir_mod;
/** the output stream used for POST and PUT */
private OutputStream output_stream;
private static final Log log = ExoLogger.getLogger("exo.ws.commons.HttpURLConnection");
static
{
// The default allowUserAction in java.net.URLConnection is
// false.
try
{
if (Boolean.getBoolean("HTTPClient.HttpURLConnection.AllowUI"))
setDefaultAllowUserInteraction(true);
}
catch (SecurityException se)
{
}
// get the RedirectionModule class
try
{
redir_mod = Class.forName("HTTPClient.RedirectionModule");
}
catch (ClassNotFoundException cnfe)
{
throw new NoClassDefFoundError(cnfe.getMessage());
}
// Set the User-Agent if the http.agent property is set
try
{
String agent = PrivilegedSystemHelper.getProperty("http.agent");
if (agent != null)
setDefaultRequestProperty("User-Agent", agent);
}
catch (SecurityException se)
{
}
}
// Constructors
private static String non_proxy_hosts = "";
private static String proxy_host = "";
private static int proxy_port = -1;
/**
* Construct a connection to the specified url. A cache of HTTPConnections is
* used to maximize the reuse of these across multiple HttpURLConnections. <BR>
* The default method is "GET".
* @param url the url of the request
* @exception ProtocolNotSuppException if the protocol is not supported
*/
public HttpURLConnection(URL url) throws ProtocolNotSuppException, IOException
{
super(url);
// first read proxy properties and set
try
{
String hosts = PrivilegedSystemHelper.getProperty("http.nonProxyHosts", "");
if (!hosts.equalsIgnoreCase(non_proxy_hosts))
{
connections.clear();
non_proxy_hosts = hosts;
String[] list = Util.splitProperty(hosts);
for (int idx = 0; idx < list.length; idx++)
HTTPConnection.dontProxyFor(list[idx]);
}
}
catch (ParseException pe)
{
throw new IOException(pe.toString());
}
catch (SecurityException se)
{
}
try
{
String host = PrivilegedSystemHelper.getProperty("http.proxyHost", "");
int port = Integer.getInteger("http.proxyPort", -1).intValue();
if (!host.equalsIgnoreCase(proxy_host) || port != proxy_port)
{
connections.clear();
proxy_host = host;
proxy_port = port;
HTTPConnection.setProxyServer(host, port);
}
}
catch (SecurityException se)
{
}
// now setup stuff
con = getConnection(url);
method = "GET";
method_set = false;
resource = url.getFile();
headers = default_headers;
do_redir = getFollowRedirects();
output_stream = null;
urlString = url.toString();
}
/**
* Returns an HTTPConnection. A cache of connections is kept and first
* consulted; only when the cache lookup fails is a new one created and added
* to the cache.
* @param url the url
* @return an HTTPConnection
* @exception ProtocolNotSuppException if the protocol is not supported
*/
protected HTTPConnection getConnection(URL url) throws ProtocolNotSuppException
{
// try the cache, using the host name
String php =
url.getProtocol() + ":" + url.getHost() + ":"
+ ((url.getPort() != -1) ? url.getPort() : URI.defaultPort(url.getProtocol()));
php = php.toLowerCase();
HTTPConnection con = (HTTPConnection)connections.get(php);
if (con != null)
return con;
// Not in cache, so create new one and cache it
con = new HTTPConnection(url);
connections.put(php, con);
return con;
}
// Methods
/**
* Sets the request method (e.g. "PUT" or "HEAD"). Can only be set before
* connect() is called.
* @param method the http method.
* @exception ProtocolException if already connected.
*/
@Override
public void setRequestMethod(String method) throws ProtocolException
{
if (connected)
throw new ProtocolException("Already connected!");
if (log.isDebugEnabled())
log.debug(urlString + " Setting request method: " + method);
this.method = method.trim().toUpperCase();
method_set = true;
}
/**
* Return the request method used.
* @return the http method.
*/
@Override
public String getRequestMethod()
{
return method;
}
/**
* Get the response code. Calls connect() if not connected.
* @return the http response code returned.
*/
@Override
public int getResponseCode() throws IOException
{
if (!connected)
connect();
try
{
return resp.getStatusCode();
}
catch (ModuleException me)
{
throw new IOException(me.toString());
}
}
/**
* Get the response message describing the response code. Calls connect() if
* not connected.
* @return the http response message returned with the response code.
*/
@Override
public String getResponseMessage() throws IOException
{
if (!connected)
connect();
try
{
return resp.getReasonLine();
}
catch (ModuleException me)
{
throw new IOException(me.toString());
}
}
/**
* Get the value part of a header. Calls connect() if not connected.
* @param name the of the header.
* @return the value of the header, or null if no such header was returned.
*/
@Override
public String getHeaderField(String name)
{
try
{
if (!connected)
connect();
return resp.getHeader(name);
}
catch (Exception e)
{
return null;
}
}
/**
* Get the value part of a header and converts it to an int. If the header
* does not exist or if its value could not be converted to an int then the
* default is returned. Calls connect() if not connected.
* @param name the of the header.
* @param def the default value to return in case of an error.
* @return the value of the header, or null if no such header was returned.
*/
@Override
public int getHeaderFieldInt(String name, int def)
{
try
{
if (!connected)
connect();
return resp.getHeaderAsInt(name);
}
catch (Exception e)
{
return def;
}
}
/**
* Get the value part of a header, interprets it as a date and converts it to
* a long representing the number of milliseconds since 1970. If the header
* does not exist or if its value could not be converted to a date then the
* default is returned. Calls connect() if not connected.
* @param name the of the header.
* @param def the default value to return in case of an error.
* @return the value of the header, or def in case of an error.
*/
@Override
public long getHeaderFieldDate(String name, long def)
{
try
{
if (!connected)
connect();
return resp.getHeaderAsDate(name).getTime();
}
catch (Exception e)
{
return def;
}
}
private String[] hdr_keys, hdr_values;
/**
* Gets header name of the n-th header. Calls connect() if not connected. The
* name of the 0-th header is <var>null</var>, even though it the 0-th header
* has a value.
* @param n which header to return.
* @return the header name, or null if not that many headers.
*/
@Override
public String getHeaderFieldKey(int n)
{
if (hdr_keys == null)
fill_hdr_arrays();
if (n >= 0 && n < hdr_keys.length)
return hdr_keys[n];
else
return null;
}
/**
* Gets header value of the n-th header. Calls connect() if not connected. The
* value of 0-th header is the Status-Line (e.g. "HTTP/1.1 200 Ok").
* @param n which header to return.
* @return the header value, or null if not that many headers.
*/
@Override
public String getHeaderField(int n)
{
if (hdr_values == null)
fill_hdr_arrays();
if (n >= 0 && n < hdr_values.length)
return hdr_values[n];
else
return null;
}
/**
* Cache the list of headers.
*/
private void fill_hdr_arrays()
{
try
{
if (!connected)
connect();
// count number of headers
int num = 1;
Enumeration en = resp.listHeaders();
while (en.hasMoreElements())
{
num++;
en.nextElement();
}
// allocate arrays
hdr_keys = new String[num];
hdr_values = new String[num];
// fill arrays
en = resp.listHeaders();
for (int idx = 1; idx < num; idx++)
{
hdr_keys[idx] = (String)en.nextElement();
hdr_values[idx] = resp.getHeader(hdr_keys[idx]);
}
// the 0'th field is special
hdr_values[0] = resp.getVersion() + " " + resp.getStatusCode() + " " + resp.getReasonLine();
}
catch (Exception e)
{
hdr_keys = hdr_values = new String[0];
}
}
/**
* Gets an input stream from which the data in the response may be read. Calls
* connect() if not connected.
* @return an InputStream
* @exception ProtocolException if input not enabled.
* @see java.net.URLConnection#setDoInput(boolean)
*/
@Override
public InputStream getInputStream() throws IOException
{
if (!doInput)
throw new ProtocolException("Input not enabled! (use setDoInput(true))");
if (!connected)
connect();
InputStream stream;
try
{
stream = resp.getInputStream();
}
catch (ModuleException e)
{
throw new IOException(e.toString());
}
return stream;
}
/**
* Returns the error stream if the connection failed but the server sent
* useful data nonetheless.
* <P>
* This method will not cause a connection to be initiated.
* @return an InputStream, or null if either the connection hasn't been
* established yet or no error occured
* @see java.net.HttpURLConnection#getErrorStream()
* @since V0.3-1
*/
@Override
public InputStream getErrorStream()
{
try
{
if (!doInput || !connected || resp.getStatusCode() < 300 || resp.getHeaderAsInt("Content-length") <= 0)
return null;
return resp.getInputStream();
}
catch (Exception e)
{
return null;
}
}
/**
* Gets an output stream which can be used send an entity with the request.
* Can be called multiple times, in which case always the same stream is
* returned.
* <P>
* The default request method changes to "POST" when this method is called.
* Cannot be called after connect().
* <P>
* If no Content-type has been set it defaults to
* <var>application/x-www-form-urlencoded</var>. Furthermore, if the
* Content-type is <var>application/x-www-form-urlencoded</var> then all
* output will be collected in a buffer before sending it to the server;
* otherwise an HttpOutputStream is used.
* @return an OutputStream
* @exception ProtocolException if already connect()'ed, if output is not
* enabled or if the request method does not support output.
* @see java.net.URLConnection#setDoOutput(boolean)
* @see HTTPClient.HttpOutputStream
*/
@Override
public synchronized OutputStream getOutputStream() throws IOException
{
if (connected)
throw new ProtocolException("Already connected!");
if (!doOutput)
throw new ProtocolException("Output not enabled! (use setDoOutput(true))");
if (!method_set)
method = "POST";
else if (method.equals("HEAD") || method.equals("GET") || method.equals("TRACE"))
throw new ProtocolException("Method " + method + " does not support output!");
if (getRequestProperty("Content-type") == null)
setRequestProperty("Content-type", "application/x-www-form-urlencoded");
if (output_stream == null)
{
if (log.isDebugEnabled())
log.debug(urlString + " creating output stream");
String cl = getRequestProperty("Content-Length");
if (cl != null)
output_stream = new HttpOutputStream(Integer.parseInt(cl.trim()));
else
{
// Hack: because of restrictions when using true output streams
// and because form-data is usually quite limited in size, we
// first collect all data before sending it if this is
// form-data.
if (getRequestProperty("Content-type").equals("application/x-www-form-urlencoded"))
output_stream = new ByteArrayOutputStream(300);
else
output_stream = new HttpOutputStream();
}
if (output_stream instanceof HttpOutputStream)
connect();
}
return output_stream;
}
/**
* Gets the url for this connection. If we're connect()'d and the request was
* redirected then the url returned is that of the final request.
* @return the final url, or null if any exception occured.
*/
@Override
public URL getURL()
{
if (connected)
{
try
{
return resp.getEffectiveURI().toURL();
}
catch (Exception e)
{
return null;
}
}
return url;
}
/**
* Sets the <var>If-Modified-Since</var> header.
* @param time the number of milliseconds since 1970.
*/
@Override
public void setIfModifiedSince(long time)
{
super.setIfModifiedSince(time);
setRequestProperty("If-Modified-Since", Util.httpDate(new Date(time)));
}
/**
* Sets an arbitrary request header.
* @param name the name of the header.
* @param value the value for the header.
*/
@Override
public void setRequestProperty(String name, String value)
{
if (log.isDebugEnabled())
log.debug(urlString + " Setting request property: " + name + " : " + value);
int idx;
for (idx = 0; idx < headers.length; idx++)
{
if (headers[idx].getName().equalsIgnoreCase(name))
break;
}
if (idx == headers.length)
headers = Util.resizeArray(headers, idx + 1);
headers[idx] = new NVPair(name, value);
}
/**
* Gets the value of a given request header.
* @param name the name of the header.
* @return the value part of the header, or null if no such header.
*/
@Override
public String getRequestProperty(String name)
{
for (int idx = 0; idx < headers.length; idx++)
{
if (headers[idx].getName().equalsIgnoreCase(name))
return headers[idx].getValue();
}
return null;
}
/**
* Sets an arbitrary default request header. All headers set here are
* automatically sent with each request.
* @param name the name of the header.
* @param value the value for the header.
*/
public static void setDefaultRequestProperty(String name, String value)
{
if (log.isDebugEnabled())
log.debug("Setting default request property: " + name + " : " + value);
int idx;
for (idx = 0; idx < default_headers.length; idx++)
{
if (default_headers[idx].getName().equalsIgnoreCase(name))
break;
}
if (idx == default_headers.length)
default_headers = Util.resizeArray(default_headers, idx + 1);
default_headers[idx] = new NVPair(name, value);
}
/**
* Gets the value for a given default request header.
* @param name the name of the header.
* @return the value part of the header, or null if no such header.
*/
public static String getDefaultRequestProperty(String name)
{
for (int idx = 0; idx < default_headers.length; idx++)
{
if (default_headers[idx].getName().equalsIgnoreCase(name))
return default_headers[idx].getValue();
}
return null;
}
/**
* Enables or disables the automatic handling of redirection responses for
* this instance only. Cannot be called after <code>connect()</code>.
* @param set enables automatic redirection handling if true.
*/
@Override
public void setInstanceFollowRedirects(boolean set)
{
if (connected)
throw new IllegalStateException("Already connected!");
do_redir = set;
}
/**
* @return true if automatic redirection handling for this instance is
* enabled.
*/
@Override
public boolean getInstanceFollowRedirects()
{
return do_redir;
}
/**
* Connects to the server (if connection not still kept alive) and issues the
* request.
*/
@Override
public synchronized void connect() throws IOException
{
if (connected)
return;
if (log.isDebugEnabled())
log.debug(urlString + " Connecting ...");
// useCaches TBD!!!
synchronized (con)
{
con.setAllowUserInteraction(allowUserInteraction);
if (do_redir)
con.addModule(redir_mod, 2);
else
con.removeModule(redir_mod);
try
{
if (output_stream instanceof ByteArrayOutputStream)
resp =
con.ExtensionMethod(method, resource, ((ByteArrayOutputStream)output_stream).toByteArray(), headers);
else
resp = con.ExtensionMethod(method, resource, (HttpOutputStream)output_stream, headers);
}
catch (ModuleException e)
{
throw new IOException(e.toString());
}
}
connected = true;
}
/**
* Closes all the connections to this server.
*/
@Override
public void disconnect()
{
if (log.isDebugEnabled())
log.debug(urlString + " Disconnecting ...");
con.stop();
}
/**
* Shows if request are being made through an http proxy or directly.
* @return true if an http proxy is being used.
*/
@Override
public boolean usingProxy()
{
return (con.getProxyHost() != null);
}
/**
* produces a string.
* @return a string containing the HttpURLConnection
*/
@Override
public String toString()
{
return getClass().getName() + "[" + url + "]";
}
}