/**
 * Copyright 2010 JBoss Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.eclipse.webdav.http.client;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import org.eclipse.webdav.IContext;
import org.eclipse.webdav.client.WebDAVFactory;
import org.eclipse.webdav.internal.authentication.AuthorizationAuthority;
import org.eclipse.webdav.internal.kernel.Context;
import org.eclipse.webdav.internal.kernel.utils.Assert;

/**
 * <p>An HTTP 1.0 or 1.1 client.  Single instances of this client enable
 * users to talk with multiple origin servers.  Moreover, connections
 * to origin servers are maintained for reuse.
 *
 * <p>Conveniences are provided for managing proxy servers, handling
 * authentication, and setting up request headers.
 *
 * <p>Here is some sample code:
 *
 * <code>
 * HttpClient client = new HttpClient();
 * try {
 * 	Request request = null;
 * 	Response response = null;
 * 	try {
 * 		URL resourceUrl = new URL("http://hostname/index.html");
 * 		request = new Request("GET", resourceUrl, (Context) null);
 * 		response = client.invoke(request);
 * 		System.out.print(response);
 * 		InputStream is = response.getInputStream();
 * 		int c;
 * 		while ((c = is.read()) != -1) {
 * 			System.out.print((char) c);
 * 		}
 * 	} catch (IOException e) {
 * 		e.printStackTrace();
 * 	} finally {
 * 		if (request != null) {
 * 			try {
 * 				request.close();
 * 			} catch (IOException e) {
 * 				e.printStackTrace();
 * 			}
 * 		}
 * 		if (response != null) {
 * 			try {
 * 				response.close();
 * 			} catch (IOException e) {
 * 				e.printStackTrace();
 * 			}
 * 		}
 * 	}
 * } finally {
 * 	client.close();
 * }
 * </code>
 * <p>
 * <b>Note:</b> This class/interface is part of an interim API that is still under 
 * development and expected to change significantly before reaching stability. 
 * It is being made available at this early stage to solicit feedback from pioneering 
 * adopters on the understanding that any code that uses this API will almost 
 * certainly be broken (repeatedly) as the API evolves.
 * </p>
 */
public class HttpClient implements IStatusCodes {
	/**
	 * The HTTP version of this client.
	 */
	private double httpVersion = 1.1;

	/**
	 * Indicates whether this client is closed.
	 */
	private boolean closed = true;

	/**
	 * The connections recycler is responsible for closing and discarding
	 * unused connections that have become stale.
	 */
	private ConnectionsRecycler connectionsRecycler = new ConnectionsRecycler("Connections Recycler"); //$NON-NLS-1$

	/**
	 * The authorization authority authorizes requests.
	 */
	private AuthorizationAuthority authority = null;

	/**
	 * A <code>Hashtable</code> mapping origin server <code>URL</code>s
	 * to <code>Context</code>s. The contexts in this table are preferred
	 * over the default context.
	 */
	private Hashtable contexts = new Hashtable(5);

	/**
	 * The default context is used when one hasn't been set for a
	 * particular origin server.
	 */
	private IContext defaultContext = null;

	/**
	 * The <code>URL</code> of the default proxy server. The default is
	 * not used if the particular origin server matches a proxy server
	 * exception, or if there is already a proxy server associated with
	 * the origin server.
	 */
	private URL defaultProxyServerUrl = null;

	/**
	 * A <code>Hashtable</code> mapping origin server <code>URL</code>s
	 * to proxy server <code>URL</code>s. If an origin server
	 * <code>URL</code> has an entry in this table, the proxy server
	 * with the mapped <code>URL</code> is used.
	 */
	private Hashtable proxyServerUrls = new Hashtable(5);

	/**
	 * A set of proxy server exception patterns used to bypass the
	 * default proxy server.  Any origin server whos host and port
	 * match the pattern do not use the default proxy server.
	 */
	private Hashtable proxyServerExceptions = new Hashtable(5);

	/**
	 * The maximum number of times a request is retried after an
	 * <code>IOException</code> occurs.
	 */
	private int maxRetries = 1;

	/**
	 * The maximum number of <code>URL</code> location redirects.
	 */
	private int maxRedirects = 4;

	/**
	 * The socket timeout (in milliseconds).
	 */
	private int socketTimeout = 0;

	/**
	 * The factory used to create <code>Socket</code>s.
	 */
	private ISocketFactory socketFactory = null;

	/**
	 * The factory used to create <code>Context</code>s.
	 */
	private WebDAVFactory webDAVFactory = new WebDAVFactory();

	/**
	 * The <code>ConnectionsRecycler</code> manages a collection of
	 * persistent <code>HttpConnection</code>s. Connections that remain
	 * unused for a given period of time are closed and discarded.
	 */
	public class ConnectionsRecycler extends Thread {
		/**
		 * The time (in milliseconds) that a connection remains unused
		 * before it is closed and discared.
		 */
		private long connectionTimeout = 10000;

		/**
		 * A <code>Hashtable</code> who's keys are origin server
		 * <code>URL</code>s that map to vectors of connections that are
		 * currently unused.
		 */
		private Hashtable unusedConnections = new Hashtable(5);

		/**
		 * A <code>Hashtable</code> who's keys are origin server
		 * <code>URL</code>s that map to vectors of connections that are
		 * currently in use.
		 */
		private Hashtable usedConnections = new Hashtable(5);

		/**
		 * Creates a new <code>ConnectionsRecycler</code> with the given
		 * name.
		 *
		 * @param name the name of the new connections recycler
		 */
		public ConnectionsRecycler(String name) {
			super(name);
			this.setDaemon(true);
		}

		/**
		 * Closes this connections recycler. All of its connections are
		 * closed and discarded.
		 */
		public synchronized void close() {
			interrupt();
			closeConnections(unusedConnections);
			closeConnections(usedConnections);
		}

		private synchronized void closeConnections(Hashtable connections) {
			Enumeration originServerUrls = connections.keys();
			while (originServerUrls.hasMoreElements()) {
				URL originServerUrl = (URL) originServerUrls.nextElement();
				Vector connectionsVector = (Vector) connections.get(originServerUrl);
				Enumeration connectionsEnum = connectionsVector.elements();
				while (connectionsEnum.hasMoreElements()) {
					HttpConnection connection = (HttpConnection) connectionsEnum.nextElement();
					try {
						connection.close();
					} catch (IOException e) {
						// ignore
					}
				}
			}
			connections.clear();
		}

		/**
		 * Returns an unused connection that is connected to the origin
		 * server at the given <code>URL</code>. The connection is marked
		 * as in use before it is returned.
		 *
		 * @param originServerUrl the <code>URL</code> of an origin
		 * server
		 * @return an unused connection that is connected to the origin
		 * server at the given <code>URL</code>
		 * @see #putConnection(HttpConnection)
		 */
		public synchronized HttpConnection getConnection(URL originServerUrl) {
			HttpConnection connection = null;

			Vector unusedConnections = (Vector) this.unusedConnections.get(originServerUrl);
			if (unusedConnections == null || unusedConnections.isEmpty()) {
				connection = new HttpConnection(originServerUrl);
			} else {
				connection = (HttpConnection) unusedConnections.lastElement();
				unusedConnections.removeElementAt(unusedConnections.size() - 1);
			}

			Vector usedConnections = (Vector) this.usedConnections.get(originServerUrl);
			if (usedConnections == null) {
				usedConnections = new Vector(5);
				this.usedConnections.put(originServerUrl, usedConnections);
			}

			usedConnections.addElement(connection);
			connection.setTimestamp(new Date());

			return connection;
		}

		public long getConnectionTimeout() {
			return connectionTimeout;
		}

		/**
		 * Marks the given connection that is in use as unused.
		 *
		 * @param connection a connection that is in use
		 * @see #getConnection(URL)
		 */
		public synchronized void putConnection(HttpConnection connection) {
			URL resourceUrl = connection.getResourceUrl();

			URL originServerUrl = null;
			try {
				originServerUrl = new URL(resourceUrl.getProtocol(), resourceUrl.getHost(), resourceUrl.getPort(), "/"); //$NON-NLS-1$
			} catch (MalformedURLException e) {
				// ignore?
			}

			Vector usedConnections = (Vector) this.usedConnections.get(originServerUrl);
			usedConnections.remove(connection);

			Vector unusedConnections = (Vector) this.unusedConnections.get(originServerUrl);
			if (unusedConnections == null) {
				unusedConnections = new Vector(5);
				this.unusedConnections.put(originServerUrl, unusedConnections);
			}

			unusedConnections.addElement(connection);
			connection.setTimestamp(new Date());
		}

		private synchronized void recycle() {
			Vector staleOriginServerUrls = new Vector(3);
			Enumeration originServerUrls = unusedConnections.keys();
			while (originServerUrls.hasMoreElements()) {
				URL originServerUrl = (URL) originServerUrls.nextElement();
				Vector connectionsVector = (Vector) unusedConnections.get(originServerUrl);

				if (connectionsVector.isEmpty()) {
					staleOriginServerUrls.add(originServerUrl);
					break;
				}

				Vector staleConnections = new Vector(5);
				Enumeration connectionsEnum = connectionsVector.elements();
				while (connectionsEnum.hasMoreElements()) {
					HttpConnection connection = (HttpConnection) connectionsEnum.nextElement();

					long currentTime = System.currentTimeMillis();
					long connectionTime = connection.getTimestamp().getTime();

					if (currentTime - connectionTime >= connectionTimeout) {
						staleConnections.addElement(connection);
					}
				} // end-while

				connectionsEnum = staleConnections.elements();
				while (connectionsEnum.hasMoreElements()) {
					HttpConnection connection = (HttpConnection) connectionsEnum.nextElement();
					connectionsVector.remove(connection);

					try {
						connection.close();
					} catch (IOException e) {
						// ignore?
					}
				} // end-while
			} // end-while

			Enumeration staleOriginServerUrlsEnum = staleOriginServerUrls.elements();
			while (staleOriginServerUrlsEnum.hasMoreElements()) {
				unusedConnections.remove(staleOriginServerUrlsEnum.nextElement());
			}
		}

		public void run() {
			while (!interrupted()) {
				try {
					Thread.sleep(connectionTimeout);
					recycle();
				} catch (InterruptedException e) {
					interrupt();
				}
			}
		}

		public void setConnectionTimeout(long connectionTimeout) {
			this.connectionTimeout = connectionTimeout;
		}
	}

	/**
	 * This type of input stream is passed back to the user in message
	 * responses.  It serves to keep the stream's connection alive by
	 * returning it to this client's unused connections when the stream
	 * is closed.
	 *
	 * @see Response
	 */
	class PersistentInputStream extends FilterInputStream {
		/**
		 * This input stream's connection.
		 */
		private HttpConnection connection;

		/**
		 * Creates a new persistent input stream on the given connection.
		 *
		 * @param connection this input stream's connection
		 */
		PersistentInputStream(HttpConnection connection) throws IOException {
			super(null);

			try {
				in = connection.getInputStream();
			} catch (IOException e) {
				closeConnection();
				throw e;
			}
			this.connection = connection;
		}

		/**
		 * @see InputStream#available()
		 */
		public int available() throws IOException {
			try {
				return super.available();
			} catch (IOException e) {
				closeConnection();
				throw e;
			}
		}

		/**
		 * @see InputStream#close()
		 */
		public void close() throws IOException {
			try {
				super.close();
			} catch (IOException e) {
				closeConnection();
				throw e;
			} finally {
				connectionsRecycler.putConnection(connection);
			}
		}

		private void closeConnection() {
			try {
				connection.close();
			} catch (IOException e) {
				// ignore?
			}
		}

		/**
		 * @see InputStream#read()
		 */
		public int read() throws IOException {
			try {
				return super.read();
			} catch (IOException e) {
				closeConnection();
				throw e;
			}
		}

		/**
		 * @see InputStream#read(byte[])
		 */
		public int read(byte b[]) throws IOException {
			try {
				return super.read(b);
			} catch (IOException e) {
				closeConnection();
				throw e;
			}
		}

		/**
		 * @see InputStream#read(byte[], int, int)
		 */
		public int read(byte b[], int off, int len) throws IOException {
			try {
				return super.read(b, off, len);
			} catch (IOException e) {
				closeConnection();
				throw e;
			}
		}

		/**
		 * @see InputStream#reset()
		 */
		public synchronized void reset() throws IOException {
			try {
				super.reset();
			} catch (IOException e) {
				closeConnection();
				throw e;
			}
		}

		/**
		 * @see InputStream#skip(long)
		 */
		public long skip(long n) throws IOException {
			try {
				return super.skip(n);
			} catch (IOException e) {
				closeConnection();
				throw e;
			}
		}
	}

	/**
	 * Creates a new <code>HttpClient</code>.
	 */
	public HttpClient() {
		open();
	}

	/**
	 * Adds the given proxy server exception pattern to this client.
	 * Origin servers whose hostname match the pattern do not communicate
	 * through the defualt proxy server.  The pattern must contain zero or
	 * one stars (*).  A star must appear at either the beginning or the
	 * end of the pattern. A star matches zero or more characters. The
	 * following are valid patterns:
	 * <ul>
	 * <li>www.company.com:80</li>
	 * <li>*.company.com</li>
	 * <li>www.company.*</li>
	 * </ul>
	 *
	 * @param pattern a proxy server exception pattern, for example:
	 * "*.company.com".
	 * @see #getDefaultProxyServerUrl()
	 * @see #getProxyServerExceptions()
	 * @see #getProxyServerUrl(URL)
	 * @see #removeProxyServerException(String)
	 * @see #setDefaultProxyServerUrl(URL)
	 * @see #setProxyServerUrl(URL, URL)
	 */
	public void addProxyServerException(String pattern) {
		Assert.isNotNull(pattern);
		proxyServerExceptions.put(pattern, pattern);
	}

	/**
	 * Closes this client.
	 */
	public void close() {
		if (!closed) {
			connectionsRecycler.close();
			closed = true;
		}
	}

	public long getConnectionTimeout() {
		return connectionsRecycler.getConnectionTimeout();
	}

	/**
	 * Returns the context for the origin server at the given
	 * <code>URL</code>.
	 *
	 * @param originServerUrl the <code>URL</code> of an origin server
	 * @return the context for the origin server at the given
	 * <code>URL</code>
	 * @see #getDefaultContext()
	 * @see #setDefaultContext(Context)
	 * @see #setContext(URL, Context)
	 */
	public IContext getContext(URL originServerUrl) {
		Assert.isNotNull(originServerUrl);
		return (IContext) contexts.get(originServerUrl);
	}

	/**
	 * Returns the default context. The default context is used for servers
	 * that do not have a context set. Initially the default context is
	 * <code>null</code>.
	 *
	 * @return the default context, or <code>null</code>
	 * @see #getContext(URL)
	 * @see #setContext(URL, Context)
	 * @see #setDefaultContext(Context)
	 */
	public IContext getDefaultContext() {
		return defaultContext;
	}

	/**
	 * Returns the <code>URL</code> of the default proxy server which is
	 * used for all servers that do not have their proxy server set and
	 * do not match a proxy server exception. If the default proxy server
	 * <code>URL</code> is <code>null</code>, no default proxy server is
	 * used. Initially the default proxy server <code>URL</code> is
	 * <code>null</code>.
	 *
	 * @return the <code>URL</code> of the default proxy server, or
	 * <code>null</code>
	 * @see #addProxyServerException(String)
	 * @see #getProxyServerExceptions()
	 * @see #getProxyServerUrl(URL)
	 * @see #removeProxyServerException(String)
	 * @see #setDefaultProxyServerUrl(URL)
	 * @see #setProxyServerUrl(URL, URL)
	 */
	public URL getDefaultProxyServerUrl() {
		return defaultProxyServerUrl;
	}

	/**
	 * Returns the version of HTTP this client uses for communication with
	 * servers. HTTP/1.1 is used by default.
	 *
	 * @return the version of HTTP this client uses for communication with
	 * servers
	 * @see #setHttpVersion(double)
	 */
	public double getHttpVersion() {
		return httpVersion;
	}

	/**
	 * Returns the maximum number of <code>URL</code> location redirects. The
	 * maximum is 4 by default.
	 *
	 * @return the maximum number of <code>URL</code> location redirects
	 */
	public int getMaxRedirects() {
		return maxRedirects;
	}

	/**
	 * Returns the maximum number of times a request is retried after an
	 * <code>IOException</code> occurs. The maximum is 1 by default.
	 *
	 * @return the maximum number of retries
	 * @see #setMaxRetries(int)
	 */
	public int getMaxRetries() {
		return maxRetries;
	}

	/**
	 * Returns an <code>Enumeration</code> over the origin server
	 * <code>URL</code>s known to this client. The known origin server
	 * <code>URL</code>s are gleaned from this client's mapped contexts and
	 * mapped proxy server <code>URL</code>s.
	 *
	 * @return an <code>Enumeration</code> over the origin server
	 * <code>URL</code>s known to this client
	 * @see #getContext(URL)
	 * @see #setContext(URL, Context)
	 * @see #getProxyServerUrl(URL)
	 * @see #setProxyServerUrl(URL, URL)
	 */
	public Enumeration getOriginServerUrls() {
		final Enumeration enum1 = contexts.keys();
		final Enumeration enum2 = proxyServerUrls.keys();

		Enumeration e = new Enumeration() {
			public boolean hasMoreElements() {
				return enum1.hasMoreElements() || enum2.hasMoreElements();
			}

			public Object nextElement() {
				if (enum1.hasMoreElements())
					return enum1.nextElement();
				return enum2.nextElement();
			}
		};
		return e;
	}

	/**
	 * Returns an <code>Enumeration</code> over this client's proxy server
	 * exception patterns.
	 *
	 * @return an <code>Enumeration</code> over this client's proxy server
	 * exception patterns
	 * @see #addProxyServerException(String)
	 * @see #getDefaultProxyServerUrl()
	 * @see #getProxyServerUrl(URL)
	 * @see #removeProxyServerException(String)
	 * @see #setDefaultProxyServerUrl(URL)
	 * @see #setProxyServerUrl(URL, URL)
	 */
	public Enumeration getProxyServerExceptions() {
		return proxyServerExceptions.keys();
	}

	/**
	 * Returns the <code>URL</code> of the proxy server that the origin
	 * server at the given <code>URL</code> uses, or <code>null</code> if
	 * no proxy server is used.
	 *
	 * @param originServerUrl the <code>URL<code> of an origin server
	 * @return the <code>URL</code> of a proxy server, or <code>null</code>
	 * @see #addProxyServerException(String)
	 * @see #getDefaultProxyServerUrl()
	 * @see #getProxyServerExceptions()
	 * @see #removeProxyServerException(String)
	 * @see #setDefaultProxyServerUrl(URL)
	 * @see #setProxyServerUrl(URL, URL)
	 */
	public URL getProxyServerUrl(URL originServerUrl) {
		Assert.isNotNull(originServerUrl);
		return (URL) proxyServerUrls.get(originServerUrl);
	}

	/**
	 * Returns the socket read timeout (in milliseconds) for this client. A
	 * value of zero indicates that a socket read operation will block
	 * indefinitely waiting for data. The value is zero by default.
	 *
	 * @return the socket read timeout (in milliseconds) for this client
	 * @see #setSoTimeout(int)
	 */
	public int getSoTimeout() {
		return socketTimeout;
	}

	/**
	 * Sends the given request to the server and returns the server's
	 * response.
	 *
	 * @param request the request to send to the server
	 * @return the server's response
	 * @throws IOException if an I/O error occurs. Reasons include:
	 * <ul>
	 * <li>The client is closed.
	 * <li>The client could not connect to the server
	 * <li>An I/O error occurs while communicating with the server
	 * <ul>
	 */
	public Response invoke(Request request) throws IOException {
		Assert.isNotNull(request);

		try {
			open();

			URL resourceUrl = request.getResourceUrl();
			URL originServerUrl = new URL(resourceUrl.getProtocol(), resourceUrl.getHost(), resourceUrl.getPort(), "/"); //$NON-NLS-1$

			URL proxyServerUrl = getProxyServerUrl(originServerUrl);
			if (proxyServerUrl == null && !matchesProxyServerException(originServerUrl)) {
				proxyServerUrl = getDefaultProxyServerUrl();
			}

			IContext context = webDAVFactory.newContext(request.getContext());

			IContext defaultContext = getContext(originServerUrl);
			if (defaultContext == null) {
				defaultContext = getDefaultContext();
			}

			if (defaultContext != null) {
				Enumeration e = defaultContext.keys();
				while (e.hasMoreElements()) {
					String key = (String) e.nextElement();
					context.put(key, defaultContext.get(key));
				}
			}

			if (authority != null) {
				authority.authorize(request, null, context, proxyServerUrl, true);
				authority.authorize(request, null, context, proxyServerUrl, false);
			}

			return invoke1(request, context, proxyServerUrl, originServerUrl, 0, 0);
		} finally {
			request.close();
		}
	}

	private Response invoke1(Request request, IContext context, URL proxyServerUrl, URL originServerUrl, int retries, int redirects) throws IOException {
		Response response = null;

		try {
			response = invoke2(request, context, proxyServerUrl, originServerUrl, false);
			int sc = response.getStatusCode();

			if (sc == HTTP_UNAUTHORIZED || sc == HTTP_PROXY_AUTHENTICATION_REQUIRED) {
				if (authority == null || !authority.authorize(request, response, context, proxyServerUrl, sc == IStatusCodes.HTTP_PROXY_AUTHENTICATION_REQUIRED)) {
					return response;
				}
				response.close();
				return invoke1(request, context, proxyServerUrl, originServerUrl, 0, 0);

			} else if (sc >= 300 && sc < 400) {
				if (redirects >= maxRedirects) {
					return response;
				}

				String location = response.getContext().getLocation();
				if (location == null) {
					return response;
				}

				URL url = new URL(location);

				if (sc == HTTP_USE_PROXY) {
					proxyServerUrl = url;
				} else {
					originServerUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "/"); //$NON-NLS-1$
					request.setResourceUrl(url);
				}

				response.close();

				return invoke1(request, context, proxyServerUrl, originServerUrl, 0, redirects + 1);
			}

			if (authority != null) {
				authority.confirm(request, response, proxyServerUrl);
			}

			return response;
		} catch (IOException e) {
			if (response != null) {
				response.close();
			}

			if (retries < maxRetries) {
				return invoke1(request, context, proxyServerUrl, originServerUrl, retries + 1, 0);
			}

			throw e;
		}
	}

	private Response invoke2(Request request, IContext context, URL proxyServerUrl, URL originServerUrl, boolean expect100Continue) throws IOException {
		HttpConnection connection = null;

		try {
			// get the connection
			connection = connectionsRecycler.getConnection(originServerUrl);

			// set the http version
			connection.setHttpVersion(httpVersion);

			// set the method, resource url, and proxy server url
			connection.setRequestMethod(request.getMethod());
			connection.setResourceUrl(request.getResourceUrl());
			connection.setProxyServerUrl(proxyServerUrl);

			// set the request header
			connection.clearRequestHeader();
			Enumeration e = context.keys();
			while (e.hasMoreElements()) {
				String key = (String) e.nextElement();
				connection.setRequestHeaderField(key, context.get(key));
			}

			// set the content length
			long contentLength = request.getContentLength();
			if (contentLength >= 0 && context.getContentLength() == -1) {
				connection.setRequestHeaderField("Content-Length", "" + contentLength); //$NON-NLS-1$ //$NON-NLS-2$
			}

			// set the chunked encoding
			String method = request.getMethod();
			boolean sendChunked = contentLength == -1 && httpVersion > 0 && !(method.equals("PROPPATCH") || method.equals("PROPFIND")); //$NON-NLS-1$ //$NON-NLS-2$
			connection.setSendChunked(sendChunked);

			// make the connection persistent
			connection.setPersistent(httpVersion > 0);

			// set the socket timeout
			connection.setSoTimeout(socketTimeout);

			// set the socket factory
			connection.setSocketFactory(socketFactory);

			// send the request header
			IContext responseHeader = null;
			if (expect100Continue && (contentLength > 0 && httpVersion > 0 || sendChunked)) {
				if (!"100-continue".equalsIgnoreCase(context.get("Expect"))) { //$NON-NLS-1$ //$NON-NLS-2$
					connection.setRequestHeaderField("Expect", "100-continue"); //$NON-NLS-1$ //$NON-NLS-2$
				}
				responseHeader = readResponseHeader(connection);
				if (connection.getStatusCode() != IStatusCodes.HTTP_CONTINUE) {
					return new Response(connection.getStatusCode(), connection.getStatusMessage(), responseHeader, new PersistentInputStream(connection));
				}
			}

			// send the request body
			if (contentLength != 0) {
				OutputStream os = connection.getOutputStream();
				request.write(os);
				os.close();
			}

			// get the response header
			responseHeader = readResponseHeader(connection);

			// return the response
			return new Response(connection.getStatusCode(), connection.getStatusMessage(), responseHeader, new PersistentInputStream(connection));
		} catch (IOException e1) {
			try {
				connection.close();
			} catch (IOException e2) {
				// ignore?
			}
			connectionsRecycler.putConnection(connection);

			throw e1;
		} // end-catch
	}

	private boolean matchesProxyServerException(URL originServerUrl) {
		String host = originServerUrl.getHost();
		int port = originServerUrl.getPort();
		String originServerUrlString = host + (port == -1 ? "" : ":" + port); //$NON-NLS-1$ //$NON-NLS-2$

		boolean found = false;
		Enumeration keys = proxyServerExceptions.keys();

		while (!found && keys.hasMoreElements()) {
			String httpProxyException = (String) keys.nextElement();
			int len = httpProxyException.length();
			found = originServerUrlString.equals(httpProxyException);
			if (!found) {
				if (httpProxyException.startsWith("*")) { //$NON-NLS-1$
					found = originServerUrlString.endsWith(httpProxyException.substring(1, len));
				} else if (httpProxyException.endsWith("*")) { //$NON-NLS-1$
					found = originServerUrlString.startsWith(httpProxyException.substring(0, len - 1));
				}
			}
		}

		return found;
	}

	private void open() {
		if (closed) {
			connectionsRecycler.start();
			closed = false;
		}
	}

	private IContext readResponseHeader(HttpConnection httpConnection) throws IOException {
		IContext responseHeader = webDAVFactory.newContext();

		int position = 0;
		String fieldName = null;
		while ((fieldName = httpConnection.getResponseHeaderFieldName(position)) != null) {
			// TBD : Should combine multiple headers
			if (responseHeader.get(fieldName.toLowerCase()) == null)
				responseHeader.put(fieldName.toLowerCase(), httpConnection.getResponseHeaderFieldValue(position));
			++position;
		}

		return responseHeader;
	}

	/**
	 * Removes the given proxy server exception pattern from this client.
	 *
	 * @param pattern a proxy server exception pattern
	 * @see #addProxyServerException(String)
	 * @see #getDefaultProxyServerUrl()
	 * @see #getProxyServerExceptions()
	 * @see #getProxyServerUrl(URL)
	 * @see #setDefaultProxyServerUrl(URL)
	 * @see #setProxyServerUrl(URL, URL)
	 */
	public void removeProxyServerException(String pattern) {
		Assert.isNotNull(pattern);
		proxyServerExceptions.remove(pattern);
	}

	/**
	 * Sets the authenticator. Authenticators store authentication
	 * information and are required to access protected resources. If the
	 * authenticator is <code>null</code> protected resources cannot be
	 * accessed. The authenticator is <code>null</code> by default.
	 *
	 * @param authenticator the authenticator, or <code>null</code>
	 */
	public void setAuthenticator(IAuthenticator authenticator) {
		authority = authenticator == null ? null : new AuthorizationAuthority(authenticator);
	}

	public void setConnectionTimeout(long connectionTimeout) {
		connectionsRecycler.setConnectionTimeout(connectionTimeout);
	}

	/**
	 * Set the context for the origin server at the given <code>URL</code>.
	 * If the given context is <code>null</code>, the context for the
	 * specified origin server is removed.
	 *
	 * @param originServerUrl the <code>URL</code> of an origin server
	 * @param context the context for the specified origin server
	 * @see #getContext(URL)
	 * @see #getDefaultContext()
	 * @see #setDefaultContext(Context)
	 */
	public void setContext(URL originServerUrl, IContext context) {
		Assert.isNotNull(originServerUrl);
		if (context == null)
			contexts.remove(originServerUrl);
		else
			contexts.put(originServerUrl, context);
	}

	/**
	 * Sets the default context which is used by all servers that do not
	 * already have a context set.
	 *
	 * @param context the default context
	 * @see #getContext(URL)
	 * @see #getDefaultContext()
	 * @see #setContext(URL, Context)
	 */
	public void setDefaultContext(IContext context) {
		defaultContext = context;
	}

	/**
	 * Sets the <code>URL</code> of the default proxy server that is used by
	 * all servers that do not have their proxy server set and do not match
	 * a proxy server exception pattern. If the given proxy server
	 * <code>URL</code> is <code>null</code>, no default proxy server is
	 * used.
	 *
	 * @param proxyServerUrl the <code>URL</code> of the default proxy server
	 * @see #addProxyServerException(String)
	 * @see #getDefaultProxyServerUrl()
	 * @see #getProxyServerExceptions()
	 * @see #getProxyServerUrl(URL)
	 * @see #removeProxyServerException(String)
	 * @see #setProxyServerUrl(URL, URL)
	 */
	public void setDefaultProxyServerUrl(URL proxyServerUrl) {
		defaultProxyServerUrl = proxyServerUrl;
	}

	/**
	 * Sets the version of HTTP this client uses for communication with
	 * servers. HTTP/1.1 is used by default.
	 *
	 * @param httpVersion the version of HTTP this client uses for
	 * communication with servers
	 * @see #getHttpVersion()
	 */
	public void setHttpVersion(double httpVersion) {
		Assert.isTrue(httpVersion == 1.0 || httpVersion == 1.1);
		this.httpVersion = httpVersion;
	}

	/**
	 * Sets the maximum number of <code>URL</code> location redirects. The
	 * maximum is 4 by default.
	 *
	 * @param maxRedirects the maximum number of <code>URL</code> redirects
	 */
	public void setMaxRedirects(int maxRedirects) {
		Assert.isTrue(maxRedirects >= 0);
		this.maxRedirects = maxRedirects;
	}

	/**
	 * Sets the maximum number of times a request is retried after an
	 * <code>IOException</code> occurs. The maximum is 1 by default.
	 *
	 * @param maxRetries the maximum number of times a request is retried
	 * after an <code>IOException</code> occurs
	 * @see #getMaxRetries()
	 */
	public void setMaxRetries(int maxRetries) {
		Assert.isTrue(maxRetries >= 0);
		this.maxRetries = maxRetries;
	}

	/**
	 * Sets the <code>URL</code> of the proxy server that this client uses
	 * to communicate with the origin server at the given <code>URL</code>.
	 * If the proxy server <code>URL</code> is <code>null</code>, the
	 * default proxy server is used if the specified origin server does not
	 * match a proxy server exception pattern.
	 *
	 * @param originServerUrl the <code>URL</code> of on origin server
	 * @param proxyServerUrl the <code>URL</code> of a proxy server, or
	 * <code>null</code>
	 * @see #addProxyServerException(String)
	 * @see #getDefaultProxyServerUrl()
	 * @see #getProxyServerExceptions()
	 * @see #getProxyServerUrl(URL)
	 * @see #removeProxyServerException(String)
	 * @see #setDefaultProxyServerUrl(URL)
	 */
	public void setProxyServerUrl(URL originServerUrl, URL proxyServerUrl) {
		Assert.isNotNull(originServerUrl);
		if (proxyServerUrl == null)
			proxyServerUrls.remove(originServerUrl);
		else
			proxyServerUrls.put(originServerUrl, proxyServerUrl);
	}

	/**
	 * Sets the factory that this client uses to create sockets.
	 *
	 * @param socketFactory the factory that this client uses to create
	 * sockets
	 */
	public void setSocketFactory(ISocketFactory socketFactory) {
		this.socketFactory = socketFactory;
	}

	/**
	 * Sets the socket read timeout (in milliseconds) for this client. A
	 * value of zero indicates that a socket read operation will block
	 * indefinitely waiting for data. The value is zero by default.
	 *
	 * @param timeout the socket read timeout
	 * @see #getSoTimeout()
	 */
	public void setSoTimeout(int timeout) {
		socketTimeout = timeout;
	}
}
