org.qas.api.http.basic.HttpUrlConnectionAuthClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of qtest-sdk-java Show documentation
Show all versions of qtest-sdk-java Show documentation
A java SDK client wrap qTest REST API
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 extends T> 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