/*
 * $Id: HTTPServiceCaller.java,v 1.122 2007/09/18 08:45:09 agoubard Exp $
 *
 * Copyright 2003-2007 Orange Nederland Breedband B.V.
 * See the COPYRIGHT file for redistribution and use restrictions.
 */
package org.xins.common.http;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.UnknownHostException;
import java.util.Iterator;

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpRecoverableException;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.httpclient.util.TimeoutController;
import org.apache.commons.httpclient.util.TimeoutController.TimeoutException;

import org.apache.log4j.NDC;
import org.xins.common.FormattedParameters;

import org.xins.common.Log;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.collections.PropertyReader;
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 currently only supports the HTTP protocol. If a
 * {@link TargetDescriptor} is passed to the constructor with a different
 * protocol, then an {@link UnsupportedProtocolException} is thrown. In the
 * future, HTTPS is expected to be supported as well.
 *
 * <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 (in the second form) depends on the
 * {@link Descriptor} passed to the
 * {@link #HTTPServiceCaller(Descriptor)} constructor. If it is a
 * {@link TargetDescriptor}, then only this single target service is called
 * and no load-balancing is performed. 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.
 * 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>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 org.xins.common.collections.BasicPropertyReader} properties = new {@link org.xins.common.collections.BasicPropertyReader#BasicPropertyReader() BasicPropertyReader}();
properties.{@link org.xins.common.collections.BasicPropertyReader#set(String,String) set}("myapi",         "group, random, server1, server2");
properties.{@link org.xins.common.collections.BasicPropertyReader#set(String,String) set}("myapi.server1", "service, http://server1/myapi, 10000");
properties.{@link org.xins.common.collections.BasicPropertyReader#set(String,String) set}("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(PropertyReader,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 org.xins.common.collections.BasicPropertyReader} params = new {@link org.xins.common.collections.BasicPropertyReader BasicPropertyReader}();
params.{@link org.xins.common.collections.BasicPropertyReader#set(String,String) set}("street",      "Broadband Avenue");
params.{@link org.xins.common.collections.BasicPropertyReader#set(String,String) set}("houseNumber", "12");

{@link HTTPCallRequest} request = new {@link HTTPCallRequest#HTTPCallRequest(HTTPMethod,PropertyReader) HTTPCallRequest}({@link HTTPMethod}.{@link HTTPMethod#GET GET}, params);
{@link HTTPCallResult} result = caller.{@link #call(HTTPCallRequest) call}(request);</pre></blockquote>
 *
 * @version $Revision: 1.122 $ $Date: 2007/09/18 08:45:09 $
 * @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 final class HTTPServiceCaller extends ServiceCaller {

   /**
    * 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 int CALL_EXECUTOR_COUNT;

   /**
    * Lock object for <code>CALL_EXECUTOR_COUNT</code>. Never
    * <code>null</code>.
    */
   private static final Object CALL_EXECUTOR_COUNT_LOCK = new Object();

   /**
    * HTTP retry handler that does not allow any retries.
    */
   private static DefaultHttpMethodRetryHandler NO_RETRIES = new DefaultHttpMethodRetryHandler(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 target
    *    the target of the service.
    *
    * @return
    *    the HttpClient shared instance.
    */
   private static HttpClient getHttpClient(TargetDescriptor target) {

      HttpClient httpClient= new HttpClient();

      // Add support for proxies
      int proxyPort = 80;
      if ("true".equals(System.getProperty("proxySet")) && System.getProperty("proxyHost") != null) {
         String proxyHost = System.getProperty("proxyHost");
         if (System.getProperty("proxyPort") != null) {
            proxyPort = Integer.parseInt(System.getProperty("proxyPort"));
         }
         httpClient.getHostConfiguration().setProxy(proxyHost, proxyPort);
      } else if (System.getProperty("http.proxyHost") != null) {
         String proxyHost = System.getProperty("http.proxyHost");
         if (System.getProperty("http.proxyPort") != null) {
            proxyPort = Integer.parseInt(System.getProperty("http.proxyPort"));
         }
         httpClient.getHostConfiguration().setProxy(proxyHost, proxyPort);
      }

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

      // Configure connection time-out and socket time-out
      // For compatibility with HTTPClient 2.0, the deprecated methods are still used.
      httpClient.setHttpConnectionFactoryTimeout(connectionTimeOut);
      httpClient.setTimeout(socketTimeOut);

      return httpClient;
   }

   /**
    * Creates an appropriate <code>HttpMethodBase</code> object for the
    * specified URL.
    *
    * @param url
    *    the URL for which to create an {@link HttpMethodBase} 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 HttpMethodBase} object, not <code>null</code>.
    */
   private static HttpMethodBase createMethod(String          url,
                                              HTTPCallRequest request,
                                              HTTPCallConfig  callConfig) {

      // Get the HTTP method (like GET and POST) and parameters
      HTTPMethod     method     = callConfig.getMethod();
      PropertyReader parameters = request.getParameters();

      // HTTP POST request
      if (method == HTTPMethod.POST) {
         PostMethod postMethod = new UnicodePostMethod(url);

         // Loop through the parameters
         if (parameters != null) {
            Iterator keys = parameters.getNames();
            while (keys.hasNext()) {

               // Get the parameter key
               String key = (String) keys.next();

               // Get the value
               String value = parameters.get(key);
               if (value == null) {
                  value = "";
               }

               // Add this parameter key/value combination.
               if (key != null) {
                  postMethod.addParameter(key, value);
               }
            }
         }

         return postMethod;

      // HTTP GET request
      } else if (method == HTTPMethod.GET) {
         GetMethod getMethod = new GetMethod(url);
         getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, NO_RETRIES);

         // Loop through the parameters
         StringBuffer query = new StringBuffer(255);
         Iterator keys = parameters.getNames();
         while (keys.hasNext()) {

            // Get the parameter key
            String key = (String) keys.next();

            // Get the value
            String value = parameters.get(key);
            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) {
            getMethod.setQueryString(query.toString());
         }

         return getMethod;

      // Unrecognized HTTP method (only GET and POST are supported)
      } else {
         String detail = "Unrecognized HTTP method \"" + method + "\".";
         throw Utils.logProgrammingError(detail);
      }
   }

   /**
    * 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>ServiceCaller</code>
    * throws an {@link UnsupportedOperationException}.
    *
    * @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
      PropertyReader 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 {
         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
         //
         // XXX: We do not use instanceof because class ConnectTimeoutException
         //      is not available in HTTPClient 2.0
         } else if (exceptionName.equals("org.apache.commons.httpclient.ConnectTimeoutException")
               || exceptionName.equals("org.apache.commons.httpclient.HttpConnection.ConnectionTimeoutException")) {
            Log.log_1104(url, params, duration, connectionTimeOut);
            executor.dispose();
            throw new ConnectionTimeOutCallException(request, target, duration);

         // Socket time-out (Java 1.4+)
         //
         // XXX: We do not use instanceof because class SocketTimeoutException
         //      is not available in Java 1.3
         } else if (exceptionName.equals("java.net.SocketTimeoutException")
               || (exception instanceof HttpRecoverableException
                  && ((HttpRecoverableException) exception).getReason().indexOf("Read timed out") != -1)) {
            Log.log_1105(url, params, duration, socketTimeOut);
            executor.dispose();
            throw new SocketTimeOutCallException(request, target, duration);

         // Socket time-out (Java 1.3)
         } else if (exception instanceof InterruptedIOException) {
            String exMessage = exception.getMessage();

            // XXX: Only tested on Sun JVM
            // TODO: Test on non-Sun JVM
            if (exMessage != null && exMessage.startsWith("Read timed out")) {
               Log.log_1105(url, params, duration, socketTimeOut);
               executor.dispose();
               throw new SocketTimeOutCallException(request, target, duration);

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

         // 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();
         throw new StatusCodeHTTPCallException(request, target, duration, code);
      }

      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,
                                         CallExceptionList 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,
                                    CallExceptionList exceptions) {

      CallException last = exceptions.last();

      // 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;

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

   /**
    * Executor of calls to an API.
    *
    * @version $Revision: 1.122 $ $Date: 2007/09/18 08:45:09 $
    * @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;
         synchronized (CALL_EXECUTOR_COUNT_LOCK) {
            instanceID = CALL_EXECUTOR_COUNT++;
         }

         // 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(_target);

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

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

         // Set the user agent, if specified.
         String userAgent = _callConfig.getUserAgent();
         if (! TextUtils.isEmpty(userAgent)) {
            method.setRequestHeader("User-Agent", userAgent);
         }

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

            // Get response body
            _throwingClass  = method.getClass().getName();
            _throwingMethod = "getResponseBodyAsStream()";
            InputStream in  = method.getResponseBodyAsStream();

            byte[] body = null;
            if (in != null) {
               _throwingMethod    = "getResponseContentLength()";
               int contentLength = 4096;

               // This method fails with HTTPClient 2.0.
               try {
                  contentLength = (int) method.getResponseContentLength();
               } catch (NoSuchMethodError nsme) {
                  // Ignore
               }

               // 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 {
            try {
               method.releaseConnection();
            } catch (Throwable exception) {
               String thisMethod = "run()";
               String subjectClass = method.getClass().getName();
               String subjectMethod = "releaseConnection()";
               Log.log_1052(exception, EXECUTOR_CLASSNAME, thisMethod, subjectClass, subjectMethod, null);
            }
         }

         // 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.122 $ $Date: 2007/09/18 08:45:09 $
    * @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.122 $ $Date: 2007/09/18 08:45:09 $
    * @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);
            }
         }
      }
   }
}