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

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.xins.common.FormattedParameters;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.http.HTTPCallConfig;
import org.xins.common.http.HTTPCallException;
import org.xins.common.http.HTTPCallRequest;
import org.xins.common.http.HTTPCallResult;
import org.xins.common.http.HTTPServiceCaller;
import org.xins.common.http.StatusCodeHTTPCallException;
import org.xins.common.service.CallConfig;
import org.xins.common.service.CallException;
import org.xins.common.service.CallRequest;
import org.xins.common.service.CallResult;
import org.xins.common.service.ConnectionTimeOutCallException;
import org.xins.common.service.ConnectionRefusedCallException;
import org.xins.common.service.Descriptor;
import org.xins.common.service.GenericCallException;
import org.xins.common.service.IOCallException;
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.spec.ErrorCodeSpec;
import org.xins.common.text.ParseException;
import org.xins.common.text.TextUtils;
import org.w3c.dom.Element;
import org.xins.common.http.*;
import org.znerd.util.ExceptionUtils;

/**
 * XINS service caller. This class can be used to perform a call to a XINS
 * service, over HTTP, and fail-over to other XINS services 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 and other protocols are expected to be supported as well.
 *
 * <h2>Load-balancing and fail-over</h2>
 *
 * <p>To perform a XINS call, use {@link #call(XINSCallRequest)}. Fail-over
 * and load-balancing can be performed automatically.
 *
 * <p>How load-balancing is done depends on the {@link Descriptor} passed to
 * the {@link #XINSServiceCaller(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>XINSServiceCaller</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 {@link XINSCallRequest};
 *    <li>on connection refusal;
 *    <li>if a connection attempt times out;
 *    <li>if an HTTP status code other than 200-299 is returned;
 *    <li>if the XINS error code <em>_InvalidRequest</em> is returned;
 *    <li>if the XINS error code <em>_DisabledFunction</em> 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 a
 * <code>XINSServiceCaller</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}();
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 a XINSServiceCaller instance
{@link Descriptor Descriptor} descriptor = {@link org.xins.common.service.DescriptorBuilder DescriptorBuilder}.{@link org.xins.common.service.DescriptorBuilder#build(Map,String) build}(properties, "myapi");
XINSServiceCaller caller = new {@link #XINSServiceCaller(Descriptor) XINSServiceCaller}(descriptor);</pre></blockquote>
 *
 * <p>Then the following code snippet uses this <code>XINSServiceCaller</code>
 * to perform a call to a XINS function named <em>_GetStatistics</em>, using
 * HTTP POST:
 *
 * <blockquote><pre>// Prepare for the call
{@link String}          function = "_GetStatistics";
{@link java.util.Map}  params   = null;
boolean         failOver = true;
{@link org.xins.common.http.HTTPMethod}      method   = {@link org.xins.common.http.HTTPMethod}.{@link org.xins.common.http.HTTPMethod#POST POST};
{@link XINSCallRequest} request  = new {@link XINSCallRequest#XINSCallRequest(String,Map,boolean,HTTPMethod) XINSCallRequest}(function, params, failOver, method);

// Perform the call
{@link XINSCallResult} result = caller.{@link #call(XINSCallRequest) call}(request);</pre></blockquote>
 *
 * @version $Revision: 1.195 $ $Date: 2013/01/23 11:36:37 $
 * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
 *
 * @since XINS 1.0.0
 */
public class XINSServiceCaller extends ServiceCaller {

   /**
    * The not modified status code.
    */
   private final static int NOT_MODIFIED = 304;

   /**
    * The result parser. This field cannot be <code>null</code>.
    */
   private final XINSCallResultParser _parser;

   /**
    * The <code>CAPI</code> object that uses this caller. This field is
    * <code>null</code> if this caller is not used by a <code>CAPI</code>
    * class.
    */
   private AbstractCAPI _capi;

   /**
    * The map containing the service caller to call for the descriptor.
    * The key of the {@link HashMap} is a {@link TargetDescriptor} and the value
    * is a {@link ServiceCaller}.
    */
   private HashMap<TargetDescriptor, ServiceCaller> _serviceCallers;

   /**
    * Constructs a new <code>XINSServiceCaller</code> with the specified
    * descriptor and call configuration.
    *
    * @param descriptor
    *    the descriptor of the service, cannot be <code>null</code>.
    *
    * @param callConfig
    *    the call configuration object for this service caller, or
    *    <code>null</code> if a default one should be associated with this
    *    service caller.
    *
    * @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 XINSServiceCaller(Descriptor descriptor, XINSCallConfig callConfig)
   throws IllegalArgumentException, UnsupportedProtocolException {

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

      // Initialize the fields
      _parser      = new XINSCallResultParser();
   }

   /**
    * Constructs a new <code>XINSServiceCaller</code> with the specified
    * descriptor and the default HTTP method.
    *
    * @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 XINSServiceCaller(Descriptor descriptor)
   throws IllegalArgumentException, UnsupportedProtocolException {
      this(descriptor, null);
   }

   /**
    * Constructs a new <code>XINSServiceCaller</code> with no
    * descriptor (yet) and the default HTTP method.
    *
    * <p>Before actual calls can be made, {@link #setDescriptor(Descriptor)}
    * should be used to set the descriptor.
    *
    * @since XINS 1.2.0
    */
   public XINSServiceCaller() {
      this((Descriptor) null, (XINSCallConfig) null);
   }

   /**
    * 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)  || "file".equalsIgnoreCase(protocol);
   }

   public void setDescriptor(Descriptor descriptor) {
      super.setDescriptor(descriptor);

      // Create the ServiceCaller for each descriptor
      if (_serviceCallers == null) {
         _serviceCallers = new HashMap();
      }
      if (descriptor != null) {
         for (TargetDescriptor nextTarget : descriptor) {
            String protocol = nextTarget.getProtocol();
            if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) {
               HTTPServiceCaller serviceCaller = new HTTPServiceCaller(nextTarget);
               _serviceCallers.put(nextTarget, serviceCaller);
            } else if ("file".equalsIgnoreCase(protocol)) {
               FileServiceCaller serviceCaller = new FileServiceCaller(nextTarget);
               _serviceCallers.put(nextTarget, serviceCaller);
            }
         }
      } else {
         _serviceCallers.clear();
      }
   }

   /**
    * Sets the associated <code>CAPI</code> instance.
    *
    * <p>This method is expected to be called only once, before any calls are
    * made with this caller.
    *
    * @param capi
    *    the associated <code>CAPI</code> instance, or
    *    <code>null</code>.
    */
   void setCAPI(AbstractCAPI capi) {
      _capi = capi;
   }

   /**
    * 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 XINSServiceCaller}
    * returns a standard {@link XINSCallConfig} object which has unconditional
    * fail-over disabled and the HTTP method set to
    * {@link org.xins.common.http.HTTPMethod#POST POST}.
    *
    * @return
    *    a new {@link XINSCallConfig} instance with default settings, never
    *    <code>null</code>.
    */
   @Override
   protected CallConfig getDefaultCallConfig() {
      return new XINSCallConfig();
   }

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

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

   /**
    * Executes the specified XINS call request towards one of the associated
    * targets. If the call succeeds with one of these targets, then a
    * {@link XINSCallResult} object is returned. Otherwise, if none of the
    * targets could successfully be called, a
    * {@link org.xins.common.service.CallException} is thrown.
    *
    * <p>If the call succeeds, but the result is unsuccessful, then an
    * {@link UnsuccessfulXINSCallException} is thrown, which contains the
    * result.
    *
    * @param request
    *    the call request, not <code>null</code>.
    *
    * @param callConfig
    *    the call configuration, or <code>null</code> if the one specified in
    *    the request should be used, or -if the request does not specify any
    *    either- the one specified for this service caller.
    *
    * @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.
    *
    * @throws XINSCallException
    *    if the first call attempt failed due to a XINS-related reason and
    *    all the other call attempts failed as well.
    *
    * @since XINS 1.1.0
    */
   public XINSCallResult call(XINSCallRequest request,
                              XINSCallConfig  callConfig)
   throws IllegalArgumentException,
          GenericCallException,
          HTTPCallException,
          XINSCallException {

      // Determine when we started the call
      long start = System.currentTimeMillis();

      // Perform the call
      XINSCallResult result;
      try {
         result = (XINSCallResult) doCall(request,callConfig);

      // Handle failures
      } catch (Throwable exception) {

         // Log that the call completely failed, unless the back-end returned
         // a functional error code. We assume that a functional error code
         // can never fail-over, so this issue will have been logged at the
         // correct (non-error) level already.
         if (!(exception instanceof UnsuccessfulXINSCallException) ||
               ((UnsuccessfulXINSCallException) exception).getType() != ErrorCodeSpec.FUNCTIONAL) {

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

            // Serialize all parameters, including the data section, for logging
            Map<String, String> parameters = request.getParameters();
            Element dataSection = request.getDataSection();
            FormattedParameters params = new FormattedParameters(parameters, dataSection, "(null)", "&", 160);

            // Serialize the exception chain
            String chain = exception.getMessage();

            Log.log_2113(request.getFunctionName(), params, duration, chain);
         }

         // Allow only GenericCallException, HTTPCallException and
         // XINSCallException to proceed
         if (exception instanceof GenericCallException) {
            throw (GenericCallException) exception;
         } if (exception instanceof HTTPCallException) {
            throw (HTTPCallException) exception;
         } if (exception instanceof XINSCallException) {
            throw (XINSCallException) exception;

         // Unknown kind of exception. This should never happen. Log and
         // re-throw the exception, wrapped within a ProgrammingException
         } else {
            throw Utils.logProgrammingError(exception);
         }
      }

      return result;
   }

   /**
    * Executes the specified XINS call request towards one of the associated
    * targets. If the call succeeds with one of these targets, then a
    * {@link XINSCallResult} object is returned. Otherwise, if none of the
    * targets could successfully be called, a
    * {@link org.xins.common.service.CallException} is thrown.
    *
    * <p>If the call succeeds, but the result is unsuccessful, then an
    * {@link UnsuccessfulXINSCallException} is thrown, which contains the
    * result.
    *
    * @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.
    *
    * @throws XINSCallException
    *    if the first call attempt failed due to a XINS-related reason and
    *    all the other call attempts failed as well.
    */
   public XINSCallResult call(XINSCallRequest request)
   throws IllegalArgumentException,
          GenericCallException,
          HTTPCallException,
          XINSCallException {
      return call(request, null);
   }

   /**
    * Executes the specified request on the given target. If the call
    * succeeds, then a {@link XINSCallResult} object is returned, otherwise a
    * {@link org.xins.common.service.CallException} is thrown.
    *
    * @param target
    *    the target to call, cannot be <code>null</code>.
    *
    * @param callConfig
    *    the call configuration, never <code>null</code>.
    *
    * @param request
    *    the call request to be executed, must be an instance of class
    *    {@link XINSCallRequest}, cannot be <code>null</code>.
    *
    * @return
    *    the result, if and only if the call succeeded, always an instance of
    *    class {@link XINSCallResult}, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>request    == null
    *          || callConfig == null
    *          || target     == null</code>.
    *
    * @throws ClassCastException
    *    if the specified <code>request</code> object is not <code>null</code>
    *    and not an instance of class {@link XINSCallRequest}.
    *
    * @throws GenericCallException
    *    if the call attempt failed due to a generic reason.
    *    other call attempts failed as well.
    *
    * @throws HTTPCallException
    *    if the call attempt failed due to an HTTP-related reason.
    *
    * @throws XINSCallException
    *    if the call attempt failed due to a XINS-related reason.
    */
   public Object doCallImpl(CallRequest      request,
                            CallConfig       callConfig,
                            TargetDescriptor target)
   throws IllegalArgumentException,
          ClassCastException,
          GenericCallException,
          HTTPCallException,
          XINSCallException {

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

      // Convert arguments to the appropriate classes
      XINSCallRequest xinsRequest = (XINSCallRequest) request;
      XINSCallConfig  xinsConfig  = (XINSCallConfig)  callConfig;

      // Get URL, function and parameters (for logging)
      String              url       = target.getURL();
      String              function  = xinsRequest.getFunctionName();
      Map<String, String> p         = xinsRequest.getParameters();
      Element dataSection = xinsRequest.getDataSection();

      FormattedParameters params = new FormattedParameters(p, dataSection, "", "&", 160);

      // Get the time-out values (for logging)
      int totalTimeOut      = target.getTotalTimeOut();
      int connectionTimeOut = target.getConnectionTimeOut();
      int socketTimeOut     = target.getSocketTimeOut();

      // Log: Right before the call is performed
      Log.log_2100(url, function, params);

      // Get the contained HTTP request from the XINS request
      HTTPCallRequest httpRequest = xinsRequest.getHTTPCallRequest();

      // Convert XINSCallConfig to HTTPCallConfig
      HTTPCallConfig httpConfig = xinsConfig.getHTTPCallConfig();

      // Determine the start time. Only required when an unexpected kind of
      // exception is caught.
      long start = System.currentTimeMillis();

      // Perform the HTTP call
      HTTPCallResult httpResult;
      long duration;
      try {
         ServiceCaller serviceCaller = (ServiceCaller) _serviceCallers.get(target);
         httpResult = (HTTPCallResult) serviceCaller.doCallImpl(httpRequest, httpConfig, target);

      // Call failed due to a generic service calling error
      } catch (GenericCallException exception) {
         duration = exception.getDuration();
         if (exception instanceof UnknownHostCallException) {
            Log.log_2102(url, function, params, duration);
         } else if (exception instanceof ConnectionRefusedCallException) {
            Log.log_2103(url, function, params, duration);
         } else if (exception instanceof ConnectionTimeOutCallException) {
            Log.log_2104(url, function, params, duration, connectionTimeOut);
         } else if (exception instanceof SocketTimeOutCallException) {
            Log.log_2105(url, function, params, duration, socketTimeOut);
         } else if (exception instanceof TotalTimeOutCallException) {
            Log.log_2106(url, function, params, duration, totalTimeOut);
         } else if (exception instanceof IOCallException) {
            Log.log_2109(exception, url, function, params, duration);
         } else if (exception instanceof UnexpectedExceptionCallException) {
            Log.log_2111(ExceptionUtils.getRootCause(exception), url, function, params, duration);
         } else {
            String detail = "Unrecognized GenericCallException subclass "
                  + exception.getClass().getName() + '.';
            Utils.logProgrammingError(detail);
         }
         logTransaction(exception, start, url, function, duration, null, params, null);
         throw exception;

      // Call failed due to an HTTP-related error
      } catch (HTTPCallException exception) {
         duration = exception.getDuration();
         if (exception instanceof StatusCodeHTTPCallException) {
            int code = ((StatusCodeHTTPCallException) exception).getStatusCode();
            Log.log_2108(url, function, params, duration, code);
         } else {
            String detail = "Unrecognized HTTPCallException subclass "
                  + exception.getClass().getName() + '.';
            Utils.logProgrammingError(detail);
         }
         logTransaction(exception, start, url, function, duration, null, params, null);
         throw exception;

      // Unknown kind of exception. This should never happen. Log and re-throw
      // the exception, packed up as a CallException.
      } catch (Throwable exception) {
         duration = System.currentTimeMillis() - start;
         Utils.logProgrammingError(exception);

         String message = "Unexpected exception: " + exception.getClass().getName()
               + ". Message: " + TextUtils.quote(exception.getMessage()) + '.';

         Log.log_2111(exception, url, function, params, duration);
         logTransaction(exception, start, url, function, duration, null, params, null);
         throw new UnexpectedExceptionCallException(request, target, duration, message, exception);
      }

      // Determine duration
      duration = httpResult.getDuration();

      // Make sure data was received
      byte[] httpData = httpResult.getData();
      if (httpResult.getStatusCode() == NOT_MODIFIED) {

         // Log: Not modified was received
         Log.log_2110(url, function, params, duration, "Not modified received.");
         logTransaction(null, start, url, function, duration, null, params, null);

         return new XINSCallResult((XINSCallRequest) request, target, duration, null, null);

      } else if (httpData == null || httpData.length == 0) {

         // Log: No data was received
         Log.log_2110(url, function, params, duration, "No data received.");
         logTransaction(null, start, url, function, duration, "=NoHTTPData", params, null);

         // Throw an appropriate exception
         throw InvalidResultXINSCallException.noDataReceived(xinsRequest, target, duration);
      }

      // Parse the result
      XINSCallResultData resultData;
      try {
         resultData = _parser.parse(httpData);

      // If parsing failed, then abort
      } catch (ParseException exception) {

         // Create a message for the new exception
         String detail = exception.getDetail();
         String message = detail != null && detail.trim().length() > 0
                        ? "Failed to parse result: " + detail.trim()
                        : "Failed to parse result.";

         // Log: Parsing failed
         Log.log_2110(url, function, params, duration, message);
         logTransaction(exception, start, url, function, duration, "=ParseError", params, null);

         // Throw an appropriate exception
         throw InvalidResultXINSCallException.parseError(httpData, xinsRequest, target, duration, exception);
      }

      // Convert the output parameters to a FormattedParameters object
      FormattedParameters outParams = new FormattedParameters(resultData.getParameters(), resultData.getDataElement(), "(null)", "&", 160);

      // If the result is unsuccessful, then throw an exception
      String errorCode = resultData.getErrorCode();
      if (errorCode != null) {

         boolean functionalError = false;
         ErrorCodeSpec.Type type = null;
         if (_capi != null) {
            functionalError = _capi.isFunctionalError(errorCode);
         }

         // Log this
         if (functionalError) {
            Log.log_2115(url, function, params, duration, errorCode);
         } else {
            Log.log_2112(url, function, params, duration, errorCode);
         }
         logTransaction(null, start, url, function, duration, errorCode, params, outParams);

         // Standard error codes (start with an underscore)
         if (errorCode.charAt(0) == '_') {
            if (errorCode.equals("_DisabledFunction")) {
               throw new DisabledFunctionException(xinsRequest, target, duration, resultData);
            } else if (errorCode.equals("_InternalError") || errorCode.equals("_InvalidResponse")) {
               throw new InternalErrorException(xinsRequest, target, duration, resultData);
            } else if (errorCode.equals("_InvalidRequest")) {
               throw new InvalidRequestException(xinsRequest, target, duration, resultData);
            } else {
               throw new UnacceptableErrorCodeXINSCallException(xinsRequest, target, duration, resultData);
            }

         // Non-standard error codes, CAPI not used
         } else if (_capi == null) {
            throw new UnsuccessfulXINSCallException(xinsRequest, target, duration, resultData, null);

         // Non-standard error codes, CAPI used
         } else {
            AbstractCAPIErrorCodeException ex = _capi.createErrorCodeException(xinsRequest, target, duration, resultData);

            if (ex != null) {
               ex.setType(type);
               throw ex;
            } else {

               // If the CAPI class was generated using a XINS release older
               // than 1.2.0, then it will not override the
               // 'createErrorCodeException' method and consequently the
               // method will return null. It cannot be determined here
               // whether the error code is acceptable or not
               String ver = _capi.getXINSVersion();
               if (ver.startsWith("1.0.") || ver.startsWith("1.1.")) {
                  throw new UnsuccessfulXINSCallException(xinsRequest, target, duration, resultData, null);
               } else {
                  throw new UnacceptableErrorCodeXINSCallException(xinsRequest, target, duration, resultData);
               }
            }
         }
      }

      // Call completely succeeded
      Log.log_2101(url, function, params, duration);
      logTransaction(null, start, url, function, duration, errorCode, params, outParams);

      return resultData;
   }

   /**
    * 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 XINSServiceCaller} expects an {@link XINSCallRequest} and
    * returns an {@link XINSCallResult}.
    *
    * @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 XINSCallRequest}.
    *
    * @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 org.xins.common.service.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 XINSCallResult}, never <code>null</code>; .
    *
    * @return
    *    a {@link XINSCallResult} 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 {

      if (result instanceof XINSCallResult) {
         return (XINSCallResult) result;
      }

      XINSCallResult r = new XINSCallResult((XINSCallRequest) request,
                                            succeededTarget,
                                            duration,
                                            exceptions,
                                            (XINSCallResultData) result);
      return r;
   }

   /**
    * Determines whether a call should fail-over to the next selected target
    * based on a request, call configuration and exception list.
    *
    * @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> and never empty.
    *
    * @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) {

      // Get the most recent exception
      CallException exception = exceptions.get(exceptions.size() - 1);

      boolean should;

      // Let the superclass look at this first.
      if (super.shouldFailOver(request, callConfig, exceptions)) {
         should = true;

      // Otherwise check if the request may fail-over from HTTP point-of-view
      //
      // XXX: Note that this duplicates code that is already in the
      //      HTTPServiceCaller. This may need to be refactored at some point.
      //      It has been decided to take this approach since the
      //      shouldFailOver method in class HTTPServiceCaller has protected
      //      access.
      //      An alternative solution that should be investigated is to
      //      subclass HTTPServiceCaller.

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

      // Some XINS error codes indicate the request was not accepted
      } else if (exception instanceof UnsuccessfulXINSCallException) {
         String s = ((UnsuccessfulXINSCallException) exception).getErrorCode();
         should = ("_InvalidRequest".equals(s)
                || "_DisabledFunction".equals(s));

      // Otherwise do not fail over
      } else {
         should = false;
      }

      return should;
   }

   /**
    * Performs (client-side) transaction logging.
    *
    * @param exception
    *    the exception, if any, can be <code>null</code>.
    *
    * @param start
    *    the start as the number of milliseconds since the UNIX Epoch.
    *
    * @param url
    *    the URL that is being called, should not be <code>null</code>.
    *
    * @param functionName
    *    the name of the function that is being invoked, should not be <code>null</code>.
    *
    * @param duration
    *    the duration of the call in milliseconds.
    *
    * @param errorCode
    *    the error code, should not be <code>null</code>.
    *
    * @param inParams
    *    the input parameters, should not be <code>null</code>.
    *
    * @param outParams
    *    the output parameters, should not be <code>null</code>.
    */
   private static void logTransaction(Throwable exception, long start, String url, String functionName, long duration, String errorCode, FormattedParameters inParams, FormattedParameters outParams) {

      final Object hyphen = "-";

      if (errorCode == null && exception != null) {
         String exceptionName = exception.getClass().getName();
         exceptionName = exceptionName.substring(exceptionName.lastIndexOf('.') + 1);
         if (exceptionName.endsWith("CallException")) {
            exceptionName = exceptionName.substring(0, exceptionName.length() - 13);
         }
         errorCode = "=" + exceptionName;
      }
      errorCode              = (errorCode == null) ? "0"    : errorCode;
      Object inParamsObject  = (inParams  == null) ? hyphen : inParams;
      Object outParamsObject = (outParams == null) ? hyphen : outParams;

      Log.log_2300(exception, start, url, functionName, duration, errorCode, inParamsObject, outParamsObject);
      Log.log_2301(exception, start, url, functionName, duration, errorCode);
   }
}