diff -r 000000000000 -r 6474c204b198 mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java Wed Dec 31 06:09:35 2014 +0100
@@ -0,0 +1,1256 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.client.AuthenticationHandler;
+import ch.boye.httpclientandroidlib.client.RedirectStrategy;
+import ch.boye.httpclientandroidlib.client.RequestDirector;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler;
+import ch.boye.httpclientandroidlib.client.NonRepeatableRequestException;
+import ch.boye.httpclientandroidlib.client.RedirectException;
+import ch.boye.httpclientandroidlib.client.UserTokenHandler;
+import ch.boye.httpclientandroidlib.client.methods.AbortableHttpRequest;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.params.ClientPNames;
+import ch.boye.httpclientandroidlib.client.params.HttpClientParams;
+import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.conn.BasicManagedEntity;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.params.ConnManagerParams;
+import ch.boye.httpclientandroidlib.conn.routing.BasicRouteDirector;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRouteDirector;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.entity.BufferedHttpEntity;
+import ch.boye.httpclientandroidlib.impl.conn.ConnectionShutdownException;
+import ch.boye.httpclientandroidlib.message.BasicHttpRequest;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
+import ch.boye.httpclientandroidlib.protocol.ExecutionContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * Default implementation of {@link RequestDirector}.
+ *
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ *
+ * - {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#PROTOCOL_VERSION}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#STRICT_TRANSFER_ENCODING}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USE_EXPECT_CONTINUE}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#WAIT_FOR_CONTINUE}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USER_AGENT}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SOCKET_BUFFER_SIZE}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_TIMEOUT}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_LINGER}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_REUSEADDR}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#TCP_NODELAY}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#CONNECTION_TIMEOUT}
+ * - {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#STALE_CONNECTION_CHECK}
+ * - {@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#FORCED_ROUTE}
+ * - {@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#LOCAL_ADDRESS}
+ * - {@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#DEFAULT_PROXY}
+ * - {@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#DATE_PATTERNS}
+ * - {@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#SINGLE_COOKIE_HEADER}
+ * - {@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#COOKIE_POLICY}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_AUTHENTICATION}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_REDIRECTS}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#MAX_REDIRECTS}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#ALLOW_CIRCULAR_REDIRECTS}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#VIRTUAL_HOST}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HOST}
+ * - {@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HEADERS}
+ *
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe // e.g. managedConn
+public class DefaultRequestDirector implements RequestDirector {
+
+ public HttpClientAndroidLog log;
+
+ /** The connection manager. */
+ protected final ClientConnectionManager connManager;
+
+ /** The route planner. */
+ protected final HttpRoutePlanner routePlanner;
+
+ /** The connection re-use strategy. */
+ protected final ConnectionReuseStrategy reuseStrategy;
+
+ /** The keep-alive duration strategy. */
+ protected final ConnectionKeepAliveStrategy keepAliveStrategy;
+
+ /** The request executor. */
+ protected final HttpRequestExecutor requestExec;
+
+ /** The HTTP protocol processor. */
+ protected final HttpProcessor httpProcessor;
+
+ /** The request retry handler. */
+ protected final HttpRequestRetryHandler retryHandler;
+
+ /** The redirect handler. */
+ @Deprecated
+ protected final ch.boye.httpclientandroidlib.client.RedirectHandler redirectHandler = null;
+
+ /** The redirect strategy. */
+ protected final RedirectStrategy redirectStrategy;
+
+ /** The target authentication handler. */
+ protected final AuthenticationHandler targetAuthHandler;
+
+ /** The proxy authentication handler. */
+ protected final AuthenticationHandler proxyAuthHandler;
+
+ /** The user token handler. */
+ protected final UserTokenHandler userTokenHandler;
+
+ /** The HTTP parameters. */
+ protected final HttpParams params;
+
+ /** The currently allocated connection. */
+ protected ManagedClientConnection managedConn;
+
+ protected final AuthState targetAuthState;
+
+ protected final AuthState proxyAuthState;
+
+ private int execCount;
+
+ private int redirectCount;
+
+ private int maxRedirects;
+
+ private HttpHost virtualHost;
+
+ @Deprecated
+ public DefaultRequestDirector(
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final ch.boye.httpclientandroidlib.client.RedirectHandler redirectHandler,
+ final AuthenticationHandler targetAuthHandler,
+ final AuthenticationHandler proxyAuthHandler,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+ this(new HttpClientAndroidLog(DefaultRequestDirector.class),
+ requestExec, conman, reustrat, kastrat, rouplan, httpProcessor, retryHandler,
+ new DefaultRedirectStrategyAdaptor(redirectHandler),
+ targetAuthHandler, proxyAuthHandler, userTokenHandler, params);
+ }
+
+
+ /**
+ * @since 4.1
+ */
+ public DefaultRequestDirector(
+ final HttpClientAndroidLog log,
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final RedirectStrategy redirectStrategy,
+ final AuthenticationHandler targetAuthHandler,
+ final AuthenticationHandler proxyAuthHandler,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+
+ if (log == null) {
+ throw new IllegalArgumentException
+ ("Log may not be null.");
+ }
+ if (requestExec == null) {
+ throw new IllegalArgumentException
+ ("Request executor may not be null.");
+ }
+ if (conman == null) {
+ throw new IllegalArgumentException
+ ("Client connection manager may not be null.");
+ }
+ if (reustrat == null) {
+ throw new IllegalArgumentException
+ ("Connection reuse strategy may not be null.");
+ }
+ if (kastrat == null) {
+ throw new IllegalArgumentException
+ ("Connection keep alive strategy may not be null.");
+ }
+ if (rouplan == null) {
+ throw new IllegalArgumentException
+ ("Route planner may not be null.");
+ }
+ if (httpProcessor == null) {
+ throw new IllegalArgumentException
+ ("HTTP protocol processor may not be null.");
+ }
+ if (retryHandler == null) {
+ throw new IllegalArgumentException
+ ("HTTP request retry handler may not be null.");
+ }
+ if (redirectStrategy == null) {
+ throw new IllegalArgumentException
+ ("Redirect strategy may not be null.");
+ }
+ if (targetAuthHandler == null) {
+ throw new IllegalArgumentException
+ ("Target authentication handler may not be null.");
+ }
+ if (proxyAuthHandler == null) {
+ throw new IllegalArgumentException
+ ("Proxy authentication handler may not be null.");
+ }
+ if (userTokenHandler == null) {
+ throw new IllegalArgumentException
+ ("User token handler may not be null.");
+ }
+ if (params == null) {
+ throw new IllegalArgumentException
+ ("HTTP parameters may not be null");
+ }
+ this.log = log;
+ this.requestExec = requestExec;
+ this.connManager = conman;
+ this.reuseStrategy = reustrat;
+ this.keepAliveStrategy = kastrat;
+ this.routePlanner = rouplan;
+ this.httpProcessor = httpProcessor;
+ this.retryHandler = retryHandler;
+ this.redirectStrategy = redirectStrategy;
+ this.targetAuthHandler = targetAuthHandler;
+ this.proxyAuthHandler = proxyAuthHandler;
+ this.userTokenHandler = userTokenHandler;
+ this.params = params;
+
+ this.managedConn = null;
+
+ this.execCount = 0;
+ this.redirectCount = 0;
+ this.maxRedirects = this.params.getIntParameter(ClientPNames.MAX_REDIRECTS, 100);
+ this.targetAuthState = new AuthState();
+ this.proxyAuthState = new AuthState();
+ } // constructor
+
+
+ private RequestWrapper wrapRequest(
+ final HttpRequest request) throws ProtocolException {
+ if (request instanceof HttpEntityEnclosingRequest) {
+ return new EntityEnclosingRequestWrapper(
+ (HttpEntityEnclosingRequest) request);
+ } else {
+ return new RequestWrapper(
+ request);
+ }
+ }
+
+
+ protected void rewriteRequestURI(
+ final RequestWrapper request,
+ final HttpRoute route) throws ProtocolException {
+ try {
+
+ URI uri = request.getURI();
+ if (route.getProxyHost() != null && !route.isTunnelled()) {
+ // Make sure the request URI is absolute
+ if (!uri.isAbsolute()) {
+ HttpHost target = route.getTargetHost();
+ uri = URIUtils.rewriteURI(uri, target);
+ request.setURI(uri);
+ }
+ } else {
+ // Make sure the request URI is relative
+ if (uri.isAbsolute()) {
+ uri = URIUtils.rewriteURI(uri, null);
+ request.setURI(uri);
+ }
+ }
+
+ } catch (URISyntaxException ex) {
+ throw new ProtocolException("Invalid URI: " +
+ request.getRequestLine().getUri(), ex);
+ }
+ }
+
+
+ // non-javadoc, see interface ClientRequestDirector
+ public HttpResponse execute(HttpHost target, HttpRequest request,
+ HttpContext context)
+ throws HttpException, IOException {
+
+ HttpRequest orig = request;
+ RequestWrapper origWrapper = wrapRequest(orig);
+ origWrapper.setParams(params);
+ HttpRoute origRoute = determineRoute(target, origWrapper, context);
+
+ virtualHost = (HttpHost) orig.getParams().getParameter(
+ ClientPNames.VIRTUAL_HOST);
+
+ // HTTPCLIENT-1092 - add the port if necessary
+ if (virtualHost != null && virtualHost.getPort() == -1)
+ {
+ int port = target.getPort();
+ if (port != -1){
+ virtualHost = new HttpHost(virtualHost.getHostName(), port, virtualHost.getSchemeName());
+ }
+ }
+
+ RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute);
+
+ boolean reuse = false;
+ boolean done = false;
+ try {
+ HttpResponse response = null;
+ while (!done) {
+ // In this loop, the RoutedRequest may be replaced by a
+ // followup request and route. The request and route passed
+ // in the method arguments will be replaced. The original
+ // request is still available in 'orig'.
+
+ RequestWrapper wrapper = roureq.getRequest();
+ HttpRoute route = roureq.getRoute();
+ response = null;
+
+ // See if we have a user token bound to the execution context
+ Object userToken = context.getAttribute(ClientContext.USER_TOKEN);
+
+ // Allocate connection if needed
+ if (managedConn == null) {
+ ClientConnectionRequest connRequest = connManager.requestConnection(
+ route, userToken);
+ if (orig instanceof AbortableHttpRequest) {
+ ((AbortableHttpRequest) orig).setConnectionRequest(connRequest);
+ }
+
+ long timeout = ConnManagerParams.getTimeout(params);
+ try {
+ managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS);
+ } catch(InterruptedException interrupted) {
+ InterruptedIOException iox = new InterruptedIOException();
+ iox.initCause(interrupted);
+ throw iox;
+ }
+
+ if (HttpConnectionParams.isStaleCheckingEnabled(params)) {
+ // validate connection
+ if (managedConn.isOpen()) {
+ this.log.debug("Stale connection check");
+ if (managedConn.isStale()) {
+ this.log.debug("Stale connection detected");
+ managedConn.close();
+ }
+ }
+ }
+ }
+
+ if (orig instanceof AbortableHttpRequest) {
+ ((AbortableHttpRequest) orig).setReleaseTrigger(managedConn);
+ }
+
+ try {
+ tryConnect(roureq, context);
+ } catch (TunnelRefusedException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage());
+ }
+ response = ex.getResponse();
+ break;
+ }
+
+ // Reset headers on the request wrapper
+ wrapper.resetHeaders();
+
+ // Re-write request URI if needed
+ rewriteRequestURI(wrapper, route);
+
+ // Use virtual host if set
+ target = virtualHost;
+
+ if (target == null) {
+ target = route.getTargetHost();
+ }
+
+ HttpHost proxy = route.getProxyHost();
+
+ // Populate the execution context
+ context.setAttribute(ExecutionContext.HTTP_TARGET_HOST,
+ target);
+ context.setAttribute(ExecutionContext.HTTP_PROXY_HOST,
+ proxy);
+ context.setAttribute(ExecutionContext.HTTP_CONNECTION,
+ managedConn);
+ context.setAttribute(ClientContext.TARGET_AUTH_STATE,
+ targetAuthState);
+ context.setAttribute(ClientContext.PROXY_AUTH_STATE,
+ proxyAuthState);
+
+ // Run request protocol interceptors
+ requestExec.preProcess(wrapper, httpProcessor, context);
+
+ response = tryExecute(roureq, context);
+ if (response == null) {
+ // Need to start over
+ continue;
+ }
+
+ // Run response protocol interceptors
+ response.setParams(params);
+ requestExec.postProcess(response, httpProcessor, context);
+
+
+ // The connection is in or can be brought to a re-usable state.
+ reuse = reuseStrategy.keepAlive(response, context);
+ if (reuse) {
+ // Set the idle duration of this connection
+ long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
+ if (this.log.isDebugEnabled()) {
+ String s;
+ if (duration > 0) {
+ s = "for " + duration + " " + TimeUnit.MILLISECONDS;
+ } else {
+ s = "indefinitely";
+ }
+ this.log.debug("Connection can be kept alive " + s);
+ }
+ managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS);
+ }
+
+ RoutedRequest followup = handleResponse(roureq, response, context);
+ if (followup == null) {
+ done = true;
+ } else {
+ if (reuse) {
+ // Make sure the response body is fully consumed, if present
+ HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ // entity consumed above is not an auto-release entity,
+ // need to mark the connection re-usable explicitly
+ managedConn.markReusable();
+ } else {
+ managedConn.close();
+ invalidateAuthIfSuccessful(this.proxyAuthState);
+ invalidateAuthIfSuccessful(this.targetAuthState);
+ }
+ // check if we can use the same connection for the followup
+ if (!followup.getRoute().equals(roureq.getRoute())) {
+ releaseConnection();
+ }
+ roureq = followup;
+ }
+
+ if (managedConn != null && userToken == null) {
+ userToken = userTokenHandler.getUserToken(context);
+ context.setAttribute(ClientContext.USER_TOKEN, userToken);
+ if (userToken != null) {
+ managedConn.setState(userToken);
+ }
+ }
+
+ } // while not done
+
+
+ // check for entity, release connection if possible
+ if ((response == null) || (response.getEntity() == null) ||
+ !response.getEntity().isStreaming()) {
+ // connection not needed and (assumed to be) in re-usable state
+ if (reuse)
+ managedConn.markReusable();
+ releaseConnection();
+ } else {
+ // install an auto-release entity
+ HttpEntity entity = response.getEntity();
+ entity = new BasicManagedEntity(entity, managedConn, reuse);
+ response.setEntity(entity);
+ }
+
+ return response;
+
+ } catch (ConnectionShutdownException ex) {
+ InterruptedIOException ioex = new InterruptedIOException(
+ "Connection has been shut down");
+ ioex.initCause(ex);
+ throw ioex;
+ } catch (HttpException ex) {
+ abortConnection();
+ throw ex;
+ } catch (IOException ex) {
+ abortConnection();
+ throw ex;
+ } catch (RuntimeException ex) {
+ abortConnection();
+ throw ex;
+ }
+ } // execute
+
+ /**
+ * Establish connection either directly or through a tunnel and retry in case of
+ * a recoverable I/O failure
+ */
+ private void tryConnect(
+ final RoutedRequest req, final HttpContext context) throws HttpException, IOException {
+ HttpRoute route = req.getRoute();
+
+ int connectCount = 0;
+ for (;;) {
+ // Increment connect count
+ connectCount++;
+ try {
+ if (!managedConn.isOpen()) {
+ managedConn.open(route, context, params);
+ } else {
+ managedConn.setSocketTimeout(HttpConnectionParams.getSoTimeout(params));
+ }
+ establishRoute(route, context);
+ break;
+ } catch (IOException ex) {
+ try {
+ managedConn.close();
+ } catch (IOException ignore) {
+ }
+ if (retryHandler.retryRequest(ex, connectCount, context)) {
+ if (this.log.isInfoEnabled()) {
+ this.log.info("I/O exception ("+ ex.getClass().getName() +
+ ") caught when connecting to the target host: "
+ + ex.getMessage());
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ this.log.info("Retrying connect");
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+
+ /**
+ * Execute request and retry in case of a recoverable I/O failure
+ */
+ private HttpResponse tryExecute(
+ final RoutedRequest req, final HttpContext context) throws HttpException, IOException {
+ RequestWrapper wrapper = req.getRequest();
+ HttpRoute route = req.getRoute();
+ HttpResponse response = null;
+
+ Exception retryReason = null;
+ for (;;) {
+ // Increment total exec count (with redirects)
+ execCount++;
+ // Increment exec count for this particular request
+ wrapper.incrementExecCount();
+ if (!wrapper.isRepeatable()) {
+ this.log.debug("Cannot retry non-repeatable request");
+ if (retryReason != null) {
+ throw new NonRepeatableRequestException("Cannot retry request " +
+ "with a non-repeatable request entity. The cause lists the " +
+ "reason the original request failed.", retryReason);
+ } else {
+ throw new NonRepeatableRequestException("Cannot retry request " +
+ "with a non-repeatable request entity.");
+ }
+ }
+
+ try {
+ if (!managedConn.isOpen()) {
+ // If we have a direct route to the target host
+ // just re-open connection and re-try the request
+ if (!route.isTunnelled()) {
+ this.log.debug("Reopening the direct connection.");
+ managedConn.open(route, context, params);
+ } else {
+ // otherwise give up
+ this.log.debug("Proxied connection. Need to start over.");
+ break;
+ }
+ }
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Attempt " + execCount + " to execute request");
+ }
+ response = requestExec.execute(wrapper, managedConn, context);
+ break;
+
+ } catch (IOException ex) {
+ this.log.debug("Closing the connection.");
+ try {
+ managedConn.close();
+ } catch (IOException ignore) {
+ }
+ if (retryHandler.retryRequest(ex, wrapper.getExecCount(), context)) {
+ if (this.log.isInfoEnabled()) {
+ this.log.info("I/O exception ("+ ex.getClass().getName() +
+ ") caught when processing request: "
+ + ex.getMessage());
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ this.log.info("Retrying request");
+ retryReason = ex;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Returns the connection back to the connection manager
+ * and prepares for retrieving a new connection during
+ * the next request.
+ */
+ protected void releaseConnection() {
+ // Release the connection through the ManagedConnection instead of the
+ // ConnectionManager directly. This lets the connection control how
+ // it is released.
+ try {
+ managedConn.releaseConnection();
+ } catch(IOException ignored) {
+ this.log.debug("IOException releasing connection", ignored);
+ }
+ managedConn = null;
+ }
+
+ /**
+ * Determines the route for a request.
+ * Called by {@link #execute}
+ * to determine the route for either the original or a followup request.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept null
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param context the context to use for the execution,
+ * never null
+ *
+ * @return the route the request should take
+ *
+ * @throws HttpException in case of a problem
+ */
+ protected HttpRoute determineRoute(HttpHost target,
+ HttpRequest request,
+ HttpContext context)
+ throws HttpException {
+
+ if (target == null) {
+ target = (HttpHost) request.getParams().getParameter(
+ ClientPNames.DEFAULT_HOST);
+ }
+ if (target == null) {
+ throw new IllegalStateException
+ ("Target host must not be null, or set in parameters.");
+ }
+
+ return this.routePlanner.determineRoute(target, request, context);
+ }
+
+
+ /**
+ * Establishes the target route.
+ *
+ * @param route the route to establish
+ * @param context the context for the request execution
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected void establishRoute(HttpRoute route, HttpContext context)
+ throws HttpException, IOException {
+
+ HttpRouteDirector rowdy = new BasicRouteDirector();
+ int step;
+ do {
+ HttpRoute fact = managedConn.getRoute();
+ step = rowdy.nextStep(route, fact);
+
+ switch (step) {
+
+ case HttpRouteDirector.CONNECT_TARGET:
+ case HttpRouteDirector.CONNECT_PROXY:
+ managedConn.open(route, context, this.params);
+ break;
+
+ case HttpRouteDirector.TUNNEL_TARGET: {
+ boolean secure = createTunnelToTarget(route, context);
+ this.log.debug("Tunnel to target created.");
+ managedConn.tunnelTarget(secure, this.params);
+ } break;
+
+ case HttpRouteDirector.TUNNEL_PROXY: {
+ // The most simple example for this case is a proxy chain
+ // of two proxies, where P1 must be tunnelled to P2.
+ // route: Source -> P1 -> P2 -> Target (3 hops)
+ // fact: Source -> P1 -> Target (2 hops)
+ final int hop = fact.getHopCount()-1; // the hop to establish
+ boolean secure = createTunnelToProxy(route, hop, context);
+ this.log.debug("Tunnel to proxy created.");
+ managedConn.tunnelProxy(route.getHopTarget(hop),
+ secure, this.params);
+ } break;
+
+
+ case HttpRouteDirector.LAYER_PROTOCOL:
+ managedConn.layerProtocol(context, this.params);
+ break;
+
+ case HttpRouteDirector.UNREACHABLE:
+ throw new HttpException("Unable to establish route: " +
+ "planned = " + route + "; current = " + fact);
+ case HttpRouteDirector.COMPLETE:
+ // do nothing
+ break;
+ default:
+ throw new IllegalStateException("Unknown step indicator "
+ + step + " from RouteDirector.");
+ }
+
+ } while (step > HttpRouteDirector.COMPLETE);
+
+ } // establishConnection
+
+
+ /**
+ * Creates a tunnel to the target server.
+ * The connection must be established to the (last) proxy.
+ * A CONNECT request for tunnelling through the proxy will
+ * be created and sent, the response received and checked.
+ * This method does not update the connection with
+ * information about the tunnel, that is left to the caller.
+ *
+ * @param route the route to establish
+ * @param context the context for request execution
+ *
+ * @return true
if the tunnelled route is secure,
+ * false
otherwise.
+ * The implementation here always returns false
,
+ * but derived classes may override.
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected boolean createTunnelToTarget(HttpRoute route,
+ HttpContext context)
+ throws HttpException, IOException {
+
+ HttpHost proxy = route.getProxyHost();
+ HttpHost target = route.getTargetHost();
+ HttpResponse response = null;
+
+ boolean done = false;
+ while (!done) {
+
+ done = true;
+
+ if (!this.managedConn.isOpen()) {
+ this.managedConn.open(route, context, this.params);
+ }
+
+ HttpRequest connect = createConnectRequest(route, context);
+ connect.setParams(this.params);
+
+ // Populate the execution context
+ context.setAttribute(ExecutionContext.HTTP_TARGET_HOST,
+ target);
+ context.setAttribute(ExecutionContext.HTTP_PROXY_HOST,
+ proxy);
+ context.setAttribute(ExecutionContext.HTTP_CONNECTION,
+ managedConn);
+ context.setAttribute(ClientContext.TARGET_AUTH_STATE,
+ targetAuthState);
+ context.setAttribute(ClientContext.PROXY_AUTH_STATE,
+ proxyAuthState);
+ context.setAttribute(ExecutionContext.HTTP_REQUEST,
+ connect);
+
+ this.requestExec.preProcess(connect, this.httpProcessor, context);
+
+ response = this.requestExec.execute(connect, this.managedConn, context);
+
+ response.setParams(this.params);
+ this.requestExec.postProcess(response, this.httpProcessor, context);
+
+ int status = response.getStatusLine().getStatusCode();
+ if (status < 200) {
+ throw new HttpException("Unexpected response to CONNECT request: " +
+ response.getStatusLine());
+ }
+
+ CredentialsProvider credsProvider = (CredentialsProvider)
+ context.getAttribute(ClientContext.CREDS_PROVIDER);
+
+ if (credsProvider != null && HttpClientParams.isAuthenticating(params)) {
+ if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) {
+
+ this.log.debug("Proxy requested authentication");
+ Map challenges = this.proxyAuthHandler.getChallenges(
+ response, context);
+ try {
+ processChallenges(
+ challenges, this.proxyAuthState, this.proxyAuthHandler,
+ response, context);
+ } catch (AuthenticationException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Authentication error: " + ex.getMessage());
+ break;
+ }
+ }
+ updateAuthState(this.proxyAuthState, proxy, credsProvider);
+
+ if (this.proxyAuthState.getCredentials() != null) {
+ done = false;
+
+ // Retry request
+ if (this.reuseStrategy.keepAlive(response, context)) {
+ this.log.debug("Connection kept alive");
+ // Consume response content
+ HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ } else {
+ this.managedConn.close();
+ }
+
+ }
+
+ } else {
+ // Reset proxy auth scope
+ this.proxyAuthState.setAuthScope(null);
+ }
+ }
+ }
+
+ int status = response.getStatusLine().getStatusCode(); // can't be null
+
+ if (status > 299) {
+
+ // Buffer response content
+ HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ response.setEntity(new BufferedHttpEntity(entity));
+ }
+
+ this.managedConn.close();
+ throw new TunnelRefusedException("CONNECT refused by proxy: " +
+ response.getStatusLine(), response);
+ }
+
+ this.managedConn.markReusable();
+
+ // How to decide on security of the tunnelled connection?
+ // The socket factory knows only about the segment to the proxy.
+ // Even if that is secure, the hop to the target may be insecure.
+ // Leave it to derived classes, consider insecure by default here.
+ return false;
+
+ } // createTunnelToTarget
+
+
+
+ /**
+ * Creates a tunnel to an intermediate proxy.
+ * This method is not implemented in this class.
+ * It just throws an exception here.
+ *
+ * @param route the route to establish
+ * @param hop the hop in the route to establish now.
+ * route.getHopTarget(hop)
+ * will return the proxy to tunnel to.
+ * @param context the context for request execution
+ *
+ * @return true
if the partially tunnelled connection
+ * is secure, false
otherwise.
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected boolean createTunnelToProxy(HttpRoute route, int hop,
+ HttpContext context)
+ throws HttpException, IOException {
+
+ // Have a look at createTunnelToTarget and replicate the parts
+ // you need in a custom derived class. If your proxies don't require
+ // authentication, it is not too hard. But for the stock version of
+ // HttpClient, we cannot make such simplifying assumptions and would
+ // have to include proxy authentication code. The HttpComponents team
+ // is currently not in a position to support rarely used code of this
+ // complexity. Feel free to submit patches that refactor the code in
+ // createTunnelToTarget to facilitate re-use for proxy tunnelling.
+
+ throw new HttpException("Proxy chains are not supported.");
+ }
+
+
+
+ /**
+ * Creates the CONNECT request for tunnelling.
+ * Called by {@link #createTunnelToTarget createTunnelToTarget}.
+ *
+ * @param route the route to establish
+ * @param context the context for request execution
+ *
+ * @return the CONNECT request for tunnelling
+ */
+ protected HttpRequest createConnectRequest(HttpRoute route,
+ HttpContext context) {
+ // see RFC 2817, section 5.2 and
+ // INTERNET-DRAFT: Tunneling TCP based protocols through
+ // Web proxy servers
+
+ HttpHost target = route.getTargetHost();
+
+ String host = target.getHostName();
+ int port = target.getPort();
+ if (port < 0) {
+ Scheme scheme = connManager.getSchemeRegistry().
+ getScheme(target.getSchemeName());
+ port = scheme.getDefaultPort();
+ }
+
+ StringBuilder buffer = new StringBuilder(host.length() + 6);
+ buffer.append(host);
+ buffer.append(':');
+ buffer.append(Integer.toString(port));
+
+ String authority = buffer.toString();
+ ProtocolVersion ver = HttpProtocolParams.getVersion(params);
+ HttpRequest req = new BasicHttpRequest
+ ("CONNECT", authority, ver);
+
+ return req;
+ }
+
+
+ /**
+ * Analyzes a response to check need for a followup.
+ *
+ * @param roureq the request and route.
+ * @param response the response to analayze
+ * @param context the context used for the current request execution
+ *
+ * @return the followup request and route if there is a followup, or
+ * null
if the response should be returned as is
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected RoutedRequest handleResponse(RoutedRequest roureq,
+ HttpResponse response,
+ HttpContext context)
+ throws HttpException, IOException {
+
+ HttpRoute route = roureq.getRoute();
+ RequestWrapper request = roureq.getRequest();
+
+ HttpParams params = request.getParams();
+ if (HttpClientParams.isRedirecting(params) &&
+ this.redirectStrategy.isRedirected(request, response, context)) {
+
+ if (redirectCount >= maxRedirects) {
+ throw new RedirectException("Maximum redirects ("
+ + maxRedirects + ") exceeded");
+ }
+ redirectCount++;
+
+ // Virtual host cannot be used any longer
+ virtualHost = null;
+
+ HttpUriRequest redirect = redirectStrategy.getRedirect(request, response, context);
+ HttpRequest orig = request.getOriginal();
+ redirect.setHeaders(orig.getAllHeaders());
+
+ URI uri = redirect.getURI();
+ if (uri.getHost() == null) {
+ throw new ProtocolException("Redirect URI does not specify a valid host name: " + uri);
+ }
+
+ HttpHost newTarget = new HttpHost(
+ uri.getHost(),
+ uri.getPort(),
+ uri.getScheme());
+
+ // Unset auth scope
+ targetAuthState.setAuthScope(null);
+ proxyAuthState.setAuthScope(null);
+
+ // Invalidate auth states if redirecting to another host
+ if (!route.getTargetHost().equals(newTarget)) {
+ targetAuthState.invalidate();
+ AuthScheme authScheme = proxyAuthState.getAuthScheme();
+ if (authScheme != null && authScheme.isConnectionBased()) {
+ proxyAuthState.invalidate();
+ }
+ }
+
+ RequestWrapper wrapper = wrapRequest(redirect);
+ wrapper.setParams(params);
+
+ HttpRoute newRoute = determineRoute(newTarget, wrapper, context);
+ RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute);
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Redirecting to '" + uri + "' via " + newRoute);
+ }
+
+ return newRequest;
+ }
+
+ CredentialsProvider credsProvider = (CredentialsProvider)
+ context.getAttribute(ClientContext.CREDS_PROVIDER);
+
+ if (credsProvider != null && HttpClientParams.isAuthenticating(params)) {
+
+ if (this.targetAuthHandler.isAuthenticationRequested(response, context)) {
+
+ HttpHost target = (HttpHost)
+ context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
+ if (target == null) {
+ target = route.getTargetHost();
+ }
+
+ this.log.debug("Target requested authentication");
+ Map challenges = this.targetAuthHandler.getChallenges(
+ response, context);
+ try {
+ processChallenges(challenges,
+ this.targetAuthState, this.targetAuthHandler,
+ response, context);
+ } catch (AuthenticationException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Authentication error: " + ex.getMessage());
+ return null;
+ }
+ }
+ updateAuthState(this.targetAuthState, target, credsProvider);
+
+ if (this.targetAuthState.getCredentials() != null) {
+ // Re-try the same request via the same route
+ return roureq;
+ } else {
+ return null;
+ }
+ } else {
+ // Reset target auth scope
+ this.targetAuthState.setAuthScope(null);
+ }
+
+ if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) {
+
+ HttpHost proxy = route.getProxyHost();
+
+ this.log.debug("Proxy requested authentication");
+ Map challenges = this.proxyAuthHandler.getChallenges(
+ response, context);
+ try {
+ processChallenges(challenges,
+ this.proxyAuthState, this.proxyAuthHandler,
+ response, context);
+ } catch (AuthenticationException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Authentication error: " + ex.getMessage());
+ return null;
+ }
+ }
+ updateAuthState(this.proxyAuthState, proxy, credsProvider);
+
+ if (this.proxyAuthState.getCredentials() != null) {
+ // Re-try the same request via the same route
+ return roureq;
+ } else {
+ return null;
+ }
+ } else {
+ // Reset proxy auth scope
+ this.proxyAuthState.setAuthScope(null);
+ }
+ }
+ return null;
+ } // handleResponse
+
+
+ /**
+ * Shuts down the connection.
+ * This method is called from a catch
block in
+ * {@link #execute execute} during exception handling.
+ */
+ private void abortConnection() {
+ ManagedClientConnection mcc = managedConn;
+ if (mcc != null) {
+ // we got here as the result of an exception
+ // no response will be returned, release the connection
+ managedConn = null;
+ try {
+ mcc.abortConnection();
+ } catch (IOException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ }
+ // ensure the connection manager properly releases this connection
+ try {
+ mcc.releaseConnection();
+ } catch(IOException ignored) {
+ this.log.debug("Error releasing connection", ignored);
+ }
+ }
+ } // abortConnection
+
+
+ private void processChallenges(
+ final Map challenges,
+ final AuthState authState,
+ final AuthenticationHandler authHandler,
+ final HttpResponse response,
+ final HttpContext context)
+ throws MalformedChallengeException, AuthenticationException {
+
+ AuthScheme authScheme = authState.getAuthScheme();
+ if (authScheme == null) {
+ // Authentication not attempted before
+ authScheme = authHandler.selectScheme(challenges, response, context);
+ authState.setAuthScheme(authScheme);
+ }
+ String id = authScheme.getSchemeName();
+
+ Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH));
+ if (challenge == null) {
+ throw new AuthenticationException(id +
+ " authorization challenge expected, but not found");
+ }
+ authScheme.processChallenge(challenge);
+ this.log.debug("Authorization challenge processed");
+ }
+
+
+ private void updateAuthState(
+ final AuthState authState,
+ final HttpHost host,
+ final CredentialsProvider credsProvider) {
+
+ if (!authState.isValid()) {
+ return;
+ }
+
+ String hostname = host.getHostName();
+ int port = host.getPort();
+ if (port < 0) {
+ Scheme scheme = connManager.getSchemeRegistry().getScheme(host);
+ port = scheme.getDefaultPort();
+ }
+
+ AuthScheme authScheme = authState.getAuthScheme();
+ AuthScope authScope = new AuthScope(
+ hostname,
+ port,
+ authScheme.getRealm(),
+ authScheme.getSchemeName());
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Authentication scope: " + authScope);
+ }
+ Credentials creds = authState.getCredentials();
+ if (creds == null) {
+ creds = credsProvider.getCredentials(authScope);
+ if (this.log.isDebugEnabled()) {
+ if (creds != null) {
+ this.log.debug("Found credentials");
+ } else {
+ this.log.debug("Credentials not found");
+ }
+ }
+ } else {
+ if (authScheme.isComplete()) {
+ this.log.debug("Authentication failed");
+ creds = null;
+ }
+ }
+ authState.setAuthScope(authScope);
+ authState.setCredentials(creds);
+ }
+
+ private void invalidateAuthIfSuccessful(final AuthState authState) {
+ AuthScheme authscheme = authState.getAuthScheme();
+ if (authscheme != null
+ && authscheme.isConnectionBased()
+ && authscheme.isComplete()
+ && authState.getCredentials() != null) {
+ authState.invalidate();
+ }
+ }
+
+} // class DefaultClientRequestDirector