/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * 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.guvnor.tools;

import java.net.MalformedURLException;
import java.net.URL;

import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;

/**
 * The activator class controls the plug-in life cycle
 */
public class Activator extends AbstractUIPlugin {

    // The plug-in ID
    public static final String PLUGIN_ID = "org.drools.guvnor"; //$NON-NLS-1$

    // The shared instance
    private static Activator plugin;

    private static GuvnorLocationManager locManager;

    private ResourceChangeListener rcListner;
    /**
     * The constructor
     */
    public Activator() {
        rcListner = new ResourceChangeListener();
    }

    public static GuvnorLocationManager getLocationManager() {
        if (locManager == null) {
            locManager = new GuvnorLocationManager();
        }
        return locManager;
    }
    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
     */
    public void start(BundleContext context) throws Exception {
        super.start(context);
        plugin = this;
        getWorkspace().addResourceChangeListener(rcListner);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
     */
    public void stop(BundleContext context) throws Exception {
        plugin = null;
        super.stop(context);
        getWorkspace().removeResourceChangeListener(rcListner);

    }

    /**
     * Returns the shared instance
     *
     * @return the shared instance
     */
    public static Activator getDefault() {
        return plugin;
    }

    public IWorkspace getWorkspace() {
        return ResourcesPlugin.getWorkspace();
    }

    private IStatus createStatus(int severity, String msg, Throwable t) {
        return new Status(severity, PLUGIN_ID, msg, t);
    }

    public void writeLog(int severity, String msg, Throwable t) {
        super.getLog().log(createStatus(severity, msg, t));
    }

    public void writeLog(IStatus status) {
        super.getLog().log(status);
    }

    public void displayMessage(final int severity, final String msg) {
        final Display display = PlatformUI.getWorkbench().getDisplay();
        display.syncExec(new Runnable() {
            public void run() {
                switch (severity) {
                    case IStatus.ERROR:
                        MessageDialog.openError(display.getActiveShell(),
                                            Messages.getString("error"), msg); //$NON-NLS-1$
                    break;
                    case IStatus.WARNING:
                        MessageDialog.openWarning(display.getActiveShell(),
                                            Messages.getString("warning"), msg); //$NON-NLS-1$
                    break;
                }
            }
        });
    }

    public void displayError(int severity, final String msg,
                             Throwable t, boolean shouldLog) {
        final IStatus status = createStatus(severity, msg, t);
        if (shouldLog) {
            writeLog(status);
        }
        final Display display = PlatformUI.getWorkbench().getDisplay();
        display.syncExec(new Runnable() {
            public void run() {
                ErrorDialog.openError(display.getActiveShell(),
                                   null,
                                   Messages.getString("error"), status); //$NON-NLS-1$
            }
        });
    }

    @Override
    protected void initializeImageRegistry(ImageRegistry reg) {
        super.initializeImageRegistry(reg);
        reg.put(IMG_GUVCONTROLLED, loadImageDescriptor(IMG_GUVCONTROLLED));
        reg.put(IMG_GUVLOCADD, loadImageDescriptor(IMG_GUVLOCADD));
        reg.put(IMG_GUVCONTROLLED, loadImageDescriptor(IMG_GUVREPWIZBAN));
        reg.put(IMG_GUVREP, loadImageDescriptor(IMG_GUVREP));
        reg.put(IMG_GUVPACK, loadImageDescriptor(IMG_GUVPACK));
        reg.put(IMG_GUVFOLDER, loadImageDescriptor(IMG_GUVFOLDER));
        reg.put(IMG_GUVSNAPPACK, loadImageDescriptor(IMG_GUVSNAPPACK));
        reg.put(IMG_GUVSNAPSHOT, loadImageDescriptor(IMG_GUVSNAPSHOT));
    }

    /**
     * Returns an image descriptor for the image file at the given
     * plug-in relative path
     *
     * @param path the path
     * @return the image descriptor
     */
    public static ImageDescriptor getImageDescriptor(String id) {
        ImageDescriptor retVal = getDefault().getImageRegistry().getDescriptor(id);
        if (retVal == null) {
            retVal = loadImageDescriptor(id);
            getDefault().getImageRegistry().put(id, retVal);
        }
        return retVal;
    }

    private static ImageDescriptor loadImageDescriptor(String id) {
        String iconPath = "icons/"; //$NON-NLS-1$

        try {
            URL installURL = Activator.getDefault().getBundle().getEntry("/"); //$NON-NLS-1$
            URL url = new URL(installURL, iconPath + id);
            return ImageDescriptor.createFromURL(url);
        } catch (MalformedURLException e) {
            return ImageDescriptor.getMissingImageDescriptor();
        }
    }

    public static final String IMG_GUVCONTROLLED         = "guvnor_controlled.gif"; //$NON-NLS-1$
    public static final String IMG_GUVLOCADD             = "guvnor_rep_add.gif"; //$NON-NLS-1$
    public static final String IMG_GUVREPWIZBAN         = "guvnor_rep_wizban.gif"; //$NON-NLS-1$
    public static final String IMG_GUVREP         = "guvnor_rep.gif"; //$NON-NLS-1$
    public static final String IMG_GUVPACK         = "package.gif"; //$NON-NLS-1$
    public static final String IMG_GUVFOLDER         = "chart_organisation.gif"; //$NON-NLS-1$
    public static final String IMG_GUVSNAPPACK         = "snapshot_small.gif"; //$NON-NLS-1$
    public static final String IMG_GUVSNAPSHOT         = "details.gif"; //$NON-NLS-1$

    public boolean useDebugHttpClient() {
        return Boolean.parseBoolean(Platform.getDebugOption("org.guvnor.tools/debugHttpClient")); //$NON-NLS-1$
    }
}


/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 *      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;

/**
 *
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.
 *
 *
Conveniences are provided for managing proxy servers, handling
 * authentication, and setting up request headers.
 *
 *
Here is some sample 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();
 * }
 *
 *

 * Note: 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.
 *

 */
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 Hashtable mapping origin server URLs
     * to Contexts. 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 URL 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 Hashtable mapping origin server URLs
     * to proxy server URLs. If an origin server
     * URL has an entry in this table, the proxy server
     * with the mapped URL 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
     * IOException occurs.
     */
    private int maxRetries = 1;

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

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

    /**
     * The factory used to create Sockets.
     */
    private ISocketFactory socketFactory = null;

    /**
     * The factory used to create Contexts.
     */
    private WebDAVFactory webDAVFactory = new WebDAVFactory();

    /**
     * The ConnectionsRecycler manages a collection of
     * persistent HttpConnections. 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 Hashtable who's keys are origin server
         * URLs that map to vectors of connections that are
         * currently unused.
         */
        private Hashtable unusedConnections = new Hashtable(5);

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

        /**
         * Creates a new ConnectionsRecycler 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 URL. The connection is marked
         * as in use before it is returned.
         *
         * @param originServerUrl the URL of an origin
         * server
         * @return an unused connection that is connected to the origin
         * server at the given URL
         * @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 HttpClient.
     */
    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:
     *

     *
www.company.com:80

     *
*.company.com

     *
www.company.*

     *

     *
     * @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
     * URL.
     *
     * @param originServerUrl the URL of an origin server
     * @return the context for the origin server at the given
     * URL
     * @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
     * null.
     *
     * @return the default context, or null
     * @see #getContext(URL)
     * @see #setContext(URL, Context)
     * @see #setDefaultContext(Context)
     */
    public IContext getDefaultContext() {
        return defaultContext;
    }

    /**
     * Returns the URL 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
     * URL is null, no default proxy server is
     * used. Initially the default proxy server URL is
     * null.
     *
     * @return the URL of the default proxy server, or
     * null
     * @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 URL location redirects. The
     * maximum is 4 by default.
     *
     * @return the maximum number of URL location redirects
     */
    public int getMaxRedirects() {
        return maxRedirects;
    }

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

    /**
     * Returns an Enumeration over the origin server
     * URLs known to this client. The known origin server
     * URLs are gleaned from this client's mapped contexts and
     * mapped proxy server URLs.
     *
     * @return an Enumeration over the origin server
     * URLs 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 Enumeration over this client's proxy server
     * exception patterns.
     *
     * @return an Enumeration 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 URL of the proxy server that the origin
     * server at the given URL uses, or null if
     * no proxy server is used.
     *
     * @param originServerUrl the URL of an origin server
     * @return the URL of a proxy server, or null
     * @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:
     *

     *
The client is closed.
     *
The client could not connect to the server
     *
An I/O error occurs while communicating with the server
     *

     */
    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 null protected resources cannot be
     * accessed. The authenticator is null by default.
     *
     * @param authenticator the authenticator, or null
     */
    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 URL.
     * If the given context is null, the context for the
     * specified origin server is removed.
     *
     * @param originServerUrl the URL 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 URL 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
     * URL is null, no default proxy server is
     * used.
     *
     * @param proxyServerUrl the URL 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 URL location redirects. The
     * maximum is 4 by default.
     *
     * @param maxRedirects the maximum number of URL 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
     * IOException occurs. The maximum is 1 by default.
     *
     * @param maxRetries the maximum number of times a request is retried
     * after an IOException occurs
     * @see #getMaxRetries()
     */
    public void setMaxRetries(int maxRetries) {
        Assert.isTrue(maxRetries >= 0);
        this.maxRetries = maxRetries;
    }

    /**
     * Sets the URL of the proxy server that this client uses
     * to communicate with the origin server at the given URL.
     * If the proxy server URL is null, the
     * default proxy server is used if the specified origin server does not
     * match a proxy server exception pattern.
     *
     * @param originServerUrl the URL of on origin server
     * @param proxyServerUrl the URL of a proxy server, or
     * null
     * @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;
    }
}


/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 *      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.*;
import java.util.Date;
import java.util.Vector;
import org.eclipse.webdav.client.Policy;
import org.eclipse.webdav.internal.kernel.utils.Assert;

/**
 * A connection to an HTTP/1.0 or HTTP/1.1 compatable server.
 *

 * Note: 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.
 *

 */
public class HttpConnection implements IStatusCodes {
    // The HTTP version to use (1.0 or 1.1).
    private double httpVersion = 1.1;

    // The request method.
    private String method = "GET"; //$NON-NLS-1$

    // The URL of the requested resource.
    private URL resourceUrl = null;

    // The URL of the proxy server.
    private URL proxyServerUrl = null;

    // The request and response headers.
    protected Header requestHeader = new Header();
    protected Header responseHeader = new Header();
    protected Header internalHeader = new Header();

    // The status code and status message of the response.
    private int statusCode;
    private String statusMessage;

    // The socket factory, socket, and socket settings.
    private ISocketFactory socketFactory = null;
    private Socket socket = null;
    private int receiveBufferSize = 0;
    private int sendBufferSize = 0;
    private int soLinger = -1;
    private int soTimeout = 0;
    private boolean tcpNoDelay = false;

    // The actual socket streams.
    protected InputStream socketIn = null;
    protected OutputStream socketOut = null;

    // The user's socket streams.
    private InputStream is = null;
    private OutputStream os = null;

    // A boolean indicating whether the connection should be closed after
    // the current request.
    private boolean closeConnection;

    // A boolean indicating whether the client wishes the connection to
    // remain open after the current request.
    private boolean persistent = true;

    // A boolean indicating whether the client wishes the request's
    // content to be sent chunked encoded.
    private boolean sendChunked = true;

    // A boolean indicating whether this connection is currently
    // connected to a server.
    private boolean connected = false;

    // A boolean indicating whether the current request has been sent.
    private boolean sentRequest = false;

    // This connection's timestamp.
    private Date timestamp = null;

    /**
     * A request or response header.
     */
    public class Header {
        // This header's field names
        private Vector fieldNames = new Vector(5);

        // This header's field values
        private Vector fieldValues = new Vector(5);

        /**
         * Adds the specified header field to this header.
         *
         * @param fieldName the new header field's name
         * @param fieldValue the new header field's value
         */
        public void addField(String fieldName, String fieldValue) {
            Assert.isNotNull(fieldName);
            Assert.isNotNull(fieldValue);
            fieldNames.addElement(fieldName);
            fieldValues.addElement(fieldValue);
        }

        /**
         * Removes all header fields from this header.
         */
        public void clear() {
            fieldNames.removeAllElements();
            fieldValues.removeAllElements();
        }

        /**
         * Returns the field value of the header field at the given
         * position, or null if there is no such postition.
         *
         * @param position the position of a header field
         * @return the field value of the header field at the given
         * position
         */
        public String getFieldValue(int position) {
            if (position < 0 || position >= fieldValues.size())
                return null;
            return (String) fieldValues.elementAt(position);
        }

        /**
         * Returns the field value of the header field with the given
         * field name, or null if there is no such field
         * name.
         *
         * @param fieldName the name of a header field
         * @return the field value of the header field with the given
         * field name
         */
        public String getFieldValue(String fieldName) {
            Assert.isNotNull(fieldName);
            return getFieldValue(fieldNames.indexOf(fieldName));
        }

        /**
         * Returns the field name of the header field at the given
         * position, or null if there is no such position.
         *
         * @param position the position of a header field in this header
         * @return the field name of the header field at the given
         * position
         */
        public String getFieldName(int position) {
            if (position < 0 || position >= fieldNames.size())
                return null;
            return (String) fieldNames.elementAt(position);
        }

        /**
         * Returns the number of header fields in this header.
         *
         * @return the number of header fields in this header
         */
        public int size() {
            return fieldNames.size();
        }
    }

    /**
     * A limited input stream reads up to a given number of bytes from
     * this connection's underlying socket input stream.
     */
    private class LimitedInputStream extends InputStream {
        /**
         * The number of bytes remaining in this stream.
         */
        private int bytesRemaining;

        /**
         * Creates a new limited input stream that reads up to
         * length bytes of data from this connection's
         * underlying socket input stream.
         *
         * @param length the length of this input stream
         */
        public LimitedInputStream(int length) {
            Assert.isTrue(length >= 0);
            bytesRemaining = length;
        }

        /**
         * @see InputStream#available()
         */
        public int available() throws IOException {
            return Math.min(socketIn.available(), bytesRemaining);
        }

        /**
         * @see InputStream#close()
         */
        public void close() throws IOException {
            while (skip(4096) > 0);
        }

        /**
         * @see InputStream#read()
         */
        public int read() throws IOException {
            if (bytesRemaining <= 0)
                return -1;
            --bytesRemaining;
            return socketIn.read();
        }

        /**
         * @see InputStream#read(byte[], int, int)
         */
        public int read(byte[] buf, int offset, int length) throws IOException {
            if (bytesRemaining <= 0)
                return -1;
            int result = socketIn.read(buf, offset, Math.min(length, bytesRemaining));
            if (result > 0)
                bytesRemaining -= result;
            return result;
        }

        /**
         * @see InputStream#skip(int)
         */
        public long skip(int amount) throws IOException {
            if (bytesRemaining <= 0)
                return -1;
            long result = socketIn.skip(Math.min(amount, bytesRemaining));
            if (result > 0)
                bytesRemaining -= result;
            return result;
        }
    }

    /**
     * A chunked input stream reads bytes, that have been chunked
     * transfer encoded, from this connection's underlying socket input
     * stream.
     */
    private class ChunkedInputStream extends InputStream {

        /**
         * The number of bytes remaining in this stream's current chunk.
         */
        private int bytesRemaining = -1;

        /**
         * A boolean indicating whether the end of this stream has been
         * reached.
         */
        private boolean atEnd = false;

        /**
         * @see InputStream#available()
         */
        public int available() throws IOException {
            return Math.min(socketIn.available(), bytesRemaining);
        }

        /**
         * @see InputStream#close()
         */
        public void close() throws IOException {
            while (skip(4096) > 0);
        }

        /**
         * @see InputStream#read()
         */
        public int read() throws IOException {
            if (!atEnd && bytesRemaining <= 0)
                readChunkSize();
            if (atEnd)
                return -1;
            --bytesRemaining;
            return socketIn.read();
        }

        /**
         * @see InputStream#read(byte[], int, int)
         */
        public int read(byte[] buf, int offset, int length) throws IOException {
            if (!atEnd && bytesRemaining <= 0)
                readChunkSize();
            if (atEnd)
                return -1;
            int result = socketIn.read(buf, offset, Math.min(length, bytesRemaining));
            if (result > 0)
                bytesRemaining -= result;
            return result;
        }

        /**
         * Read the size of the next chunk of data.
         */
        private void readChunkSize() throws IOException {
            if (bytesRemaining == 0)
                readln();
            String size = readln();
            int index = size.indexOf(";"); //$NON-NLS-1$
            if (index >= 0)
                size = size.substring(0, index);
            try {
                bytesRemaining = Integer.parseInt(size.trim(), 16);
            } catch (NumberFormatException exception) {
                throw new IOException(exception.getMessage());
            }
            if (bytesRemaining == 0) {
                atEnd = true;
                readHeader(responseHeader);
            }
        }

        /**
         * @see InputStream#skip(int)
         */
        public long skip(int amount) throws IOException {
            if (!atEnd && bytesRemaining <= 0)
                readChunkSize();
            if (atEnd)
                return -1;
            long result = socketIn.skip(Math.min(amount, bytesRemaining));
            if (result > 0)
                bytesRemaining -= result;
            return result;
        }
    }

    /**
     * A limited output stream writes up to a given number of bytes to
     * this connection's underlying socket input stream.
     */
    private class LimitedOutputStream extends OutputStream {
        // The maximum number of bytes that can be written to this
        // stream.
        private int limit;

        // A boolean indicating whether this stream is closed.
        private boolean closed = false;

        /**
         * Creates a new limited output stream that writes up to the
         * given limit of bytes to this connection's underlying socket
         * output stream.
         *
         * @param limit the maximum number of bytes that can be written
         * to this stream
         */
        public LimitedOutputStream(int limit) {
            Assert.isTrue(limit >= 0);
            this.limit = limit;
        }

        /**
         * @see OutputStream#close()
         */
        public void close() throws IOException {
            if (closed)
                return;
            try {
                flush();
                if (limit > 0)
                    throw new IOException(Policy.bind("exception.contentLengthUnderflow")); //$NON-NLS-1$
                readServerResponse();
            } finally {
                closed = true;
            }
        }

        /**
         * @see OutputStream#flush()
         */
        public void flush() throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
            socketOut.flush();
        }

        /**
         * @see OutputStream#write(int)
         */
        public void write(int data) throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
            if (limit == 0)
                throw new IOException(Policy.bind("exception.contentLengthExceeded")); //$NON-NLS-1$
            --limit;
            socketOut.write(data);
        }

        /**
         * @see OutputStream#write(byte[], int, int)
         */
        public void write(byte[] buffer, int offset, int count) throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
            if (count > limit)
                throw new IOException(Policy.bind("exception.contentLengthExceeded")); //$NON-NLS-1$
            limit -= count;
            socketOut.write(buffer, offset, count);
        }
    }

    /**
     * A chunked output stream writes bytes to this connection's
     * underlying socket output stream. The bytes are chunked transfer
     * encoded before they are written to the stream.
     */
    private class ChunkedOutputStream extends OutputStream {
        // The maximum size of this output stream's buffer
        private static final int MAX_BUFFER_SIZE = 1024;

        // A buffer that holds the next chunk of data to be written to
        // this output stream.
        private ByteArrayOutputStream buffer = new ByteArrayOutputStream(MAX_BUFFER_SIZE);

        // A boolean indicating whether this output stream is closed.
        private boolean closed = false;

        /**
         * @see OutputStream#close()
         */
        public void close() throws IOException {
            if (closed)
                return;
            try {
                sendBuffer();
                output(socketOut, "0\r\n\r\n"); //$NON-NLS-1$
                readServerResponse();
            } finally {
                closed = true;
            }
        }

        /**
         * @see OutputStream#flush()
         */
        public void flush() throws IOException {
            if (closed)
                throw new IOException("closed"); //$NON-NLS-1$
            sendBuffer();
            socketOut.flush();
        }

        /**
         * Writes the content of the buffer to this stream.
         */
        public void sendBuffer() throws IOException {
            int chunkSize = buffer.size();
            if (chunkSize > 0) {
                output(socketOut, Integer.toHexString(chunkSize) + "\r\n"); //$NON-NLS-1$
                buffer.writeTo(socketOut);
                buffer.reset();
                output(socketOut, "\r\n"); //$NON-NLS-1$
            }
        }

        /**
         * @see OutputStream#write(int)
         */
        public void write(int data) throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
            buffer.write(data);
            if (buffer.size() >= MAX_BUFFER_SIZE)
                sendBuffer();
        }

        /**
         * @see OutputStream#write(byte[], int, int)
         */
        public void write(byte[] buf, int off, int len) throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
            int bufferSize = buffer.size();
            if (bufferSize + len < MAX_BUFFER_SIZE) {
                buffer.write(buf, off, len);
            } else {
                output(socketOut, Integer.toHexString(bufferSize + len) + "\r\n"); //$NON-NLS-1$
                buffer.writeTo(socketOut);
                buffer.reset();
                socketOut.write(buf, off, len);
                output(socketOut, "\r\n"); //$NON-NLS-1$
            }
        }
    }

    /**
     * A cached output stream writes bytes to a cache before sending the
     * data to the underlying socket output stream. This is done so the
     * content length can be determined.
     */
    private class CachedOutputStream extends OutputStream {
        // The initial size of this output stream's cache
        private static final int INITIAL_CACHE_SIZE = 1024;

        // A cache that stores the bytes written to this output stream.
        private ByteArrayOutputStream cache = new ByteArrayOutputStream(INITIAL_CACHE_SIZE);

        // A boolean indicating whether this output stream is closed.
        private boolean closed = false;

        /**
         * @see OutputStream#close()
         */
        public void close() throws IOException {
            if (closed)
                return;
            try {
                String contentLength = Integer.toString(cache.size());
                internalHeader.addField("Content-Length", contentLength); //$NON-NLS-1$
                sendRequest();
                cache.writeTo(socketOut);
                readServerResponse();
            } finally {
                closed = true;
            }
        }

        /**
         * @see OutputStream#flush()
         */
        public void flush() throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
        }

        /**
         * @see OutputStream#write(int)
         */
        public void write(int data) throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
            cache.write(data);
        }

        /**
         * @see OutputStream#write(byte[], int, int)
         */
        public void write(byte[] buffer, int offset, int count) throws IOException {
            if (closed)
                throw new IOException(Policy.bind("exception.closed")); //$NON-NLS-1$
            cache.write(buffer, offset, count);
        }
    }

    /**
     * Creates a new connection on the specified resource.
     *
     * @param resourceUrl the URL of a resource
     */
    public HttpConnection(URL resourceUrl) {
        Assert.isNotNull(resourceUrl);
        this.resourceUrl = resourceUrl;
    }

    /**
     * Creates a new connection on the specified resource. This connection
     * communicates through the proxy at the given proxy server
     * URL.
     *
     * @param proxyServerUrl the URL of a proxy server
     * @param resourceUrl the URL of a resource
     */
    public HttpConnection(URL proxyServerUrl, URL resourceUrl) {
        Assert.isNotNull(proxyServerUrl);
        Assert.isNotNull(resourceUrl);
        this.proxyServerUrl = proxyServerUrl;
        this.resourceUrl = resourceUrl;
    }

    /**
     * Clear the request header.
     */
    public void clearRequestHeader() {
        endRequest();
        requestHeader.clear();
    }

    /**
     * Close this connection.
     *
     * @exception IOException if there is an I/O error closing the socket
     */
    public void close() throws IOException {
        endRequest();
        if (connected) {
            connected = false;
            socket.close();
        }
    }

    private void connect() throws IOException {
        if (!connected) {
            String protocol;
            String host;
            int port;
            if (proxyServerUrl == null) {
                protocol = resourceUrl.getProtocol();
                host = resourceUrl.getHost();
                port = getPort(resourceUrl);
            } else {
                protocol = proxyServerUrl.getProtocol();
                host = proxyServerUrl.getHost();
                port = getPort(proxyServerUrl);
            }

            if (socketFactory == null) {
                socket = new Socket(host, port);
            } else {
                socket = socketFactory.createSocket(protocol, host, port);
            }

            if (receiveBufferSize > 0) {
                socket.setReceiveBufferSize(receiveBufferSize);
            }

            if (sendBufferSize > 0) {
                socket.setSendBufferSize(sendBufferSize);
            }

            socket.setSoLinger(soLinger >= 0, soLinger >= 0 ? soLinger : 0);
            socket.setSoTimeout(soTimeout);
            socket.setTcpNoDelay(tcpNoDelay);

            socketOut = new BufferedOutputStream(socket.getOutputStream());
            socketIn = new BufferedInputStream(socket.getInputStream());

            closeConnection = httpVersion == 1.0 || !persistent;

            connected = true;
        }
    }

    private void endRequest() {
        if (sentRequest) {
            boolean failed = false;

            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    failed = true;
                }
            }

            try {
                getInputStream().close();
            } catch (IOException e) {
                failed = true;
            }

            sentRequest = false;

            if (closeConnection || failed) {
                try {
                    connected = false;
                    socket.close();
                } catch (IOException e) {
                    // ignore or log?
                }
            }
        }

        os = null;
        is = null;

        internalHeader.clear();
    }

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

    /**
     * Returns this connection's InputStream.
     *
     * @return this connection's InputStream
     * @exception IOException if an I/O error occurs while sending the
     * request
     */
    public InputStream getInputStream() throws IOException {
        if (is != null)
            return is;
        sendRequest();
        String transferEncoding = responseHeader.getFieldValue("Transfer-Encoding"); //$NON-NLS-1$
        String contentLength = responseHeader.getFieldValue("Content-Length"); //$NON-NLS-1$
        if ("chunked".equalsIgnoreCase(transferEncoding)) { //$NON-NLS-1$
            is = new ChunkedInputStream();
        } else if (method.equals("HEAD") && statusCode == HTTP_OK) { //$NON-NLS-1$
            is = new LimitedInputStream(0);
        } else if (contentLength != null) {
            try {
                is = new LimitedInputStream(Integer.parseInt(contentLength));
            } catch (NumberFormatException e) {
                throw new IOException(e.getMessage());
            }
        } else if ((statusCode >= 100 && statusCode < 200) || statusCode == HTTP_NO_CONTENT || statusCode == HTTP_NOT_MODIFIED) {
            is = new LimitedInputStream(0);
        } else {
            closeConnection = true;
            is = socketIn;
        }
        return is;
    }

    /**
     * Returns this connection's OutputStream.
     *
     * @return this connection's OutputStream
     * @exception IOException if an I/O error occurs while sending the
     * request
     */
    public OutputStream getOutputStream() throws IOException {
        if (os != null)
            return os;
        String contentLength = requestHeader.getFieldValue("Content-Length"); //$NON-NLS-1$
        if (sendChunked && httpVersion > 1.0) {
            os = new ChunkedOutputStream();
        } else if (contentLength != null) {
            try {
                os = new LimitedOutputStream(Integer.parseInt(contentLength));
            } catch (NumberFormatException e) {
                throw new IOException(Policy.bind("exception.malformedContentLength")); //$NON-NLS-1$
            }
        } else {
            os = new CachedOutputStream();
            return os;
        }
        sendRequest();
        return os;
    }

    /**
     * Returns a boolean indicating whether this connection should remain
     * open after each request.
     *
     * @return a boolean indicating whether this connection should remain
     * open after each request
     * @see #setPersistent(boolean)
     */
    public boolean getPersistent() {
        return persistent;
    }

    private int getPort(URL url) {
        String protocol = url.getProtocol();
        int port = url.getPort();

        if (port == -1) {
            if (protocol.equals("http")) //$NON-NLS-1$
                return 80;
            if (protocol.equals("https")) //$NON-NLS-1$
                return 443;
            return -1;
        }

        return port;
    }

    /**
     * Returns the URL of the proxy server this connection uses
     * to communicate with the origin server, or null if a proxy
     * server is not used.
     *
     * @return the URL of the proxy server this connection uses
     * to communicate with the origin server
     * @see #setProxyServerUrl(URL)
     */
    public URL getProxyServerUrl() {
        return proxyServerUrl;
    }

    /**
     * @see Socket#getReceiveBufferSize()
     * @see #setReceiveBufferSize(int)
     */
    public int getReceiveBufferSize() throws IOException {
        if (connected)
            return socket.getReceiveBufferSize();
        return receiveBufferSize;
    }

    /**
     * Returns the request header value associated with the given field name,
     * or null if there is no such field name.
     *
     * @param fieldName the request header field name
     * @return the request header value associated with the given field name
     * @see #setRequestHeaderField(String, String)
     */
    public String getRequestHeaderFieldValue(String fieldName) {
        Assert.isNotNull(fieldName);
        return requestHeader.getFieldValue(fieldName);
    }

    /**
     * Returns the request method. "GET" is used by default.
     *
     * @return the request method
     * @see #setRequestMethod(String)
     */
    public String getRequestMethod() {
        return method;
    }

    /**
     * Returns the URL of this connection's resource.
     *
     * @return the URL of this connection's resource
     * @see #setResourceUrl(URL)
     */
    public URL getResourceUrl() {
        return resourceUrl;
    }

    /**
     * Returns the response header field name at the given position, or
     * null if there is no field name at that position.
     *
     * @param position a position in the response header greater than or
     * equal to zero
     * @return the response header field name at the given postion
     * @exception IOException if an I/O error occurs while sending the
     * request
     * @see #getResponseHeaderFieldValue(int)
     * @see #getResponseHeaderFieldValue(String)
     */
    public String getResponseHeaderFieldName(int position) throws IOException {
        Assert.isTrue(position >= 0);
        sendRequest();
        return responseHeader.getFieldName(position);
    }

    /**
     * Returns the response header field value at the given position, or
     * null if there is no value at that position.
     *
     * @param position a position in the response header greater than or
     * equal to zero
     * @return the response header field value at the given postion
     * @exception IOException if an I/O error occurs while sending the
     * request
     * @see #getResponseHeaderFieldName(int)
     * @see #getResponseHeaderFieldValue(String)
     */
    public String getResponseHeaderFieldValue(int position) throws IOException {
        Assert.isTrue(position >= 0);
        sendRequest();
        return responseHeader.getFieldValue(position);
    }

    /**
     * Returns the response header field value that is associated with the
     * given field name, or null if there is no value associated
     * with that field name.
     *
     * @param fieldName the name of a response header field
     * @return the response header field value that is associated with the
     * given field name
     * @exception IOException if an I/O error occurs while sending the
     * request
     * @see #getResponseHeaderFieldValue(int)
     * @see #getResponseHeaderFieldName(int)
     */
    public String getResponseHeaderFieldValue(String fieldName) throws IOException {
        Assert.isNotNull(fieldName);
        sendRequest();
        return responseHeader.getFieldValue(fieldName);
    }

    /**
     * @see Socket#getSendBufferSize()
     * @see #setSendBufferSize(int)
     */
    public int getSendBufferSize() throws IOException {
        if (connected)
            return socket.getSendBufferSize();
        return sendBufferSize;
    }

    /**
     * Returns a boolean indicating whether the request's body should be sent
     * chunked encoded.
     *
     * @return a boolean indicating whether the request's body should be sent
     * chunked encoded
     * @see #setSendChunked(boolean)
     */
    public boolean getSendChunked() {
        return sendChunked;
    }

    /**
     * @see Socket#getSoLinger()
     * @see #setSoLinger(boolean, int)
     */
    public int getSoLinger() {
        return soLinger;
    }

    /**
     * @see Socket#getSoTimeout()
     * @see #setSoTimeout(int)
     */
    public int getSoTimeout() {
        return soTimeout;
    }

    /**
     * Returns the status code of the server's response.
     *
     * @return the status code of the server's response
     * @exception IOException if an I/O error occurs while sending the
     * request
     * @see #getStatusMessage()
     */
    public int getStatusCode() throws IOException {
        sendRequest();
        return statusCode;
    }

    /**
     * Returns the status message of the server's response.
     *
     * @return the status message of the server's response
     * @exception IOException if an I/O error occurs while sending the
     * request
     * @see #getStatusCode()
     */
    public String getStatusMessage() throws IOException {
        sendRequest();
        return statusMessage;
    }

    /**
     * @see Socket#getTcpNoDelay()
     * @see #setTcpNoDelay(boolean)
     */
    public boolean getTcpNoDelay() {
        return tcpNoDelay;
    }

    /**
     * Returns this connection's timestamp.
     *
     * @return this connection's timestamp
     * @see #setTimestamp(Date)
     */
    public Date getTimestamp() {
        return timestamp;
    }

    protected void output(OutputStream stream, String output) throws IOException {
        stream.write(output.getBytes("UTF8")); //$NON-NLS-1$
    }

    protected void readHeader(Header header) throws IOException {
        String line = null;

        while ((line = readln()).length() > 0) {
            int index = line.indexOf(":"); //$NON-NLS-1$

            if (index < 0)
                throw new IOException(Policy.bind("exception.malformedHeaderField")); //$NON-NLS-1$
            String fieldName = line.substring(0, index);
            String fieldValue = line.substring(index + 1).trim();
            header.addField(fieldName, fieldValue);
        }

        String fieldValue = header.getFieldValue("Connection"); //$NON-NLS-1$
        if (fieldValue != null && fieldValue.equalsIgnoreCase("close")) { //$NON-NLS-1$
            closeConnection = true;
        }
    }

    protected String readln() throws IOException {
        boolean foundCR = false;
        StringBuffer result = new StringBuffer();

        while (true) {
            int c = socketIn.read();

            if (c < 0) {
                throw new IOException(Policy.bind("exception.unexpectedEndStream")); //$NON-NLS-1$
            }

            if (foundCR) {
                if (c == '\n')
                    break;
                result.append('\r');
            }

            if (c == '\r') {
                foundCR = true;
            } else {
                result.append((char) c);
            }
        }
        return result.toString();
    }

    protected void readServerResponse() throws IOException {
        socketOut.flush();

        String statusLine = readln();

        if (!statusLine.startsWith("HTTP/")) { //$NON-NLS-1$
            throw new IOException(Policy.bind("exception.malformedStatusLine")); //$NON-NLS-1$
        }

        int firstSpace = statusLine.indexOf(' ', 5);
        if (firstSpace == -1) {
            throw new IOException(Policy.bind("exception.malformedStatusLine")); //$NON-NLS-1$
        }

        int secondSpace = statusLine.indexOf(' ', firstSpace + 1);
        if (secondSpace == -1) {
            throw new IOException(Policy.bind("exception.malformedStatusLine")); //$NON-NLS-1$
        }

        double protocolVersion;

        try {
            protocolVersion = Double.parseDouble(statusLine.substring(5, firstSpace));
            statusCode = Integer.parseInt(statusLine.substring(firstSpace + 1, secondSpace));
            statusMessage = statusLine.substring(secondSpace + 1);
        } catch (NumberFormatException e) {
            throw new IOException(Policy.bind("exception.malformedStatusLine")); //$NON-NLS-1$
        }

        if (protocolVersion == 1.0) {
            httpVersion = 1.0;
            closeConnection = true;
        }

        responseHeader.clear();
        readHeader(responseHeader);
    }

    protected void sendRequest() throws IOException {
        if (sentRequest)
            return;
        connect();
        if (requestHeader.getFieldValue("Host") == null) { //$NON-NLS-1$
            internalHeader.addField("Host", //$NON-NLS-1$
                    resourceUrl.getHost() + (resourceUrl.getPort() == -1 ? "" : ":" + resourceUrl.getPort())); //$NON-NLS-1$ //$NON-NLS-2$
        }

        if (httpVersion > 1.0 && !persistent && requestHeader.getFieldValue("Connection") == null) { //$NON-NLS-1$
            internalHeader.addField("Connection", "close"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        if (os != null || "100-continue".equals(requestHeader.getFieldValue("Expect"))) { //$NON-NLS-1$ //$NON-NLS-2$
            if (requestHeader.getFieldValue("Content-Type") == null) { //$NON-NLS-1$
                internalHeader.addField("Content-Type", "application/x-www-form-urlencoded"); //$NON-NLS-1$ //$NON-NLS-2$
            }
            if (httpVersion > 1.0 && sendChunked) {
                internalHeader.addField("Transfer-Encoding", "chunked"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        output(socketOut, method);
        output(socketOut, " "); //$NON-NLS-1$
        output(socketOut, proxyServerUrl == null ? resourceUrl.getFile() : resourceUrl.toString());
        output(socketOut, " HTTP/1.1\r\n"); //$NON-NLS-1$

        writeHeader(internalHeader);
        writeHeader(requestHeader);
        output(socketOut, "\r\n"); //$NON-NLS-1$

        sentRequest = true;

        if (os == null) {
            readServerResponse();
        }
    }

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

    /**
     * Sets a boolean indicating whether this connection should remain open
     * after each request.
     *
     * @param close a boolean indicating whether this connection should
     * remain open after each request
     * @see #getPersistent()
     */
    public void setPersistent(boolean close) {
        endRequest();
        persistent = close;
    }

    /**
     * Sets the URL of the proxy server this connection uses to
     * communicate with the origin server. If null is given, no
     * proxy server is used.
     *
     * @param proxyServerUrl the URL of a proxy server
     * @see #getProxyServerUrl()
     */
    public void setProxyServerUrl(URL proxyServerUrl) {
        endRequest();
        if (proxyServerUrl == null && this.proxyServerUrl == null)
            return;
        boolean closeConnection = true;

        if (proxyServerUrl != null && this.proxyServerUrl != null) {
            URL oldProxyServerUrl = null;
            URL newProxyServerUrl = null;

            try {
                oldProxyServerUrl = new URL(this.proxyServerUrl.getProtocol(), this.proxyServerUrl.getHost(), this.proxyServerUrl.getPort(), "/"); //$NON-NLS-1$
                newProxyServerUrl = new URL(proxyServerUrl.getProtocol(), proxyServerUrl.getHost(), proxyServerUrl.getPort(), "/"); //$NON-NLS-1$
            } catch (MalformedURLException e) {
                // ignore or log?
            }

            if (oldProxyServerUrl.equals(newProxyServerUrl)) {
                closeConnection = false;
            }
        }

        if (closeConnection) {
            try {
                close();
            } catch (IOException e) {
                // ignore or log?
            }
        }

        this.proxyServerUrl = proxyServerUrl;
    }

    /**
     * @see Socket#setReceiveBufferSize(int)
     * @see #getReceiveBufferSize()
     */
    public void setReceiveBufferSize(int size) throws IOException {
        Assert.isTrue(size > 0);
        if (size != getReceiveBufferSize()) {
            receiveBufferSize = size;
            if (connected) {
                socket.setReceiveBufferSize(receiveBufferSize);
            }
        }
    }

    /**
     * Sets the request header value associated with the given field.
     *
     * @param fieldName the request header field
     * @param fieldValue the request header value
     * @see #getRequestHeaderFieldValue(String)
     */
    public void setRequestHeaderField(String fieldName, String fieldValue) {
        Assert.isNotNull(fieldName);
        Assert.isNotNull(fieldValue);
        endRequest();
        requestHeader.addField(fieldName, fieldValue);
    }

    /**
     * Sets the request method. "GET" is used by default.
     *
     * @param method the request method
     * @see #getRequestMethod()
     */
    public void setRequestMethod(String method) {
        Assert.isNotNull(method);
        endRequest();
        this.method = method;
    }

    /**
     * Sets the URL of this connection's resource.
     *
     * @param resourceUrl the URL of this connection's resource
     * @see #getResourceUrl()
     */
    public void setResourceUrl(URL resourceUrl) {
        Assert.isNotNull(resourceUrl);
        endRequest();
        URL oldOriginServerUrl = null;
        URL newOriginServerUrl = null;
        try {
            oldOriginServerUrl = new URL(this.resourceUrl.getProtocol(), this.resourceUrl.getHost(), this.resourceUrl.getPort(), "/"); //$NON-NLS-1$
            newOriginServerUrl = new URL(resourceUrl.getProtocol(), resourceUrl.getHost(), resourceUrl.getPort(), "/"); //$NON-NLS-1$
        } catch (MalformedURLException e) {
            // ignore?
        }
        if (!oldOriginServerUrl.equals(newOriginServerUrl)) {
            try {
                close();
            } catch (IOException e) {
                // ignore?
            }
        }
        this.resourceUrl = resourceUrl;
    }

    /**
     * @see Socket#setSendBufferSize(int)
     * @see #getSendBufferSize()
     */
    public void setSendBufferSize(int size) throws IOException {
        Assert.isTrue(size > 0);
        if (size != getSendBufferSize()) {
            sendBufferSize = size;
            if (connected) {
                socket.setSendBufferSize(sendBufferSize);
            }
        }
    }

    /**
     * Sets a boolean indicating whether the request's body should be sent
     * chunked encoded.
     *
     * @param chunked a boolean indicating whether the request's body should
     * be sent chunked encoded
     * @see #getSendChunked()
     */
    public void setSendChunked(boolean chunked) {
        endRequest();
        sendChunked = chunked;
    }

    /**
     * Sets the factory this connection uses to create sockets. If the given
     * socket factory is null the default socket is used.
     *
     * @param socketFactory the factory this connection uses to create
     * sockets
     */
    public void setSocketFactory(ISocketFactory socketFactory) {
        endRequest();
        if (socketFactory == this.socketFactory)
            return;
        try {
            close();
        } catch (IOException e) {
            // ignore?
        }
        this.socketFactory = socketFactory;
    }

    /**
     * @see Socket#setSoLinger(boolean, int)
     * @see #getSoLinger()
     */
    public void setSoLinger(boolean on, int linger) throws IOException {
        Assert.isTrue(linger >= 0);
        if (!on && soLinger != -1 || on && linger != soLinger) {
            soLinger = on ? linger : -1;
            if (connected) {
                socket.setSoLinger(on, linger);
            }
        }
    }

    /**
     * @see Socket#setSoTimeout(int)
     * @see #getSoTimeout()
     */
    public void setSoTimeout(int timeout) throws IOException {
        Assert.isTrue(timeout >= 0);
        if (timeout != soTimeout) {
            soTimeout = timeout;
            if (connected) {
                socket.setSoTimeout(soTimeout);
            }
        }
    }

    /**
     * @see Socket#setTcpNoDelay(boolean)
     * @see #getTcpNoDelay()
     */
    public void setTcpNoDelay(boolean on) throws IOException {
        if (on != tcpNoDelay) {
            tcpNoDelay = on;
            if (connected) {
                socket.setTcpNoDelay(tcpNoDelay);
            }
        }
    }

    /**
     * Sets this connection's timestamp.
     *
     * @param date this connection's timestamp
     * @see #getTimestamp()
     */
    public void setTimestamp(Date date) {
        timestamp = date;
    }

    private void writeHeader(Header header) throws IOException {
        for (int i = 0; i < header.size(); ++i) {
            String fieldName = header.getFieldName(i);
            String fieldValue = header.getFieldValue(fieldName);
            output(socketOut, fieldName);
            output(socketOut, ": "); //$NON-NLS-1$
            output(socketOut, fieldValue);
            output(socketOut, "\r\n"); //$NON-NLS-1$
        }
    }
}