All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.qas.api.http.basic.HttpUrlConnectionAuthClient Maven / Gradle / Ivy

The newest version!
package org.qas.api.http.basic;

import org.qas.api.*;
import org.qas.api.handler.RequestHandler;
import org.qas.api.http.*;
import org.qas.api.internal.CustomBackoffStrategy;
import org.qas.api.internal.util.Https;
import org.qas.api.internal.util.google.base.Charsets;
import org.qas.api.internal.util.google.base.Strings;
import org.qas.api.internal.util.google.io.ByteStreams;
import org.qas.api.internal.util.google.io.Closeables;
import org.qas.api.internal.util.google.net.HttpHeaders;

import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * HttpUrlConnectionAuthClient
 *
 * @author Dzung Nguyen
 * @version $Id HttpUrlConnectionAuthClient 2014-03-27 11:57:30z dungvnguyen $
 * @since 1.0
 */
public class HttpUrlConnectionAuthClient extends AbstractHttpAuthClient
  implements HttpAuthClient {
  //~ class properties ========================================================
  private static final Logger LOG = Logger.getLogger(HttpUrlConnectionAuthClient.class.getName());
  private static final int HTTP_TEMPORARY_REDIRECT = 307;

  private static final int MAX_BACKOFF_IN_MILLISECONDS = 20 * 1000;
  private static final Random random = new Random();

  //~ class members ===========================================================

  /**
   * Creates {@link HttpUrlConnectionAuthClient} object from the specified
   * client configuration object.
   *
   * @param configuration the specified client configuration object.
   */
  public HttpUrlConnectionAuthClient(ClientConfiguration configuration) {
    super(configuration);
  }

  @Override
  public HttpResponse execute(Request request, ExecutionContext context)
    throws AuthClientException {
    if (context == null) {
      throw new AuthClientException("Internal SDK Error: No execution context parameter specified.");
    }

    List requestHandlers = context.getRequestHandlers();
    if (requestHandlers == null) requestHandlers = Collections.emptyList();

    // apply the additional service specific request handlers that need to be run.
    for (RequestHandler requestHandler : requestHandlers) {
      requestHandler.beforeRequest(request);
    }

    throw new UnsupportedOperationException();
  }

  @Override
  public  T execute(Request request,
                       HttpResponseHandler> responseHandler,
                       HttpResponseHandler errorResponseHandler,
                       ExecutionContext context) throws AuthClientException {
    if (context == null) {
      throw new AuthClientException("Internal SDK Error: No execution context parameter specified.");
    }

    List requestHandlers = context.getRequestHandlers();
    if (requestHandlers == null) requestHandlers = Collections.emptyList();

    // apply the additional service specific request handlers that need to be run.
    for (RequestHandler requestHandler : requestHandlers) {
      requestHandler.beforeRequest(request);
    }

    try {
      T result = executeHelper(request, responseHandler, errorResponseHandler, context);

      for (RequestHandler requestHandler : requestHandlers) {
        try {
          requestHandler.afterResponse(request, result);
        } catch (Exception ex) {
        }
      }

      return result;
    } catch (AuthClientException ex) {
      for (RequestHandler requestHandler : requestHandlers) {
        requestHandler.afterError(request, ex);
      }

      throw ex;
    }
  }

  /**
   * Executes the requests and returns the result.
   *
   * @param request              the request to send to the remote server.
   * @param errorResponseHandler A response handler to accept an unsuccessful response
   *                             from the remote server.
   * @param context              Additional information about the context of this web service call.
   */
  private  T executeHelper(Request request,
                              HttpResponseHandler> responseHandler,
                              HttpResponseHandler errorResponseHandler,
                              ExecutionContext context) throws AuthClientException {
    boolean leaveHttpConnectionOpen = false;

    // apply whatever request options we know how to handle, such as user-agent.
    applyRequestData(request);

    int retryCount = 0;
    URI redirectedUri = null;
    AuthServiceException exception = null;

    // make a copy of original request parameters and headers.
    Map originalParameters = new HashMap();
    originalParameters.putAll(request.getParameters());
    Map originalHeaders = new HashMap();
    originalHeaders.putAll(request.getHeaders());

    while (true) {
      // reset the request.
      if (retryCount > 0) {
        request.withHeaders(originalHeaders)
          .withParameters(originalParameters);
      }

      // make a connection.
      HttpURLConnection connection = null;
      try {
        // sign request if necessary.
        if (context.getSigner() != null && context.getCredentials() != null) {
          context.getSigner().sign(request, context.getCredentials());
        }

        if (LOG.isLoggable(Level.FINE)) {
          LOG.fine("Sending request: \n" + request.toString());
        }

        connection = HttpUrlConnectionFactory.createHttpRequest(request, redirectedUri, getConfiguration(), context);
        final HttpRequest httpRequest = new HttpUrlConnectionRequest(request, connection);

        // pause
        if (retryCount > 0) {
          pauseExponentially(retryCount, exception, context.getCustomBackoffStrategy());
        }

        // reset content.
        if (request.getContent() != null) {
          if (retryCount > 0) {
            if (request.getContent().markSupported()) {
              request.getContent().reset();
              request.getContent().mark(-1);
            }
          } else {
            if (request.getContent().markSupported()) {
              request.getContent().mark(-1);
            }
          }
        }

        // set the connection specified field.
        connection.setDoInput(true);
        connection.setUseCaches(false);

        // connect to server.
        connection.connect();

        // write the payload.
        if (connection.getDoOutput()) {
          writePayload(connection, request);
        }

        if (isRequestSuccessful(connection)) {
          leaveHttpConnectionOpen = responseHandler.needsConnectionLeftOpen();
          return handleResponse(httpRequest, responseHandler, connection, context);
        } else if (isTemporaryRedirect(connection)) {
          String location = connection.getHeaderField(HttpHeaders.LOCATION);
          if (LOG.isLoggable(Level.FINE)) {
            LOG.log(Level.FINE, "Redirecting to: [" + location + "]");
          }

          // set the location.
          redirectedUri = URI.create(location);
        } else {
          leaveHttpConnectionOpen = errorResponseHandler.needsConnectionLeftOpen();
          exception = handleErrorResponse(httpRequest, errorResponseHandler, connection);
          if (!shouldRetry(connection, exception, retryCount)) {
            throw exception;
          }

          // reset the request.
          resetRequestAfterError(request, exception);
        }
      } catch (IOException ioex) {
        if (LOG.isLoggable(Level.INFO)) {
          LOG.log(Level.INFO, "Unable to execute HTTP request: [" + ioex.getMessage() + "]", ioex);

          if (!shouldRetry(connection, ioex, retryCount)) {
            throw new AuthClientException("Unable to execute the HTTP request: " + ioex.getMessage(), ioex);
          }

          // reset the request.
          resetRequestAfterError(request, ioex);
        }
      } finally {
        retryCount++;

        if (!leaveHttpConnectionOpen) {
          try {
            connection.disconnect();
          } catch (Throwable t) {
          }
        }
      }
    }
  }

  private void writePayload(HttpURLConnection connection, Request request)
    throws IOException {
    // get connection output stream.
    OutputStream output = connection.getOutputStream();

    if (Https.usePayloadForQueryParameters(request)) {
      output.write(Https.toQueryString(request.getParameters(), Charsets.UTF_8).getBytes());
    } else if (request.getContent() != null) {
      ByteStreams.copy(request.getContent(), output);
    }

    // flush the content.
    output.flush();

    // close the output.
    Closeables.close(output, false);
  }

  /**
   * Applies any additional options set in the request.
   */
  private void applyRequestData(Request request) {
    if (getConfiguration().getUserAgent() != null) {
      request.setHeader("User-Agent", getConfiguration().getUserAgent());
    }
  }

  /**
   * Returns if a failed request should be retried.
   *
   * @param connection the current HTTP request being executed.
   * @param ex         The exception from the failed request.
   * @param retries    The number of times the current request has been attempted.
   * @return {@code true} if failed request should be retried.
   */
  private boolean shouldRetry(HttpURLConnection connection, Exception ex, int retries) {
    if (retries > getConfiguration().getMaxErrorRetry()) return false;

    // if return is need payment, the connection will be terminated.
    try {
      if (connection.getResponseCode() == HttpURLConnection.HTTP_PAYMENT_REQUIRED) return false;
    } catch (IOException ioe) {
    }

    if (ex instanceof IOException) {
      if (LOG.isLoggable(Level.INFO)) {
        LOG.info("Retrying on " + ex.getClass().getName() + ": " + ex.getMessage());
      }

      return true;
    }

    if (ex instanceof AuthServiceException) {
      AuthServiceException lase = (AuthServiceException) ex;
      if (lase.getStatusCode() == HttpURLConnection.HTTP_INTERNAL_ERROR
        || lase.getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
        return true;
      }

      if (isThrottlingException(lase)) return true;
    }

    return false;
  }

  /**
   * @return if the request get temporary redirect to another location.
   */
  private boolean isTemporaryRedirect(HttpURLConnection connection) {
    try {
      int status = connection.getResponseCode();
      return status == HTTP_TEMPORARY_REDIRECT
        && !Strings.isNullOrEmpty(connection.getHeaderField(HttpHeaders.LOCATION));
    } catch (IOException ioex) {
      return false;
    }
  }

  /**
   * @return if the request get successful.
   */
  private boolean isRequestSuccessful(HttpURLConnection connection) {
    try {
      int status = connection.getResponseCode();
      return status / 100 == HttpURLConnection.HTTP_OK / 100;
    } catch (IOException ioex) {
      return false;
    }
  }

  /**
   * Responsible for handling an error response.
   *
   * @param request              The request that generated the error response being handled.
   * @param errorResponseHandler The response handler responsible for unmarshalling
   *                             the error response.
   * @param connection           the HTTP connection containing the actual response content.
   * @return the error information.
   * @throws IOException If any problems are encountering reading the error response.
   */
  private AuthServiceException handleErrorResponse(HttpRequest request,
                                                   HttpResponseHandler errorResponseHandler,
                                                   HttpURLConnection connection) throws IOException {
    if (errorResponseHandler == null) {
      throw new AuthClientException("Unable to handle the response from server.");
    }

    int statusCode = connection.getResponseCode();
    printResponseError(request, connection);
    HttpResponse response = createResponse(connection, request);

    AuthServiceException exception;
    try {
      exception = errorResponseHandler.handle(response);
    } catch (Exception ex) {
      if (statusCode == 413) {
        exception = new AuthServiceException("Request entity too large")
          .withErrorType(AuthServiceException.ErrorType.Client)
          .withErrorCode("Request entity too large");
      } else if (statusCode == 503 && "Service Unavailable".equalsIgnoreCase(response.getStatus())) {
        exception = new AuthServiceException("Service unavailable")
          .withErrorType(AuthServiceException.ErrorType.Service)
          .withErrorCode("Service unavailable");
      } else {
        throw new AuthClientException("Unable to parse error response (" + ex.getMessage() + ")", ex);
      }
    }

    // fill the status code.
    exception.withStatusCode(statusCode)
      .withServiceName(request.getServiceName());
    exception.fillInStackTrace();

    return exception;
  }

  private void printResponseError(HttpRequest request, HttpURLConnection connection) throws IOException {
    StringBuilder builder = new StringBuilder("Request to: " + request.getEndpoint() + request.getResourcePath() +
      ", code: " + connection.getResponseCode() + ", headers: ");
    List ignoredHeaders = Arrays.asList("Transfer-Encoding", "Server", "Access-Control-Allow-Origin",
      "Access-Control-Allow-Methods", "Pragma", "Access-Control-Allow-Headers", "Cache-Control", "Access-Control-Allow-Credentials",
      "X-XSS-Protection", "Access-Control-Max-Age", "Connection");
    for (Map.Entry> entry : connection.getHeaderFields().entrySet()) {
      if (ignoredHeaders.contains(entry.getKey())) {
        continue;
      }
      builder.append(entry + ",");
    }
    LOG.log(Level.WARNING, builder.toString());
  }

  /**
   * Handle the successful response from a service call by unmarshalling the results
   * using the specified response handler.
   */
  private  T handleResponse(HttpRequest request,
                               HttpResponseHandler> responseHandler,
                               HttpURLConnection connection,
                               ExecutionContext context) throws IOException {
    // create the response.
    HttpResponse response = createResponse(connection, request);

    try {
      ApiServiceResponse lbResponse = responseHandler.handle(response);
      if (lbResponse == null) {
        throw new AuthClientException("Unable to parse response metadata, path: " + request.getResourcePath());
      }

      // show the information.
      if (LOG.isLoggable(Level.INFO)) {
        LOG.info("Received successful response: " + response.getStatusCode());
      }

      return lbResponse.getResult();
    } catch (Exception ex) {
      throw new AuthClientException("Unable to parse response, path: " + request.getResourcePath() + ", error: " + ex.getMessage(), ex);
    } finally {
      response.close();
    }
  }

  /**
   * Creates and initializes an HttpResponse object suitable to be parsed to an
   * HTTP response handler object.
   *
   * @param connection the HTTP connection that was invoked to get the response.
   * @param request    the HTTP request associated with the response.
   * @return The new, initialized HttpResponse object ready to be passed to an
   * HTTP response handler object.
   */
  private HttpResponse createResponse(HttpURLConnection connection,
                                                         HttpRequest request)
    throws IOException {
    return new HttpUrlConnectionResponse(connection, request);
  }

  /**
   * Resets the specified request, so that it can be sent again, after receiving
   * the specified error. If a problem is encountered with resetting the request,
   * the AuthClientException is thrown with the original error as the cause.
   *
   * @param request The request being executed that failed and needs to be reset.
   * @param cause   The original error that caused the request to fail.
   * @throws AuthClientException if the request can't be reset.
   */
  private void resetRequestAfterError(Request request, Exception cause)
    throws AuthClientException {
    if (request.getContent() == null) return;

    if (!request.getContent().markSupported()) {
      throw new AuthClientException("Encountered an exception and stream is not resettable", cause);
    }

    try {
      request.getContent().reset();
    } catch (IOException ioex) {
      throw new AuthClientException("Encountered an exception and couldn't reset the stream to retry", cause);
    }
  }

  private void pauseExponentially(int retries, AuthServiceException previousException, CustomBackoffStrategy backoffStrategy) {
    long delay = 0;
    if (backoffStrategy != null) {
      delay = backoffStrategy.getBackoffPeriod(retries);
    } else {
      long scaleFactor = 300;
      if (isThrottlingException(previousException)) {
        scaleFactor = 500 + random.nextInt(100);
      }
      delay = (long) (Math.pow(2, retries) * scaleFactor);
    }

    delay = Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS);
    if (LOG.isLoggable(Level.FINE)) {
      LOG.log(Level.FINE, "Retriable error detected, will retry in " + delay + "ms, attempt number: " + retries);
    }

    try {
      Thread.sleep(delay);
    } catch (InterruptedException ex) {
      Thread.currentThread().interrupt();
      throw new AuthClientException(ex.getMessage(), ex);
    }
  }

  public static boolean isThrottlingException(AuthServiceException ase) {
    if (ase == null) return false;
    return "Throttling".equals(ase.getErrorCode())
      || "ThrottlingException".equals(ase.getErrorCode());
  }

  public static boolean isRequestEntityTooLargeException(AuthServiceException ase) {
    if (ase == null) return false;
    return "Request entity too large".equals(ase.getErrorCode());
  }


  @Override
  public void shutdown() {
  }

  @Override
  protected void finalize() throws Throwable {
    this.shutdown();
    super.finalize();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy