diff --git a/jersey/jersey-client/pom.xml b/jersey/jersey-client/pom.xml
index 3797a6d..9382820 100644
--- a/jersey/jersey-client/pom.xml
+++ b/jersey/jersey-client/pom.xml
@@ -54,6 +54,11 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
+          <groupId>commons-httpclient</groupId>
+          <artifactId>commons-httpclient</artifactId>
+          <version>3.1</version>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>3.8.1</version>
diff --git a/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/DefaultCredentialsProvider.java b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/DefaultCredentialsProvider.java
new file mode 100644
index 0000000..964e225
--- /dev/null
+++ b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/DefaultCredentialsProvider.java
@@ -0,0 +1,138 @@
+/*
+ *
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License").  You
+ * may not use this file except in compliance with the License. You can obtain
+ * a copy of the License at https://jersey.dev.java.net/CDDL+GPL.html
+ * or jersey/legal/LICENSE.txt.  See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at jersey/legal/LICENSE.txt.
+ * Sun designates this particular file as subject to the "Classpath" exception
+ * as provided by Sun in the GPL Version 2 section of the License file that
+ * accompanied this code.  If applicable, add the following below the License
+ * Header, with the fields enclosed by brackets [] replaced by your own
+ * identifying information: "Portions Copyrighted [year]
+ * [name of copyright owner]"
+ *
+ * Contributor(s):
+ *
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license."  If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above.  However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package com.sun.jersey.impl.client.httpclient;
+
+import java.io.IOException;
+
+import javax.swing.JOptionPane;
+import javax.swing.JPasswordField;
+import javax.swing.JTextField;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.NTCredentials;
+import org.apache.commons.httpclient.UsernamePasswordCredentials;
+import org.apache.commons.httpclient.auth.CredentialsProvider;
+import org.apache.commons.httpclient.auth.AuthScheme;
+import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
+import org.apache.commons.httpclient.auth.NTLMScheme;
+import org.apache.commons.httpclient.auth.RFC2617Scheme;
+
+
+/**
+   A very simple credential provider.
+
+   @author jorgew
+ **/
+
+
+public class DefaultCredentialsProvider implements CredentialsProvider
+{
+    public Credentials getCredentials (AuthScheme scheme,
+                                       String host,
+                                       int port,
+                                       boolean proxy)
+        throws CredentialsNotAvailableException
+    {
+        if (scheme == null) {
+            return null;
+            }
+
+        try {
+            JTextField         userField = new JTextField();
+            JPasswordField passwordField = new JPasswordField();
+            int response;
+
+            if (scheme instanceof NTLMScheme) {
+                JTextField domainField = new JTextField();
+                Object[] msg = {
+                    host+":"+port+" requires Windows authentication",
+                    "Domain",
+                    domainField,
+                    "User Name",
+                    userField,
+                    "Password",
+                    passwordField
+                };
+                response = JOptionPane.showConfirmDialog (null, msg, "Authenticate",
+                                                          JOptionPane.OK_CANCEL_OPTION);
+
+                if ((response == JOptionPane.CANCEL_OPTION) ||
+                    (response == JOptionPane.CLOSED_OPTION))
+                    {
+                        throw new CredentialsNotAvailableException ("User cancled windows authentication.");
+                    }
+
+                return new NTCredentials (userField.getText(), new String (passwordField.getPassword()),
+                                          host, domainField.getText());
+
+
+            } else if (scheme instanceof RFC2617Scheme) {
+                Object[] msg = {
+                    host+":"+port+" requires authentication with the realm '"+
+                    scheme.getRealm()+"'",
+                    "User Name",
+                    userField,
+                    "Password",
+                    passwordField
+                };
+
+                response = JOptionPane.showConfirmDialog (null, msg, "Authenticate",
+                                                          JOptionPane.OK_CANCEL_OPTION);
+
+                if ((response == JOptionPane.CANCEL_OPTION) ||
+                    (response == JOptionPane.CLOSED_OPTION))
+                    {
+                        throw new CredentialsNotAvailableException ("User cancled windows authentication.");
+                    }
+
+
+                return new UsernamePasswordCredentials (userField.getText(),
+                                                        new String(passwordField.getPassword()));
+
+            } else {
+
+                throw new CredentialsNotAvailableException ("Unsupported authentication scheme: " +
+                                                            scheme.getSchemeName());
+
+            }
+        }catch (IOException ioe) {
+
+            throw new CredentialsNotAvailableException (ioe.getMessage(), ioe);
+
+        }
+    }
+}
diff --git a/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/DefaultHttpClientConfig.java b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/DefaultHttpClientConfig.java
new file mode 100644
index 0000000..669a15b
--- /dev/null
+++ b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/DefaultHttpClientConfig.java
@@ -0,0 +1,162 @@
+/*
+ *
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License").  You
+ * may not use this file except in compliance with the License. You can obtain
+ * a copy of the License at https://jersey.dev.java.net/CDDL+GPL.html
+ * or jersey/legal/LICENSE.txt.  See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at jersey/legal/LICENSE.txt.
+ * Sun designates this particular file as subject to the "Classpath" exception
+ * as provided by Sun in the GPL Version 2 section of the License file that
+ * accompanied this code.  If applicable, add the following below the License
+ * Header, with the fields enclosed by brackets [] replaced by your own
+ * identifying information: "Portions Copyrighted [year]
+ * [name of copyright owner]"
+ *
+ * Contributor(s):
+ *
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license."  If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above.  However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package com.sun.jersey.impl.client.httpclient;
+
+import java.util.Map;
+
+import org.apache.commons.httpclient.HttpState;
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.UsernamePasswordCredentials;
+import org.apache.commons.httpclient.NTCredentials;
+import org.apache.commons.httpclient.auth.AuthScope;
+
+import com.sun.jersey.api.client.config.DefaultClientConfig;
+
+/**
+   A default client configuration for clients that root with the
+   {@link
+   com.sun.jersey.impl.client.httpclient.HttpClientHandler}. This
+   class may be extended for specific configuration purposes.
+
+   @author jorgew
+**/
+
+public class DefaultHttpClientConfig extends DefaultClientConfig
+    implements HttpClientConfig
+{
+    /**
+       The HttpState of the current client.  This is used to maintain
+       authentication credentials.
+
+       The value MUST be an instance of {@link
+       org.apache.commons.httpclient.HttpState}.
+
+       This shouldn't be used directly, instead use {@link
+       #setCredentials}, {@link #setProxyCredentials}, {@link
+       #clearCredentials}, or {@link #clearProxyCredentials}.
+    **/
+    protected static final String PROPERTY_HTTP_STATE =
+        "com.sun.jersey.impl.client.httpclient.httpState";
+
+    private HttpState getHttpState()
+    {
+        Map<String, Object> props = getProperties();
+        HttpState state = (HttpState) props.get(PROPERTY_HTTP_STATE);
+
+        if (state == null) {
+            state = new HttpState();
+            props.put (PROPERTY_HTTP_STATE, state);
+        }
+
+        return state;
+    }
+
+    public void setCredentials(String realm, String host, int port,
+                               String usernamepassword)
+    {
+        AuthScope authScope = new AuthScope (host, port, realm);
+        UsernamePasswordCredentials creds = new UsernamePasswordCredentials (usernamepassword);
+        HttpState state = getHttpState();
+
+        state.setCredentials (authScope, creds);
+    }
+
+    public void setCredentials(String realm, String host, int port,
+                               String username, String password)
+    {
+        AuthScope authScope = new AuthScope (host, port, realm);
+        UsernamePasswordCredentials creds = new UsernamePasswordCredentials (username,password);
+        HttpState state = getHttpState();
+
+        state.setCredentials (authScope, creds);
+    }
+
+    public void setCredentials(String realm, String host, int port,
+                               String username, String password,
+                               String domain, String thisHost)
+    {
+        AuthScope authScope = new AuthScope (host, port, realm);
+        NTCredentials creds = new NTCredentials (username,password, thisHost, domain);
+        HttpState state = getHttpState();
+
+        state.setCredentials (authScope, creds);
+    }
+
+    public void setProxyCredentials(String realm, String host, int port,
+                                    String usernamepassword)
+    {
+        AuthScope authScope = new AuthScope (host, port, realm);
+        UsernamePasswordCredentials creds = new UsernamePasswordCredentials (usernamepassword);
+        HttpState state = getHttpState();
+
+        state.setProxyCredentials (authScope, creds);
+    }
+
+    public void setProxyCredentials(String realm, String host, int port,
+                                    String username, String password)
+    {
+        AuthScope authScope = new AuthScope (host, port, realm);
+        UsernamePasswordCredentials creds = new UsernamePasswordCredentials (username,password);
+        HttpState state = getHttpState();
+
+        state.setProxyCredentials (authScope, creds);
+    }
+
+    public void setProxyCredentials(String realm, String host, int port,
+                                    String username, String password,
+                                    String domain, String thisHost)
+    {
+        AuthScope authScope = new AuthScope (host, port, realm);
+        NTCredentials creds = new NTCredentials (username,password, thisHost, domain);
+        HttpState state = getHttpState();
+
+        state.setProxyCredentials (authScope, creds);
+    }
+
+    public void clearCredentials()
+    {
+        HttpState state = getHttpState();
+        state.clearCredentials();
+    }
+
+    public void clearProxyCredentials()
+    {
+        HttpState state = getHttpState();
+        state.clearProxyCredentials();
+    }
+}
+
diff --git a/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/HttpClientConfig.java b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/HttpClientConfig.java
new file mode 100644
index 0000000..a378b81
--- /dev/null
+++ b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/HttpClientConfig.java
@@ -0,0 +1,292 @@
+/*
+ *
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License").  You
+ * may not use this file except in compliance with the License. You can obtain
+ * a copy of the License at https://jersey.dev.java.net/CDDL+GPL.html
+ * or jersey/legal/LICENSE.txt.  See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at jersey/legal/LICENSE.txt.
+ * Sun designates this particular file as subject to the "Classpath" exception
+ * as provided by Sun in the GPL Version 2 section of the License file that
+ * accompanied this code.  If applicable, add the following below the License
+ * Header, with the fields enclosed by brackets [] replaced by your own
+ * identifying information: "Portions Copyrighted [year]
+ * [name of copyright owner]"
+ *
+ * Contributor(s):
+ *
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license."  If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above.  However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package com.sun.jersey.impl.client.httpclient;
+
+import com.sun.jersey.api.client.config.ClientConfig;
+
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.httpclient.Credentials;
+
+/**
+   Contains configuration options specific to clients that root with
+   the {@link com.sun.jersey.impl.client.httpclient.HttpClientHandler}.
+
+   @author jorgew
+**/
+
+public interface HttpClientConfig extends ClientConfig
+{
+    /**
+       A value of "true" indicates that the client should
+       interactively prompt for credentials should it receive a 401
+       response.
+
+       The value MUST be an instance of {@link java.lang.Boolean}.
+       If the property is absent the default value is "true"
+     **/
+    public static final String PROPERTY_INTERACTIVE =
+        "com.sun.jersey.impl.client.httpclient.interactive";
+
+    /**
+       A value of "true" indicates the client should handle cookies
+       automatically using HttpClient's default cookie policy. A value
+       of "false" will cause the client to ignore all cookies.
+
+       The value MUST be an instance of {@link java.lang.Boolean}.
+       If the property is absent the default value is "false"
+    **/
+    public static final String PROPERTY_HANDLE_COOKIES =
+        "com.sun.jersey.impl.client.httpclient.handleCookies";
+
+    /**
+       The credential provider that should be used to retrive
+       credentials from a user. The provider will be used only if
+       PROPERTY_INTERACTIVE is set to true.
+
+       The value MUST be an instance of {@link
+       org.apache.commons.httpclient.auth.CredentialsProvider}.  If
+       the property is absent a default provider will be used.
+    **/
+    public static final String PROPERTY_CREDENTIALS_PROVIDER =
+        "com.sun.jersey.impl.client.httpclient.credentialsProvider";
+
+    /**
+       A value of "true" indicates that a client should send an
+       authentication request even before the server gives a 401
+       response.
+
+       If the value of this property is set to "true" default
+       credientials must be set for the target or proxy.
+
+       The value MUST be an instance of {@link java.lang.Boolean}.
+       If the property is absent the default value is "false"
+     **/
+    public static final String PROPERTY_PREEMPTIVE_AUTHENTICATION =
+        "com.sun.jersey.impl.client.httpclient.preemptiveAuthentication";
+
+    /**
+       A value of "true" indicates that a client should use a proxy
+       when connecting to a host. {@link #PROPERTY_PROXY_HOST} and
+       {@link #PROPERTY_PROXY_PORT} MUST be set.
+
+       The value MUST be an instance of {@link java.lang.Boolean}. If
+       the property is absent the default value is "false".
+     **/
+    public static final String PROPERTY_PROXY_SET =
+        "com.sun.jersey.impl.client.httpclient.proxySet";
+
+    /**
+       A value indicating the proxy host to use.  The proxy host will
+       only be set if {@link #PROPERTY_PROXY_SET} is set to "true".
+
+       The value MUST be an instance of {@link java.lang.String}.  If
+       the property is absent the default value is "localhost".
+     **/
+    public static final String PROPERTY_PROXY_HOST =
+        "com.sun.jersey.impl.client.httpclient.proxyHost";
+
+    /**
+       A value indicating the proxy port to use.  The proxy port will
+       only be set if {@link #PROPERTY_PROXY_SET} is set to "true".
+
+       The value MUST be an instance of {@link java.lang.Integer}. If
+       the property is absent the default value is 8080.
+     **/
+    public static final String PROPERTY_PROXY_PORT =
+        "com.sun.jersey.impl.client.httpclient.proxyPort";
+
+    /**
+       Sets the credentials for the given authentication scope.  Any
+       previous credentials for the given scope will be overwritten.
+
+       @param realm The authentication realm.  The null realm
+       signifies default credentials for the given host, which should
+       be used when no credentials have been explicitly supplied for
+       the challenging realm.
+       @param host The host the realm belongs to. The null host
+       signifies default credentials which should be used when no
+       credentials have been explicitly supplied for the challenging
+       host.
+       @param port The port the realm belongs to. A negitive port
+       signifies the credentials are applicaple to any port when no
+       credentials have been explicitly supplied for the challenging
+       port.
+       @param usernamepassword The username:password formed string.
+    **/
+    public void setCredentials(String realm, String host, int port,
+                               String usernamepassword);
+
+    /**
+       Sets the credentials for the given authentication scope.  Any
+       previous credentials for the given scope will be overwritten.
+
+       @param realm The authentication realm.  The null realm
+       signifies default credentials for the given host, which should
+       be used when no credentials have been explicitly supplied for
+       the challenging realm.
+       @param host The host the realm belongs to. The null host
+       signifies default credentials which should be used when no
+       credentials have been explicitly supplied for the challenging
+       host.
+       @param port The port the realm belongs to. A negitive port
+       signifies the credentials are applicaple to any port when no
+       credentials have been explicitly supplied for the challenging
+       port.
+       @param username The username
+       @param password The password
+    **/
+    public void setCredentials(String realm, String host, int port,
+                               String username, String password);
+
+    /**
+       Sets the credentials for the given authentication scope.  Any
+       previous credentials for the given scope will be overwritten.
+
+       This method should be used when setting credentials for the
+       NTLM authentication scheme.
+
+       @param realm The authentication realm.  The null realm
+       signifies default credentials for the given host, which should
+       be used when no credentials have been explicitly supplied for
+       the challenging realm.
+       @param host The host the realm belongs to. The null host
+       signifies default credentials which should be used when no
+       credentials have been explicitly supplied for the challenging
+       host.
+       @param port The port the realm belongs to. A negitive port
+       signifies the credentials are applicaple to any port when no
+       credentials have been explicitly supplied for the challenging
+       port.
+       @param username The username, this should not include the
+       domain to authenticate with. For example: "user" is correct
+       wheareas "DOMAIN\\user" is not.
+       @param password The password
+       @param thisHost The host the authentication requiest is originating
+       from. Essentially, the computer name for this machine.
+       @param domain The domain to authentice with.
+    **/
+    public void setCredentials(String realm, String host, int port,
+                               String username, String password,
+                               String thisHost, String domain);
+
+    /**
+       Sets the proxy credentials for the given authentication scope.
+       Any previous credentials for the given scope will be
+       overwritten.
+
+       @param realm The authentication realm.  The null realm
+       signifies default credentials for the given host, which should
+       be used when no credentials have been explicitly supplied for
+       the challenging realm.
+       @param host The host the realm belongs to. The null host
+       signifies default credentials which should be used when no
+       credentials have been explicitly supplied for the challenging
+       host.
+       @param port The port the realm belongs to. A negitive port
+       signifies the credentials are applicaple to any port when no
+       credentials have been explicitly supplied for the challenging
+       port.
+       @param usernamepassword The username:password formed string.
+    **/
+    public void setProxyCredentials(String realm, String host, int port,
+                                    String usernamepassword);
+
+    /**
+       Sets the proxy credentials for the given authentication scope.
+       Any previous credentials for the given scope will be
+       overwritten.
+
+       @param realm The authentication realm.  The null realm
+       signifies default credentials for the given host, which should
+       be used when no credentials have been explicitly supplied for
+       the challenging realm.
+       @param host The host the realm belongs to. The null host
+       signifies default credentials which should be used when no
+       credentials have been explicitly supplied for the challenging
+       host.
+       @param port The port the realm belongs to. A negitive port
+       signifies the credentials are applicaple to any port when no
+       credentials have been explicitly supplied for the challenging
+       port.
+       @param username The username
+       @param password The password
+    **/
+    public void setProxyCredentials(String realm, String host, int port,
+                                    String username, String password);
+
+    /**
+       Sets the proxy credentials for the given authentication scope.
+       Any previous credentials for the given scope will be
+       overwritten.
+
+       This method should be used when setting credentials for the
+       NTLM authentication scheme.
+
+       @param realm The authentication realm.  The null realm
+       signifies default credentials for the given host, which should
+       be used when no credentials have been explicitly supplied for
+       the challenging realm.
+       @param host The host the realm belongs to. The null host
+       signifies default credentials which should be used when no
+       credentials have been explicitly supplied for the challenging
+       host.
+       @param port The port the realm belongs to. A negitive port
+       signifies the credentials are applicaple to any port when no
+       credentials have been explicitly supplied for the challenging
+       port.
+       @param username The username, this should not include the
+       domain to authenticate with. For example: "user" is correct
+       wheareas "DOMAIN\\user" is not.
+       @param password The password
+       @param thisHost The host the authentication requiest is originating
+       from. Essentially, the computer name for this machine.
+       @param domain The domain to authentice with.
+    **/
+    public void setProxyCredentials(String realm, String host, int port,
+                                    String username, String password,
+                                    String thisHost, String domain);
+
+    /**
+       Clears all credentials.
+    **/
+    public void clearCredentials();
+
+    /**
+       Clears all proxy credentials.
+    **/
+    public void clearProxyCredentials();
+}
diff --git a/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/HttpClientHandler.java b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/HttpClientHandler.java
new file mode 100644
index 0000000..d27b12a
--- /dev/null
+++ b/jersey/jersey-client/src/main/java/com/sun/jersey/impl/client/httpclient/HttpClientHandler.java
@@ -0,0 +1,478 @@
+/*
+ *
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License").  You
+ * may not use this file except in compliance with the License. You can obtain
+ * a copy of the License at https://jersey.dev.java.net/CDDL+GPL.html
+ * or jersey/legal/LICENSE.txt.  See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at jersey/legal/LICENSE.txt.
+ * Sun designates this particular file as subject to the "Classpath" exception
+ * as provided by Sun in the GPL Version 2 section of the License file that
+ * accompanied this code.  If applicable, add the following below the License
+ * Header, with the fields enclosed by brackets [] replaced by your own
+ * identifying information: "Portions Copyrighted [year]
+ * [name of copyright owner]"
+ *
+ * Contributor(s):
+ *
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license."  If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above.  However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package com.sun.jersey.impl.client.httpclient;
+
+import java.io.IOException;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.GenericEntity;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.RuntimeDelegate;
+import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate;
+
+import com.sun.jersey.api.InBoundHeaders;
+import com.sun.jersey.api.client.ClientHandler;
+import com.sun.jersey.api.client.ClientHandlerException;
+import com.sun.jersey.api.client.ClientRequest;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.GenericType;
+
+import com.sun.jersey.spi.MessageBodyWorkers;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpState;
+import org.apache.commons.httpclient.HostConfiguration;
+import org.apache.commons.httpclient.ProxyHost;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.params.HttpClientParams;
+import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.OptionsMethod;
+import org.apache.commons.httpclient.methods.HeadMethod;
+import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
+import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
+import org.apache.commons.httpclient.cookie.CookiePolicy;
+import org.apache.commons.httpclient.auth.CredentialsProvider;
+
+
+/**
+   A root handler with Jakarta Commons HttpClient acting as a backend.
+   <p>
+     <strong>Please Note:</strong>
+     <ul>
+        <li>There is a single HTTPClient per HttpClientHandler. It may
+        be worth while to use it as a root for multiple Clients to
+        save on resources. There should typically be a single
+        HttpClientHandler per application.</li>
+        <li>Client operations are thread safe, the HTTP connection may
+        be shared between different threads. If you retrive a
+        response entity input stream you <strong>must</strong> call
+        close() on the stream when you're done with it in order to
+        release the connection to other threads.</li>
+        <li>In this initial implementation, only standard methods
+        (GET, POST, DELETE, OPTIONS, HEAD, and PUT) are
+        supported.</li>
+        <li>Chunk encoding is a true/false operation in HTTPClient
+         there's no way of specifying a chunk size.  If you set
+         PROPERTY_CHUNKED_ENCODING_SIZE to anything other than null it
+         will be set to true.</li>
+        <li>In this initial implementation, chunk encoding probably
+        doesn't matter since we write entities to a byte array before
+        we transmit them</li>
+     </ul>
+   </p>
+
+   @author jorgew
+**/
+
+public final class HttpClientHandler implements ClientHandler
+{
+    private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
+    private static final DefaultCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER = new DefaultCredentialsProvider();
+
+    @Context private MessageBodyWorkers bodyContext;
+    private HttpClient client = new HttpClient(new MultiThreadedHttpConnectionManager());
+
+    public ClientResponse handle (ClientRequest cr)
+        throws ClientHandlerException {
+        HttpMethod method = null;
+        try {
+
+            Map<String, Object> props = cr.getProperties();
+            HttpClientParams clientParams = client.getParams();
+            HttpConnectionManagerParams params = client.getHttpConnectionManager().getParams();
+
+            Boolean handleCookies = (Boolean) props.get(HttpClientConfig.PROPERTY_HANDLE_COOKIES);
+            if ((handleCookies == null) || (!handleCookies)) {
+                clientParams.setCookiePolicy (CookiePolicy.IGNORE_COOKIES);
+            }
+            Boolean proxySet = (Boolean) props.get(HttpClientConfig.PROPERTY_PROXY_SET);
+            if ((proxySet != null) && (proxySet)) {
+                HostConfiguration hostConfig = client.getHostConfiguration();
+                String proxyHost = (String) props.get(HttpClientConfig.PROPERTY_PROXY_HOST);
+                if (proxyHost == null) {
+                    proxyHost = "localhost";
+                }
+
+                Integer proxyPort = (Integer) props.get(HttpClientConfig.PROPERTY_PROXY_PORT);
+                if (proxyPort == null) {
+                    proxyPort = 8080;
+                }
+
+                String setHost = hostConfig.getProxyHost();
+                int setPort = hostConfig.getProxyPort();
+
+                if ((setHost == null) ||
+                    (!setHost.equals (proxyHost)) ||
+                    (setPort == -1) ||
+                    (setPort != proxyPort)) {
+                    hostConfig.setProxyHost (new ProxyHost (proxyHost, proxyPort));
+                }
+
+            } else {
+                client.setHostConfiguration (HostConfiguration.ANY_HOST_CONFIGURATION);
+            }
+
+            HttpState httpState = (HttpState) props.get(DefaultHttpClientConfig.PROPERTY_HTTP_STATE);
+            if (httpState != null) {
+                client.setState (httpState);
+            }
+
+            Boolean preemptiveAuth  = (Boolean) props.get(HttpClientConfig.PROPERTY_PREEMPTIVE_AUTHENTICATION);
+            if (preemptiveAuth != null) {
+                clientParams.setAuthenticationPreemptive (preemptiveAuth);
+            } else {
+                clientParams.setAuthenticationPreemptive (false);
+            }
+
+            Boolean interactiveAuth = (Boolean) props.get(HttpClientConfig.PROPERTY_INTERACTIVE);
+            if ((interactiveAuth == null) || (interactiveAuth)) {
+                CredentialsProvider provider = (CredentialsProvider) props.get(HttpClientConfig.PROPERTY_CREDENTIALS_PROVIDER);
+                if (provider == null) {
+                    provider = DEFAULT_CREDENTIALS_PROVIDER;
+                }
+                clientParams.setParameter(CredentialsProvider.PROVIDER, provider);
+            } else {
+                clientParams.setParameter(CredentialsProvider.PROVIDER, null);
+            }
+
+            Integer readTimeout = (Integer) props.get(HttpClientConfig.PROPERTY_READ_TIMEOUT);
+            if (readTimeout != null) {
+                params.setSoTimeout (readTimeout);
+            }
+
+            Integer connectTimeout = (Integer) props.get(HttpClientConfig.PROPERTY_CONNECT_TIMEOUT);
+            if (connectTimeout != null) {
+                params.setConnectionTimeout (connectTimeout);
+            }
+
+            String strMethod = cr.getMethod();
+            String uri = cr.getURI().toString();
+
+            if (strMethod.equals ("GET")) {
+                method = new GetMethod (uri);
+            } else if (strMethod.equals ("POST")) {
+                method = new PostMethod (uri);
+            } else if (strMethod.equals ("PUT")) {
+                method = new PutMethod (uri);
+            } else if (strMethod.equals ("DELETE")) {
+                method = new DeleteMethod (uri);
+            } else if (strMethod.equals ("HEAD")) {
+                method = new HeadMethod (uri);
+            } else if (strMethod.equals ("OPTIONS")) {
+                method = new OptionsMethod(uri);
+            } else {
+                throw new ClientHandlerException ("Method "+method+" is not supported.");
+            }
+
+            method.setDoAuthentication (true);
+
+            writeHeaders (cr.getMetadata(), method);
+
+            if (method instanceof EntityEnclosingMethod) {
+                EntityEnclosingMethod entMethod = (EntityEnclosingMethod) method;
+                Integer chunkedEncodingSize = (Integer) props.get(HttpClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE);
+                if (chunkedEncodingSize != null) {
+                    //
+                    //  There doesn't seems to be a way to set the
+                    //  chunk size.
+                    //
+                    entMethod.setContentChunked (true);
+                } else {
+                    entMethod.setContentChunked (false);
+                }
+
+                Object entity = cr.getEntity();
+                if (entity != null) {
+                    writeEntity (entMethod, cr, entity);
+                }
+            } else {
+                Boolean followRedirects = (Boolean) props.get(HttpClientConfig.PROPERTY_FOLLOW_REDIRECTS);
+                if (followRedirects != null){
+                    method.setFollowRedirects (followRedirects);
+                } else {
+                    method.setFollowRedirects (true);
+                }
+            }
+
+            client.executeMethod (method);
+
+            return new HttpClientResponse (method);
+
+        }catch (Exception e) {
+            if (method != null)
+                {
+                    method.releaseConnection();
+                }
+            throw new ClientHandlerException (e);
+        }
+    }
+
+    private void writeEntity (EntityEnclosingMethod entMethod, ClientRequest cr, Object entity)
+        throws IOException {
+
+        MultivaluedMap<String, Object> metadata = cr.getMetadata();
+        MediaType mediaType = null;
+        final Object mediaTypeHeader = metadata.getFirst("Content-Type");
+        if (mediaTypeHeader instanceof MediaType) {
+            mediaType = (MediaType)mediaTypeHeader;
+        } else {
+            if (mediaTypeHeader != null) {
+                mediaType = MediaType.valueOf(mediaTypeHeader.toString());
+            } else {
+                mediaType = new MediaType("application", "octet-stream");
+            }
+        }
+
+        Type entityType = null;
+        if (entity instanceof GenericEntity) {
+            final GenericEntity ge = (GenericEntity)entity;
+            entityType = ge.getType();
+            entity = ge.getEntity();
+        } else {
+            entityType = entity.getClass();
+        }
+        final Class entityClass = entity.getClass();
+
+        final MessageBodyWriter bw = bodyContext.getMessageBodyWriter(entityClass, entityType,
+                                                                      EMPTY_ANNOTATIONS, mediaType);
+        if (bw == null) {
+            throw new ClientHandlerException(
+                    "A message body writer for Java type, " + entity.getClass() +
+                    ", and MIME media type, " + mediaType + ", was not found");
+        }
+
+        final ByteArrayOutputStream  bout = new ByteArrayOutputStream();
+        final OutputStream out = cr.getAdapter().adapt (cr, bout);
+        bw.writeTo(entity, entityClass, entityType,
+                   EMPTY_ANNOTATIONS, mediaType, metadata, out);
+        out.flush();
+        out.close();
+
+        entMethod.setRequestEntity (new ByteArrayRequestEntity (bout.toByteArray(), mediaType.toString()));
+    }
+
+    private void writeHeaders (MultivaluedMap<String, Object> metadata, HttpMethod method)
+    {
+        for (Map.Entry<String, List<Object>> e : metadata.entrySet()) {
+            List<Object> vs = e.getValue();
+            if (vs.size() == 1) {
+                method.setRequestHeader (e.getKey(), getHeaderValue(vs.get(0)));
+            } else {
+                StringBuilder b = new StringBuilder();
+                boolean add = false;
+                for (Object v : e.getValue()) {
+                    if (add) b.append(',');
+                    add = true;
+                    b.append(getHeaderValue(v));
+                }
+                method.setRequestHeader(e.getKey(), b.toString());
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private String getHeaderValue(Object headerValue) {
+        HeaderDelegate hp = RuntimeDelegate.getInstance().
+                createHeaderDelegate(headerValue.getClass());
+        return hp.toString(headerValue);
+    }
+
+    private final class HttpClientResponseInputStream extends FilterInputStream {
+        private final HttpMethod method;
+
+        HttpClientResponseInputStream (HttpMethod method)
+            throws IOException
+        {
+            super(method.getResponseBodyAsStream());
+            this.method = method;
+        }
+
+        @Override
+        public void close()
+            throws IOException
+        {
+            super.close();
+            method.releaseConnection();
+        }
+    }
+
+    private final class HttpClientResponse extends ClientResponse {
+        private final HttpMethod method;
+        private final MultivaluedMap<String, String> metadata;
+        private Map<String, Object> properties;
+        private InputStream in;
+        private int status;
+
+        HttpClientResponse(HttpMethod method)
+        {
+            this.method = method;
+            this.status = method.getStatusCode();
+
+            this.metadata = new InBoundHeaders();
+            Header[] respHeaders = method.getResponseHeaders();
+            for (Header header : respHeaders) {
+                List<String> list = metadata.get(header.getName());
+                if (list == null) {
+                    list = new ArrayList<String>();
+                }
+                list.add (header.getValue());
+                metadata.put (header.getName(), list);
+            }
+
+            try  {
+                in = new HttpClientResponseInputStream(method);
+            } catch (IOException ex) {
+                throw new IllegalArgumentException (ex);
+            }
+        }
+
+        @Override
+        public int getStatus() {
+            return status;
+        }
+
+        @Override
+        public void setStatus (int status) {
+            this.status = status;
+        }
+
+        @Override
+        public Response.Status getResponseStatus() {
+            return Response.Status.fromStatusCode(status);
+        }
+
+        @Override
+        public void setResponseStatus(Response.Status status) {
+            setStatus(status.getStatusCode());
+        }
+
+        @Override
+        public MultivaluedMap<String, String> getMetadata() {
+            return metadata;
+        }
+
+        @Override
+        public boolean hasEntity() {
+            if ((method instanceof HeadMethod) ||
+                (in == null)) {
+                return false;
+            }
+
+            Header contentLength = method.getResponseHeader("Content-Length");
+            if (contentLength != null){
+                try {
+                    int len = Integer.parseInt(contentLength.getValue());
+                    return len > 0 || len == -1;
+                }catch (NumberFormatException nfe) {
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        public InputStream getEntityInputStream() {
+            return in;
+        }
+
+        @Override
+        public void setEntityInputStream(InputStream in) {
+            this.in = in;
+        }
+
+        @Override
+        public <T> T getEntity(Class<T> c) {
+            return getEntity(c, c);
+        }
+
+        @Override
+        public <T> T getEntity(GenericType<T> gt) {
+            return getEntity(gt.getRawClass(), gt.getType());
+        }
+
+        private <T> T getEntity(Class<T> c, Type type) {
+            try {
+                MediaType mediaType = getType();
+                final MessageBodyReader<T> br = bodyContext.getMessageBodyReader(
+                        c, type,
+                        EMPTY_ANNOTATIONS, mediaType);
+                if (br == null) {
+                    throw new ClientHandlerException(
+                            "A message body reader for Java type, " + c +
+                            ", and MIME media type, " + mediaType + ", was not found");
+                }
+                T t = br.readFrom(c, type, EMPTY_ANNOTATIONS, mediaType, metadata, in);
+                if (!(t instanceof InputStream)) {
+                    in.close();
+                }
+                in = null;
+                return t;
+            } catch (IOException ex) {
+                throw new IllegalArgumentException(ex);
+            }
+        }
+
+        @Override
+        public Map<String, Object> getProperties() {
+            if (properties != null) return properties;
+
+            return properties = new HashMap<String, Object>();
+        }
+    }
+}