/*
 * $Id: HTTPServiceCaller.java,v 1.141 2013/01/23 11:36:37 agoubard Exp $
 *
 * See the COPYRIGHT file for redistribution and use restrictions.
 */
package org.xins.common.http;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.HttpEntity;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;

import org.apache.log4j.NDC;

import org.xins.common.FormattedParameters;
import org.xins.common.Log;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.TimeOutException;
import org.xins.common.Utils;
import org.xins.common.service.CallConfig;
import org.xins.common.service.CallException;
import org.xins.common.service.CallExceptionList;
import org.xins.common.service.CallRequest;
import org.xins.common.service.CallResult;
import org.xins.common.service.ConnectionRefusedCallException;
import org.xins.common.service.ConnectionTimeOutCallException;
import org.xins.common.service.Descriptor;
import org.xins.common.service.GenericCallException;
import org.xins.common.service.IOCallException;
import org.xins.common.service.NoRouteToHostCallException;
import org.xins.common.service.ServiceCaller;
import org.xins.common.service.SocketTimeOutCallException;
import org.xins.common.service.TargetDescriptor;
import org.xins.common.service.TotalTimeOutCallException;
import org.xins.common.service.UnexpectedExceptionCallException;
import org.xins.common.service.UnknownHostCallException;
import org.xins.common.service.UnsupportedProtocolException;
import org.xins.common.text.TextUtils;
import org.xins.common.text.URLEncoding;

/**
 * HTTP service caller. This class can be used to perform a call to an HTTP
 * server and fail-over to other HTTP servers if the first one fails.
 *
 *
 * <h2>Supported protocols</h2>
 *
 * <p>This service caller supports both HTTP and HTTPS (which is HTTP
 * tunneled over a secure SSL connection). If a {@link TargetDescriptor} is
 * passed to the constructor with a protocol other than <code>"http"</code>
 * or <code>"https"</code>, then an {@link UnsupportedProtocolException} is
 * thrown.
 *
 * <h2>Load-balancing and fail-over</h2>
 *
 * <p>To perform an HTTP call using a
 * <code>HTTPServiceCaller</code> use {@link #call(HTTPCallRequest)}.
 *
 * <p>How load-balancing is done depends on the {@link Descriptor} passed to
 * the {@link #HTTPServiceCaller(Descriptor)} constructor:
 *
 * <ul>
 *    <li>if it is a {@link TargetDescriptor}, then only this single target
 *        service is called and no load-balancing is performed;
 *    <li>if it is a {@link org.xins.common.service.GroupDescriptor}, then the
 *        configuration of the <code>GroupDescriptor</code> determines how the
 *        load-balancing is done.
 * </ul>
 *
 * <p>A <code>GroupDescriptor</code> is a recursive data structure, which allows
 * for fairly advanced load-balancing algorithms.
 *
 * <p>If a call attempt fails and there are more available target services,
 * then the <code>HTTPServiceCaller</code> may or may not fail-over to a next
 * target. If the request was not accepted by the target service, then
 * fail-over is considered acceptable and will be performed. This includes
 * the following situations:
 *
 * <ul>
 *    <li>if the <em>failOverAllowed</em> property is set to <code>true</code>
 *        for the active {@link HTTPCallConfig};
 *    <li>if the connection cannot be established (e.g. due to connection
 *        refusal, a DNS error, connection time-out, etc.)
 *    <li>if an HTTP status code other than 200-299 is returned;
 * </ul>
 *
 * <p>If none of these conditions holds, then fail-over is not considered
 * acceptable and will not be performed.
 *
 * <h2>Thread-safety</h2>
 *
 * <p>Instances of this class can safely be used from multiple threads at the
 * same time.
 *
 *
 * <h2>Example code</h2>
 *
 * <p>The following example code snippet constructs an
 * <code>HTTPServiceCaller</code> instance:
 *
 * <blockquote><pre>// Initialize properties for the services. Normally these
// properties would come from a configuration source, like a file.
{@link java.util.Map} properties = new {@link java.util.HashMap#HashMap() HashMap}();
properties.{@link java.util.Map#put(Object,Object) put}("myapi",         "group, random, server1, server2");
properties.{@link java.util.Map#put(Object,Object) put}("myapi.server1", "service, http://server1/myapi, 10000");
properties.{@link java.util.Map#put(Object,Object) put}("myapi.server2", "service, http://server2/myapi, 12000");

// Construct a descriptor and an HTTPServiceCaller instance
{@link Descriptor Descriptor} descriptor = {@link org.xins.common.service.DescriptorBuilder DescriptorBuilder}.{@link org.xins.common.service.DescriptorBuilder#build(Map,String) build}(properties, "myapi");
HTTPServiceCaller caller = new {@link #HTTPServiceCaller(Descriptor) HTTPServiceCaller}(descriptor);</pre></blockquote>
 *
 * <p>Then the following code snippet uses this <code>HTTPServiceCaller</code>
 * to perform an HTTP GET call:
 *
 * <blockquote><pre>{@link java.util.Map} params = new {@link java.util.HashMap HashMap}();
params.{@link java.util.Map#put(Object,Object) put}("street",      "Broadband Avenue");
params.{@link java.util.Map#put(Object,Object) put}("houseNumber", "12");

{@link HTTPCallRequest} request = new {@link HTTPCallRequest#HTTPCallRequest(HTTPMethod,Map) HTTPCallRequest}({@link HTTPMethod}.{@link HTTPMethod#GET GET}, params);
{@link HTTPCallResult} result = caller.{@link #call(HTTPCallRequest) call}(request);</pre></blockquote>
 *
 * @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
 * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
 * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
 *
 * @since XINS 1.0.0
 */
public class HTTPServiceCaller extends ServiceCaller {

   /**
    * Charset for the error messages.
    */
   private static final Charset UTF_CHARSET = Charset.forName("UTF-8");

   /**
    * Fully-qualified name of this class.
    */
   private static final String CLASSNAME = HTTPServiceCaller.class.getName();

   /**
    * Fully-qualified name of the inner class <code>CallExecutor</code>.
    */
   private static final String EXECUTOR_CLASSNAME = HTTPServiceCaller.CallExecutor.class.getName();

   /**
    * The number of constructed call executors.
    */
   private static AtomicInteger CALL_EXECUTOR_COUNT = new AtomicInteger();

   /**
    * HTTP retry handler that does not allow any retries.
    */
   private static HttpRequestRetryHandler NO_RETRIES = new DefaultHttpRequestRetryHandler(0, false);

   /**
    * Constructs a new <code>HTTPServiceCaller</code> object with the
    * specified descriptor and call configuration.
    *
    * @param descriptor
    *    the descriptor of the service, cannot be <code>null</code>.
    *
    * @param callConfig
    *    the call configuration, or <code>null</code> if a default one should
    *    be used.
    *
    * @throws IllegalArgumentException
    *    if <code>descriptor == null</code>.
    *
    * @throws UnsupportedProtocolException
    *    if <code>descriptor</code> is or contains a {@link TargetDescriptor}
    *    with an unsupported protocol.
    *
    * @since XINS 1.1.0
    */
   public HTTPServiceCaller(Descriptor     descriptor,
                            HTTPCallConfig callConfig)
   throws IllegalArgumentException, UnsupportedProtocolException {

      // Call superclass constructor
      super(descriptor, callConfig);
   }


   /**
    * Constructs a new <code>HTTPServiceCaller</code> object.
    *
    * @param descriptor
    *    the descriptor of the service, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>descriptor == null</code>.
    *
    * @throws UnsupportedProtocolException
    *    if <code>descriptor</code> is or contains a {@link TargetDescriptor}
    *    with an unsupported protocol (<em>since XINS 1.1.0</em>).
    */
   public HTTPServiceCaller(Descriptor descriptor)
   throws IllegalArgumentException, UnsupportedProtocolException {
      this(descriptor, null);
   }

   /**
    * Returns the {@link HttpClient} to use to contact the given target.
    *
    * @param config
    *    the HTTP configuration of the service.
    *
    * @param target
    *    the target of the service.
    *
    * @return
    *    the HttpClient shared instance.
    */
   private static HttpClient getHttpClient(HTTPCallConfig config, TargetDescriptor target) {

      HttpClient httpClient = config.getHttpClient();
      HttpParams httpParams = httpClient.getParams();

      int connectionTimeOut = target.getConnectionTimeOut();
      int socketTimeOut     = target.getSocketTimeOut();

      // Configure connection time-out and socket time-out
      httpParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, connectionTimeOut);
      httpParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, socketTimeOut);

      // Redirection handling
      httpParams.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, config.getFollowRedirect());

      // Retry stategy if not already set
      if (httpClient instanceof AbstractHttpClient
              && ((AbstractHttpClient) httpClient).getHttpRequestRetryHandler() == null) {
         ((AbstractHttpClient) httpClient).setHttpRequestRetryHandler(NO_RETRIES);
      }

      return httpClient;
   }

   /**
    * Creates an appropriate <code>HttpRequestBase</code> object for the
    * specified URL.
    *
    * @param url
    *    the URL for which to create an {@link HttpRequestBase} object, should
    *    not be <code>null</code>.
    *
    * @param request
    *    the HTTP call request, not <code>null</code>.
    *
    * @param callConfig
    *    the HTTP call configuration object, not <code>null</code>.
    *
    * @return
    *    the constructed {@link HttpRequestBase} object, not <code>null</code>.
    */
   private static HttpRequestBase createMethod(String          url,
                                               HTTPCallRequest request,
                                               HTTPCallConfig  callConfig) {

      // Get the HTTP method (like GET and POST) and parameters
      HTTPMethod method = callConfig.getMethod();
      Map<String, String> parameters = request.getParameters();


      // HTTP POST, PUT requests
      if (method == HTTPMethod.POST || method == HTTPMethod.PUT) {
         List<BasicNameValuePair> httpParameters = new ArrayList<BasicNameValuePair>();

         // Loop through the parameters
         for (Map.Entry<String, String> param : parameters.entrySet()) {
            httpParameters.add(new BasicNameValuePair(param.getKey(), param.getValue()));
         }

         HttpEntityEnclosingRequestBase httpMethod = null;
         if (method == HTTPMethod.POST) {
            httpMethod = new HttpPost(url);
         } else {
            httpMethod = new HttpPut(url);
         }
         try {
            httpMethod.setEntity(new UrlEncodedFormEntity(httpParameters, "UTF-8"));
         } catch (UnsupportedEncodingException ex) {
            throw new IllegalStateException("UTF-8 not supported on this platform: " + ex.getMessage());
         }

         return httpMethod;

      // HTTP GET, DELETE, HEAD, OPTIONS, TRACE requests
      } else {

         // Loop through the parameters
         StringBuffer query = new StringBuffer(255);
         for (Map.Entry<String, String> param : parameters.entrySet()) {
            String key = param.getKey();
            String value = param.getValue();
            if (value == null) {
               value = "";
            }

            // Add this parameter key/value combination.
            if (key != null) {

               if (query.length() > 0) {
                  query.append("&");
               }
               query.append(URLEncoding.encode(key));
               query.append("=");
               query.append(URLEncoding.encode(value));
            }
         }
         if (query.length() > 0) {
            url += "?" + query;
         }
         HttpRequestBase httpMethod = null;
         if (method == HTTPMethod.GET) {
            httpMethod = new HttpGet(url);
         } else if (method == HTTPMethod.DELETE) {
            httpMethod = new HttpDelete(url);
         } else if (method == HTTPMethod.HEAD) {
            httpMethod = new HttpHead(url);
         } else if (method == HTTPMethod.OPTIONS) {
            httpMethod = new HttpOptions(url);
         } else if (method == HTTPMethod.TRACE) {
            httpMethod = new HttpTrace(url);
         }

         return httpMethod;
      }
   }

   /**
    * Checks if the specified protocol is supported (implementation method).
    * The protocol is the part in a URL before the string <code>"://"</code>).
    *
    * <p>This method should only ever be called from the
    * {@link #isProtocolSupported(String)} method.
    *
    * <p>The implementation of this method in class
    * <code>HTTPServiceCaller</code> throws an
    * {@link UnsupportedOperationException} unless the protocol equals either
    * <code>"http"</code> or <code>"https</code> (ignoring case)..
    *
    * @param protocol
    *    the protocol, guaranteed not to be <code>null</code>.
    *
    * @return
    *    <code>true</code> if the specified protocol is supported, or
    *    <code>false</code> if it is not.
    *
    * @since XINS 1.2.0
    */
   protected boolean isProtocolSupportedImpl(String protocol) {
      return "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol);
   }

   /**
    * Returns a default <code>CallConfig</code> object. This method is called
    * by the <code>ServiceCaller</code> constructor if no
    * <code>CallConfig</code> object was given.
    *
    * <p>The implementation of this method in class {@link HTTPServiceCaller}
    * returns a standard {@link HTTPCallConfig} object which has unconditional
    * fail-over disabled and the HTTP method set to
    * {@link HTTPMethod#POST POST}.
    *
    * @return
    *    a new {@link HTTPCallConfig} instance, never <code>null</code>.
    */
   protected CallConfig getDefaultCallConfig() {
      return new HTTPCallConfig();
   }

   /**
    * Sets the <code>HTTPCallConfig</code> associated with this HTTP service
    * caller.
    *
    * @param config
    *    the fall-back {@link HTTPCallConfig} object for this service caller,
    *    cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>config == null</code>.
    *
    * @since XINS 1.2.0
    */
   protected void setHTTPCallConfig(HTTPCallConfig config)
   throws IllegalArgumentException {
      super.setCallConfig(config);
   }

   /**
    * Returns the <code>HTTPCallConfig</code> associated with this service
    * caller.
    *
    * <p>This method is the type-safe equivalent of {@link #getCallConfig()}.
    *
    * @return
    *    the fall-back {@link HTTPCallConfig} object for this HTTP service
    *    caller, never <code>null</code>.
    *
    * @since XINS 1.2.0
    */
   public HTTPCallConfig getHTTPCallConfig() {
      return (HTTPCallConfig) getCallConfig();
   }

   /**
    * Executes a request towards the specified target. If the call succeeds,
    * then a {@link HTTPCallResult} object is returned, otherwise a
    * {@link CallException} is thrown.
    *
    * @param request
    *    the call request to be executed, must be an instance of class
    *    {@link HTTPCallRequest}, cannot be <code>null</code>.
    *
    * @param callConfig
    *    the call configuration, never <code>null</code> and should always be
    *    an instance of class {@link HTTPCallConfig}.
    *
    * @param target
    *    the target to call, cannot be <code>null</code>.
    *
    * @return
    *    the result, if and only if the call succeeded, always an instance of
    *    class {@link HTTPCallResult}, never <code>null</code>.
    *
    * @throws ClassCastException
    *    if the specified <code>request</code> object is not <code>null</code>
    *    and not an instance of class {@link HTTPCallRequest}.
    *
    * @throws IllegalArgumentException
    *    if <code>target == null || request == null</code>.
    *
    * @throws CallException
    *    if the call to the specified target failed.
    *
    * @since XINS 1.1.0
    */
   public Object doCallImpl(CallRequest      request,
                               CallConfig       callConfig,
                               TargetDescriptor target)
   throws ClassCastException, IllegalArgumentException, CallException {

      // Delegate to method with more specialized interface
      Object ret = call((HTTPCallRequest) request,
                        (HTTPCallConfig)  callConfig,
                        target);

      return ret;
   }

   /**
    * Performs the specified request towards the HTTP service. If the call
    * succeeds with one of the targets, then a {@link HTTPCallResult} object
    * is returned, that combines the HTTP status code and the data returned.
    * Otherwise, if none of the targets could successfully be called, a
    * {@link CallException} is thrown.
    *
    * @param request
    *    the call request, not <code>null</code>.
    *
    * @param callConfig
    *    the call configuration to use, or <code>null</code>.
    *
    * @return
    *    the result of the call, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>request == null</code>.
    *
    * @throws GenericCallException
    *    if the first call attempt failed due to a generic reason and all the
    *    other call attempts failed as well.
    *
    * @throws HTTPCallException
    *    if the first call attempt failed due to an HTTP-related reason and
    *    all the other call attempts failed as well.
    *
    * @since XINS 1.1.0
    */
   public HTTPCallResult call(HTTPCallRequest request,
                              HTTPCallConfig  callConfig)
   throws IllegalArgumentException, GenericCallException, HTTPCallException {

      // Check preconditions
      MandatoryArgumentChecker.check("request", request);

      // Perform the call
      CallResult callResult;
      try {
         callResult = doCall(request, callConfig);

      // Allow GenericCallException, HTTPCallException and Error to proceed,
      // but block other kinds of exceptions and throw an Error instead.
      } catch (GenericCallException exception) {
         throw exception;
      } catch (HTTPCallException exception) {
         throw exception;
      } catch (Exception exception) {
         throw Utils.logProgrammingError(exception);
      }

      return (HTTPCallResult) callResult;
   }

   /**
    * Performs the specified request towards the HTTP service. If the call
    * succeeds with one of the targets, then a {@link HTTPCallResult} object
    * is returned, that combines the HTTP status code and the data returned.
    * Otherwise, if none of the targets could successfully be called, a
    * {@link CallException} is thrown.
    *
    * @param request
    *    the call request, not <code>null</code>.
    *
    * @return
    *    the result of the call, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>request == null</code>.
    *
    * @throws GenericCallException
    *    if the first call attempt failed due to a generic reason and all the
    *    other call attempts failed as well.
    *
    * @throws HTTPCallException
    *    if the first call attempt failed due to an HTTP-related reason and
    *    all the other call attempts failed as well.
    */
   public HTTPCallResult call(HTTPCallRequest request)
   throws IllegalArgumentException,
          GenericCallException,
          HTTPCallException {
      return call(request, (HTTPCallConfig) null);
   }

   /**
    * Executes the specified HTTP call request on the specified target with
    * the specified call configuration. If the call fails in any way, then a
    * {@link CallException} is thrown.
    *
    * @param request
    *    the call request to execute, cannot be <code>null</code>.
    *
    * @param callConfig
    *    the (optional) call configuration, or <code>null</code> if it should
    *    be determined at a lower level.
    *
    * @param target
    *    the service target on which to execute the request, cannot be
    *    <code>null</code>.
    *
    * @return
    *    the call result, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>target == null || request == null</code>.
    *
    * @throws GenericCallException
    *    if the first call attempt failed due to a generic reason and all the
    *    other call attempts failed as well.
    *
    * @throws HTTPCallException
    *    if the first call attempt failed due to an HTTP-related reason and
    *    all the other call attempts failed as well.
    *
    * @since XINS 1.1.0
    */
   private HTTPCallResult call(HTTPCallRequest  request,
                              HTTPCallConfig   callConfig,
                              TargetDescriptor target)
   throws IllegalArgumentException,
          GenericCallException,
          HTTPCallException {

      // Get the parameters for logging
      Map<String, String> p = request.getParameters();
      FormattedParameters params = new FormattedParameters(p, null, "", "?", 160);

      // Prepare a thread for execution of the call
      // NOTE: Preconditions are checked by the CallExecutor constructor
      CallExecutor executor = new CallExecutor(request, callConfig, target, NDC.peek());

      // Get URL and time-out values
      String url               = target.getURL();
      int    totalTimeOut      = target.getTotalTimeOut();
      int    connectionTimeOut = target.getConnectionTimeOut();
      int    socketTimeOut     = target.getSocketTimeOut();

      // About to make an HTTP call
      Log.log_1100(url, params);

      // Perform the HTTP call
      long start = System.currentTimeMillis();
      long duration;
      try {
         controlTimeOut(executor, target);
         // 2.1 code: TimeoutController.execute(executor, totalTimeOut);

      // Total time-out exceeded
      } catch (TimeOutException exception) {
         duration = System.currentTimeMillis() - start;
         Log.log_1106(url, params, duration, totalTimeOut);
         executor.dispose();
         throw new TotalTimeOutCallException(request, target, duration);

      }

      // Determine the call duration
      duration = System.currentTimeMillis() - start;

      // Log that the HTTP call is done
      Log.log_1101(url, params, duration);

      // Check for exceptions
      Throwable exception = executor.getException();
      if (exception != null) {

         String exceptionName = exception.getClass().getName();
         // Unknown host
         if (exception instanceof UnknownHostException) {
            Log.log_1102(url, params, duration);
            executor.dispose();
            throw new UnknownHostCallException(request, target, duration);

         // No route to host
         } else if (exception instanceof NoRouteToHostException) {
            Log.log_1110(url, params, duration);
            executor.dispose();
            throw new NoRouteToHostCallException(request, target, duration);

         // Connection refusal
         } else if (exception instanceof ConnectException) {
            Log.log_1103(url, params, duration);
            executor.dispose();
            throw new ConnectionRefusedCallException(request, target, duration);

         // Connection time-out
         } else if (exception instanceof ConnectTimeoutException) {
            Log.log_1104(url, params, duration, connectionTimeOut);
            executor.dispose();
            throw new ConnectionTimeOutCallException(request, target, duration);

         // Socket time-out
         } else if (exception instanceof SocketTimeoutException) {
            Log.log_1105(url, params, duration, socketTimeOut);
            executor.dispose();
            throw new SocketTimeOutCallException(request, target, duration);

         // Unspecific I/O error
         } else if (exception instanceof IOException) {
            Log.log_1109(exception, url, params, duration);
            executor.dispose();
            throw new IOCallException(request, target, duration,
                                      (IOException) exception);

         // Unrecognized kind of exception caught
         } else {
            String thisMethod = "call(HTTPCallREquest, HTTPCallConfig, TargetDescriptor)";
            String subjectClass  = executor.getThrowingClass();
            String subjectMethod = executor.getThrowingMethod();
            Log.log_1052(exception, CLASSNAME, thisMethod, subjectClass, subjectMethod, null);
            executor.dispose();
            throw new UnexpectedExceptionCallException(request, target, duration, null, exception);
         }
      }

      // Retrieve the data returned from the HTTP call
      HTTPCallResultData data = executor.getData();

      // Determine the HTTP status code
      int code = data.getStatusCode();

      HTTPStatusCodeVerifier verifier = request.getStatusCodeVerifier();

      // Status code is considered acceptable
      if (verifier == null || verifier.isAcceptable(code)) {
         Log.log_1107(url, params, duration, code);

      // Status code is considered unacceptable
      } else {
         Log.log_1108(url, params, duration, code);

         // TODO: Pass down body as well. Perhaps just pass down complete
         //       HTTPCallResult object and add getter for the body to the
         //       StatusCodeHTTPCallException class.

         executor.dispose();
         String details = data.getData() == null ? null : new String(data.getData(), UTF_CHARSET);
         throw new StatusCodeHTTPCallException(request, target, duration, code, details);
      }

      executor.dispose();
      return new HTTPCallResult(request, target, duration, null, data);
   }

   /**
    * Constructs an appropriate <code>CallResult</code> object for a
    * successful call attempt. This method is called from
    * {@link #doCall(CallRequest,CallConfig)}.
    *
    * <p>The implementation of this method in class
    * {@link HTTPServiceCaller} expects an {@link HTTPCallRequest} and
    * returns an {@link HTTPCallResult}.
    *
    * @param request
    *    the {@link CallRequest} that was to be executed, never
    *    <code>null</code> when called from {@link #doCall(CallRequest,CallConfig)};
    *    should be an instance of class {@link HTTPCallRequest}.
    *
    * @param succeededTarget
    *    the {@link TargetDescriptor} for the service that was successfully
    *    called, never <code>null</code> when called from
    *    {@link #doCall(CallRequest,CallConfig)}.
    *
    * @param duration
    *    the call duration in milliseconds, must be a non-negative number.
    *
    * @param exceptions
    *    the list of {@link CallException} instances, or <code>null</code> if
    *    there were no call failures.
    *
    * @param result
    *    the result from the call, which is the object returned by
    *    {@link #doCallImpl(CallRequest,CallConfig,TargetDescriptor)},
    *    always an instance of class {@link HTTPCallResult}, never <code>null</code>; .
    *
    * @return
    *    an {@link HTTPCallResult} instance, never <code>null</code>.
    *
    * @throws ClassCastException
    *    if either <code>request</code> or <code>result</code> is not of the
    *    correct class.
    */
   protected CallResult createCallResult(CallRequest         request,
                                         TargetDescriptor    succeededTarget,
                                         long                duration,
                                         List<CallException> exceptions,
                                         Object              result)
   throws ClassCastException {


      return new HTTPCallResult((HTTPCallRequest) request,
                                succeededTarget,
                                duration,
                                exceptions,
                                (HTTPCallResultData) result);
   }

   /**
    * Determines whether a call should fail-over to the next selected target
    * based on a request, call configuration and exception list.
    * This method should only be called from
    * {@link #doCall(CallRequest,CallConfig)}.
    *
    * <p>The implementation of this method in class {@link HTTPServiceCaller}
    * returns <code>true</code> if and only if one of the following conditions
    * holds true:
    *
    * <ul>
    *    <li><code>super.shouldFailOver(request, callConfig, exceptions)</code>
    *    <li><code>exceptions.{@link CallExceptionList#last() last()}
    *        instanceof {@link StatusCodeHTTPCallException}</code> and for
    *        that exception
    *        {@link StatusCodeHTTPCallException#getStatusCode() getStatusCode()} is not in the
    *        range 200-299.
    * </ul>

    *
    * @param request
    *    the request for the call, as passed to {@link #doCall(CallRequest,CallConfig)},
    *    should not be <code>null</code>.
    *
    * @param callConfig
    *    the call config that is currently in use, never <code>null</code>.
    *
    * @param exceptions
    *    the current list of {@link CallException}s; never
    *    <code>null</code>; get the most recent one by calling
    *    <code>exceptions.</code>{@link CallExceptionList#last() last()}.
    *
    * @return
    *    <code>true</code> if the call should fail-over to the next target, or
    *    <code>false</code> if it should not.
    */
   protected boolean shouldFailOver(CallRequest         request,
                                    CallConfig          callConfig,
                                    List<CallException> exceptions) {

      CallException last = exceptions.get(exceptions.size() - 1);

      // Let the superclass do it's job
      if (super.shouldFailOver(request, callConfig, exceptions)) {
         return true;

      // A non-2xx HTTP status code indicates the request was not handled
      } else if (last instanceof StatusCodeHTTPCallException) {
         int code = ((StatusCodeHTTPCallException) last).getStatusCode();
         return (code < 200 || code > 299) && code != 304;

      // Otherwise do not fail over
      } else {
         return false;
      }
   }

   /**
    * Executor of calls to an API.
    *
    * @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
    * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
    */
   private static final class CallExecutor extends Thread {

      /**
       * Constructs a new <code>CallExecutor</code> for the specified call to
       * an HTTP service.
       *
       * <p>A <em>Nested Diagnostic Context identifier</em> (NDC) may be
       * specified, which will be set for the new thread when it is executed.
       * If the NDC is <code>null</code>, then it will be left unchanged. See
       * the {@link NDC} class.
       *
       * @param request
       *    the call request to execute, cannot be <code>null</code>.
       *
       * @param callConfig
       *    the call configuration, never <code>null</code>.
       *
       * @param target
       *    the service target on which to execute the request, cannot be
       *    <code>null</code>.
       *
       * @param context
       *    the <em>Nested Diagnostic Context identifier</em> (NDC), or
       */
      private CallExecutor(HTTPCallRequest  request,
                           HTTPCallConfig   callConfig,
                           TargetDescriptor target,
                           String           context) {

         // Store data for later use in the run() method
         _request    = request;
         _callConfig = callConfig;
         _target     = target;
         _context    = context;

         // Determine the unique ID of this instance
         int instanceID = CALL_EXECUTOR_COUNT.incrementAndGet();

         // Set the name of this thread
         String name = EXECUTOR_CLASSNAME + " #" + instanceID;
         setName(name);
      }

      /**
       * The call request to execute. Never <code>null</code>.
       */
      private HTTPCallRequest _request;

      /**
       * The call configuration. Never <code>null</code>.
       */
      private HTTPCallConfig _callConfig;

      /**
       * The service target on which to execute the request. Never
       * <code>null</code>.
       */
      private TargetDescriptor _target;

      /**
       * The <em>Nested Diagnostic Context identifier</em> (NDC). Is set to
       * <code>null</code> if it should be left unchanged.
       */
      private String _context;

      /**
       * The exception caught while executing the call. If there was no
       * exception, then this field is <code>null</code>.
       */
      private Throwable _exception;

      /**
       * The name of the class that threw the exception. This field is
       * <code>null</code> if the call was performed successfully.
       */
      private String _throwingClass;

      /**
       * The name of the method that threw the exception. This field is
       * <code>null</code> if the call was performed successfully.
       */
      private String _throwingMethod;

      /**
       * The result from the call. The value of this field is
       * <code>null</code> if the call was unsuccessful or if it was not
       * executed yet.
       */
      private HTTPCallResultData _result;

      /**
       * Runs this thread (wrapper method). It will call the HTTP service. If that call was
       * successful, then the result is stored in this object. Otherwise
       * there is an exception, in which case that exception is stored in this
       * object instead.
       */
      public void run() {

         // XXX: Note that performance could be improved by using local
         //      variables for _target and _request

         // Activate the diagnostic context ID
         if (_context != null) {
            NDC.push(_context);
         }

         // Get the HttpClient object
         HttpClient client = getHttpClient(_callConfig, _target);

         // Determine URL and time-outs
         String url = _target.getURL();

         // Construct the method object
         HttpRequestBase method = createMethod(url, _request, _callConfig);

         // Set the user agent, if specified.
         String userAgent = _callConfig.getUserAgent();
         if (! TextUtils.isEmpty(userAgent)) {
            client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, userAgent);
         }

         // Perform the HTTP call
         try {
            // Execute call
            _throwingClass  = client.getClass().getName();
            _throwingMethod = "execute(" + method.getClass().getName() + ')';
            HttpResponse response = client.execute(method);
            int statusCode = response.getStatusLine().getStatusCode();

            // Get response body
            _throwingClass  = method.getClass().getName();
            _throwingMethod = "getContent()";
            HttpEntity responseEntity  = response.getEntity();

            byte[] body = null;
            if (responseEntity != null) {
               InputStream in = responseEntity.getContent();

               if (in != null) {
                  _throwingMethod    = "getResponseContentLength()";
                  int contentLength = (int) response.getEntity().getContentLength();

                  // Create byte array output stream
                  int size = contentLength > 0 ? contentLength : 4096;
                  ByteArrayOutputStream out = new ByteArrayOutputStream(size);
                  byte[] buffer = new byte[4096];

                  // Copy from the input stream to the byte array
                  String inClass  = in.getClass().getName();
                  String outClass = "java.io.ByteArrayOutputStream";
                  _throwingClass  = inClass;
                  _throwingMethod = "read(byte[])";
                  for (int len = in.read(buffer); len > 0; ) {

                     _throwingClass  = outClass;
                     _throwingMethod = "write(byte[],int,int)";
                     out.write(buffer, 0, len);

                     _throwingClass  = inClass;
                     _throwingMethod = "read(byte[])";
                     len             = in.read(buffer);
                  }

                  _throwingClass  = outClass;
                  _throwingMethod = "toByteArray()";
                  body            = out.toByteArray();
               }
            }

            // Store the result
            _throwingClass  = HTTPCallResultDataHandler.class.getName();
            _throwingMethod = "<init>(int,byte[])";
            _result         = new HTTPCallResultDataHandler(statusCode, body);

            // No exception thrown, reset _throwingXXXX fields
            _throwingClass  = null;
            _throwingMethod = null;

         // If an exception is thrown, store it for processing at later stage
         } catch (Throwable exception) {
            _exception = exception;

         // Release the HTTP connection immediately
         } finally {
            method.abort();
         }

         // Remove the diagnostic context ID
         if (_context != null) {
            NDC.pop();
         }
         NDC.remove();

         // Set objects to null for garbage collection
         _callConfig = null;
         _context = null;
         _request = null;
         _target = null;
      }

      /**
       * Gets the exception if any generated when calling the method.
       *
       * @return
       *    the invocation exception or <code>null</code> if the call was
       *    performed successfully.
       */
      private Throwable getException() {
         return _exception;
      }

      /**
       * Gets the name of the class that threw the exception.
       *
       * @return
       *    the name of the class that threw the exception or
       *    <code>null</code> if the call was performed successfully.
       */
      private String getThrowingClass() {
         return _throwingClass;
      }

      /**
       * Gets the name of the method that threw the exception.
       *
       * @return
       *    the name of the method that threw the exception or
       *    <code>null</code> if the call was performed successfully.
       */
      private String getThrowingMethod() {
         return _throwingMethod;
      }

      /**
       * Returns the result if the call was successful. If the call was
       * unsuccessful, then <code>null</code> is returned.
       *
       * @return
       *    the result from the call, or <code>null</code> if it was
       *    unsuccessful.
       */
      private HTTPCallResultData getData() {
         return _result;
      }

      /**
       * Disposes the result variables, so that the variables could be
       * garbage collected.
       */
      private void dispose() {
         _exception = null;
         _throwingClass = null;
         _throwingMethod = null;
         _result = null;
      }
   }

   /**
    * Container of the data part of an HTTP call result.
    *
    * @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
    * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
    *
    * @since XINS 1.0.0
    */
   private static final class HTTPCallResultDataHandler
   implements HTTPCallResultData {
      /**
       * Constructs a new <code>HTTPCallResultDataHandler</code> object.
       *
       * @param code
       *    the HTTP status code.
       *
       * @param data
       *    the data returned from the call, as a set of bytes.
       */
      HTTPCallResultDataHandler(int code, byte[] data) {
         _code = code;
         _data = data;
      }

      /**
       * The HTTP status code.
       */
      private final int _code;

      /**
       * The data returned.
       */
      private final byte[] _data;

      /**
       * Returns the HTTP status code.
       *
       * @return
       *    the HTTP status code.
       */
      public int getStatusCode() {
         return _code;
      }

      /**
       * Returns the result data as a byte array. Note that this is not a copy or
       * clone of the internal data structure, but it is a link to the actual
       * data structure itself.
       *
       * @return
       *    a byte array of the result data, never <code>null</code>.
       */
      public byte[] getData() {
         return _data;
      }
   }

   /**
    * Post method that encode the Unicode characters above 255 as %uxxxx
    * where xxxx is the hexadecimal value of the character.
    *
    * @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
    * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
    *
    * @since XINS 1.4.0
    */
   /*private static class UnicodePostMethod extends PostMethod {

      public UnicodePostMethod(String url) {
         super(url);

         // Disable retries
         getParams().setParameter(HttpMethodParams.RETRY_HANDLER, NO_RETRIES);
      }

      protected RequestEntity generateRequestEntity() {
         NameValuePair[] params = getParameters();
         int paramsCount = params.length;
         if (paramsCount == 0) {
            return super.generateRequestEntity();
         } else {
            StringBuffer queryString = new StringBuffer();
            for (int i = 0; i < paramsCount; i++) {
               if (i > 0) {
                  queryString.append('&');
               }
               queryString.append(URLEncoding.encode(params[i].getName()));
               queryString.append('=');
               queryString.append(URLEncoding.encode(params[i].getValue()));
            }
            try {
               return new StringRequestEntity(queryString.toString(),
                     "application/x-www-form-urlencoded", "UTF-8");
            } catch (UnsupportedEncodingException ueex) {
               // Should never happen
               throw Utils.logProgrammingError(ueex);
            }
         }
      }
   }*/
}