/**
   * This method should be used when the caller wants to dispatch the request to a server chosen by
   * the load balancer, instead of specifying the server in the request's URI. It calculates the
   * final URI by calling {@link #computeFinalUriWithLoadBalancer(ClientRequest)} and then calls
   * {@link #execute(ClientRequest)}.
   *
   * @param request request to be dispatched to a server chosen by the load balancer. The URI can be
   *     a partial URI which does not contain the host name or the protocol.
   */
  public T executeWithLoadBalancer(S request) throws ClientException {
    int retries = 0;
    boolean done = false;
    boolean retryOkayOnOperation = okToRetryOnAllOperations;

    retryOkayOnOperation = request.isRetriable();
    // Is it okay to retry for this particular operation?

    // see if maxRetries has been overriden
    int numRetries = maxAutoRetriesNextServer;
    IClientConfig overriddenClientConfig = request.getOverrideConfig();
    if (overriddenClientConfig != null) {
      try {
        numRetries =
            Integer.parseInt(
                ""
                    + overriddenClientConfig.getProperty(
                        CommonClientConfigKey.MaxAutoRetriesNextServer, maxAutoRetriesNextServer));
      } catch (Exception e) {
        logger.warn(
            "Invalid maxAutoRetriesNextServer requested for RestClient:" + this.getClientName());
      }
      try {
        // Retry operation can be forcefully turned on or off for this particular request
        Boolean requestSpecificRetryOn =
            Boolean.valueOf(
                ""
                    + overriddenClientConfig.getProperty(
                        CommonClientConfigKey.RequestSpecificRetryOn, "false"));
        retryOkayOnOperation = requestSpecificRetryOn.booleanValue();
      } catch (Exception e) {
        logger.warn("Invalid RequestSpecificRetryOn set for RestClient:" + this.getClientName());
      }
    }

    T response = null;

    do {
      try {
        S resolved = computeFinalUriWithLoadBalancer(request);
        response = executeOnSingleServer(resolved);
        done = true;
      } catch (Exception e) {
        boolean shouldRetry = false;
        if (e instanceof ClientException) {
          // we dont want to retry for PUT/POST and DELETE, we can for GET
          shouldRetry = retryOkayOnOperation && numRetries > 0;
        }
        if (shouldRetry) {
          retries++;
          if (retries > numRetries) {
            throw new ClientException(
                ClientException.ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED,
                "NUMBER_OF_RETRIES_NEXTSERVER_EXCEEDED :"
                    + numRetries
                    + " retries, while making a RestClient call for:"
                    + request.getUri()
                    + ":"
                    + getDeepestCause(e).getMessage(),
                e);
          }
          logger.error(
              "Exception while executing request which is deemed retry-able, retrying ..., Next Server Retry Attempt#:"
                  + retries
                  + ", URI tried:"
                  + request.getUri());
        } else {
          if (e instanceof ClientException) {
            throw (ClientException) e;
          } else {
            throw new ClientException(
                ClientException.ErrorType.GENERAL,
                "Unable to execute request for URI:" + request.getUri(),
                e);
          }
        }
      }
    } while (!done);
    return response;
  }
  /**
   * Compute the final URI from a partial URI in the request. The following steps are performed:
   * <li>if host is missing and there is a load balancer, get the host/port from server chosen from
   *     load balancer
   * <li>if host is missing and there is no load balancer, try to derive host/port from virtual
   *     address set with the client
   * <li>if host is present and the authority part of the URI is a virtual address set for the
   *     client, and there is a load balancer, get the host/port from server chosen from load
   *     balancer
   * <li>if host is present but none of the above applies, interpret the host as the actual physical
   *     address
   * <li>if host is missing but none of the above applies, throws ClientException
   *
   * @param original Original URI passed from caller
   * @return new request with the final URI
   */
  protected S computeFinalUriWithLoadBalancer(S original) throws ClientException {
    URI newURI;
    URI theUrl = original.getUri();

    if (theUrl == null) {
      throw new ClientException(ClientException.ErrorType.GENERAL, "NULL URL passed in");
    }

    String host = theUrl.getHost();
    Pair<String, Integer> schemeAndPort = deriveSchemeAndPortFromPartialUri(original);
    String scheme = schemeAndPort.first();
    int port = schemeAndPort.second();
    // Various Supported Cases
    // The loadbalancer to use and the instances it has is based on how it was registered
    // In each of these cases, the client might come in using Full Url or Partial URL
    ILoadBalancer lb = getLoadBalancer();
    Object loadBalancerKey = original.getLoadBalancerKey();
    if (host == null) {
      // Partial URL Case
      // well we have to just get the right instances from lb - or we fall back
      if (lb != null) {
        Server svc = lb.chooseServer(loadBalancerKey);
        if (svc == null) {
          throw new ClientException(
              ClientException.ErrorType.GENERAL,
              "LoadBalancer returned null Server for :" + clientName);
        }
        host = svc.getHost();
        port = svc.getPort();
        if (host == null) {
          throw new ClientException(
              ClientException.ErrorType.GENERAL, "Invalid Server for :" + svc);
        }
        if (logger.isDebugEnabled()) {
          logger.debug(clientName + " using LB returned Server:" + svc + "for request:" + theUrl);
        }
      } else {
        // No Full URL - and we dont have a LoadBalancer registered to
        // obtain a server
        // if we have a vipAddress that came with the registration, we
        // can use that else we
        // bail out
        if (vipAddresses != null && vipAddresses.contains(",")) {
          throw new ClientException(
              ClientException.ErrorType.GENERAL,
              this.clientName
                  + "Partial URI of ("
                  + theUrl
                  + ") has been sent in to RestClient (with no LB) to be executed."
                  + " Also, there are multiple vipAddresses and hence RestClient cant pick"
                  + "one vipAddress to complete this partial uri");
        } else if (vipAddresses != null) {
          try {
            Pair<String, Integer> hostAndPort = deriveHostAndPortFromVipAddress(vipAddresses);
            host = hostAndPort.first();
            port = hostAndPort.second();
          } catch (URISyntaxException e) {
            throw new ClientException(
                ClientException.ErrorType.GENERAL,
                this.clientName
                    + "Partial URI of ("
                    + theUrl
                    + ") has been sent in to RestClient (with no LB) to be executed."
                    + " Also, the configured/registered vipAddress is unparseable (to determine host and port)");
          }
        } else {
          throw new ClientException(
              ClientException.ErrorType.GENERAL,
              this.clientName
                  + " has no LoadBalancer registered and passed in a partial URL request (with no host:port)."
                  + " Also has no vipAddress registered");
        }
      }
    } else {
      // Full URL Case
      // This could either be a vipAddress or a hostAndPort or a real DNS
      // if vipAddress or hostAndPort, we just have to consult the loadbalancer
      // but if it does not return a server, we should just proceed anyways
      // and assume its a DNS
      // For restClients registered using a vipAddress AND executing a request
      // by passing in the full URL (including host and port), we should only
      // consult lb IFF the URL passed is registered as vipAddress in Discovery
      boolean shouldInterpretAsVip = false;

      if (lb != null) {
        shouldInterpretAsVip = isVipRecognized(original.getUri().getAuthority());
      }
      if (shouldInterpretAsVip) {
        Server svc = lb.chooseServer(loadBalancerKey);
        if (svc != null) {
          host = svc.getHost();
          port = svc.getPort();
          if (host == null) {
            throw new ClientException(
                ClientException.ErrorType.GENERAL, "Invalid Server for :" + svc);
          }
          if (logger.isDebugEnabled()) {
            logger.debug("using LB returned Server:" + svc + "for request:" + theUrl);
          }
        } else {
          // just fall back as real DNS
          if (logger.isDebugEnabled()) {
            logger.debug(
                host + ":" + port + " assumed to be a valid VIP address or exists in the DNS");
          }
        }
      } else {
        // consult LB to obtain vipAddress backed instance given full URL
        // Full URL execute request - where url!=vipAddress
        if (logger.isDebugEnabled()) {
          logger.debug("Using full URL passed in by caller (not using LB/Discovery):" + theUrl);
        }
      }
    }
    // end of creating final URL
    if (host == null) {
      throw new ClientException(
          ClientException.ErrorType.GENERAL, "Request contains no HOST to talk to");
    }
    // just verify that at this point we have a full URL

    try {
      String urlPath = "";
      if (theUrl.getRawPath() != null && theUrl.getRawPath().startsWith("/")) {
        urlPath = theUrl.getRawPath();
      } else {
        urlPath = "/" + theUrl.getRawPath();
      }

      newURI =
          new URI(
              scheme,
              theUrl.getUserInfo(),
              host,
              port,
              urlPath,
              theUrl.getQuery(),
              theUrl.getFragment());
      return (S) original.replaceUri(newURI);
    } catch (URISyntaxException e) {
      throw new ClientException(ClientException.ErrorType.GENERAL, e.getMessage());
    }
  }
  /**
   * Execute the request on single server after the final URI is calculated. This method takes care
   * of retries and update server stats.
   */
  protected T executeOnSingleServer(S request) throws ClientException {
    boolean done = false;
    int retries = 0;

    boolean retryOkayOnOperation = okToRetryOnAllOperations;
    if (request.isRetriable()) {
      retryOkayOnOperation = true;
    }
    int numRetries = maxAutoRetries;
    URI uri = request.getUri();
    Server server = new Server(uri.getHost(), uri.getPort());
    ServerStats serverStats = null;
    ILoadBalancer lb = this.getLoadBalancer();
    if (lb instanceof AbstractLoadBalancer) {
      LoadBalancerStats lbStats = ((AbstractLoadBalancer) lb).getLoadBalancerStats();
      serverStats = lbStats.getSingleServerStat(server);
    }
    IClientConfig overriddenClientConfig = request.getOverrideConfig();
    if (overriddenClientConfig != null) {
      try {
        numRetries =
            Integer.parseInt(
                ""
                    + overriddenClientConfig.getProperty(
                        CommonClientConfigKey.MaxAutoRetries, maxAutoRetries));
      } catch (Exception e) {
        logger.warn("Invalid maxRetries requested for RestClient:" + this.clientName);
      }
    }

    T response = null;
    Exception lastException = null;
    if (tracer == null) {
      tracer =
          Monitors.newTimer(this.getClass().getName() + "_ExecutionTimer", TimeUnit.MILLISECONDS);
    }
    do {
      noteOpenConnection(serverStats, request);
      Stopwatch w = tracer.start();
      try {
        response = execute(request);
        done = true;
      } catch (Exception e) {
        if (serverStats != null) {
          serverStats.addToFailureCount();
        }
        lastException = e;
        if (isCircuitBreakerException(e) && serverStats != null) {
          serverStats.incrementSuccessiveConnectionFailureCount();
        }
        boolean shouldRetry = retryOkayOnOperation && numRetries >= 0 && isRetriableException(e);
        if (shouldRetry) {
          retries = handleRetry(uri.toString(), retries, numRetries, e);
        } else {
          ClientException niwsClientException = generateNIWSException(uri.toString(), e);
          throw niwsClientException;
        }
      } finally {
        w.stop();
        noteRequestCompletion(
            serverStats, request, response, lastException, w.getDuration(TimeUnit.MILLISECONDS));
      }
    } while (!done);
    return response;
  }