com.smartsheet.api.internal.http.AndroidHttpClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of smartsheet-sdk-java Show documentation
Show all versions of smartsheet-sdk-java Show documentation
Library for connecting to Smartsheet Services
/*
* Copyright (C) 2023 Smartsheet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.smartsheet.api.internal.http;
import com.smartsheet.api.internal.json.JacksonJsonSerializer;
import com.smartsheet.api.internal.json.JsonSerializer;
import com.smartsheet.api.internal.util.StreamUtil;
import com.smartsheet.api.internal.util.Util;
import com.smartsheet.api.models.Error;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class AndroidHttpClient implements HttpClient {
/** logger for general errors, warnings, etc */
protected static final Logger logger = LoggerFactory.getLogger(AndroidHttpClient.class);
private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json");
private static final String ERROR_OCCURRED = "Error occurred.";
/**
* Represents the underlying OkHttpClient.
*
* It will be initialized in constructor and will not change afterwards.
*
*/
private final OkHttpClient client;
/** The okhttp http response. */
private Response currentResponse;
protected JsonSerializer jsonSerializer;
protected long maxRetryTimeMillis = 15000;
/**
* Constructor.
*/
public AndroidHttpClient() {
this.client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
this.jsonSerializer = new JacksonJsonSerializer();
}
/**
* Log to the SLF4J logger (level based upon response status code). Override this function to add logging
* or capture performance metrics.
*
* @param request request
* @param response response
* @param durationMillis response time in ms
*/
public void logRequest(Request request, Response response, long durationMillis) {
logger.info("{} {}, Response Code:{}, Request completed in {} ms", request.method(), request.url(),
response.code(), durationMillis);
if (response.code() != 200) {
// log the request and response on error
try {
logger.warn(this.currentResponse.peekBody(4096).string());
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Make an HTTP request and return the response.
*
* @param smartsheetRequest the smartsheet request
* @return the HTTP response
* @throws HttpClientException the HTTP client exception
*/
@Override
public HttpResponse request(HttpRequest smartsheetRequest) throws HttpClientException {
Util.throwIfNull(smartsheetRequest);
if (smartsheetRequest.getUri() == null) {
throw new IllegalArgumentException("A Request URI is required.");
}
int attempt = 0;
long start = System.currentTimeMillis();
InputStream bodyStream = null;
if (smartsheetRequest.getEntity() != null && smartsheetRequest.getEntity().getContent() != null) {
bodyStream = smartsheetRequest.getEntity().getContent();
}
// the retry logic will consume the body stream so we make sure it supports mark/reset and mark it
boolean canRetryRequest = bodyStream == null || bodyStream.markSupported();
if (!canRetryRequest) {
try {
// attempt to wrap the body stream in a input-stream that does support mark/reset
bodyStream = new ByteArrayInputStream(StreamUtil.readBytesFromStream(bodyStream));
// close the old stream (just to be tidy) and then replace it with a reset-able stream
smartsheetRequest.getEntity().getContent().close();
smartsheetRequest.getEntity().setContent(bodyStream);
canRetryRequest = true;
} catch (IOException ignore) {
}
}
HttpResponse smartsheetResponse;
while (true) {
// Create our new request
Request.Builder builder = new Request.Builder();
try {
builder.url(smartsheetRequest.getUri().toURL());
} catch (MalformedURLException e) {
throw new HttpClientException(ERROR_OCCURRED, e);
}
// Clone our headers to request
for (Map.Entry entry : smartsheetRequest.getHeaders().entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
try {
switch (smartsheetRequest.getMethod()) {
case GET:
builder.get();
break;
case POST:
builder.post(getRequestBody(smartsheetRequest));
break;
case PUT:
builder.put(getRequestBody(smartsheetRequest));
break;
case DELETE:
builder.delete();
break;
default:
// This switch is exhaustive, but the checkstyle doesn't know that
throw new UnsupportedOperationException("Unsupported method: " + smartsheetRequest.getMethod());
}
} catch (IOException e) {
throw new HttpClientException(ERROR_OCCURRED, e);
}
// mark the body so we can reset on retry
if (canRetryRequest && bodyStream != null) {
bodyStream.mark((int) smartsheetRequest.getEntity().getContentLength());
}
try {
// Create API request
Request request = builder.build();
long startTime = System.currentTimeMillis();
this.currentResponse = client.newCall(request).execute();
long endTime = System.currentTimeMillis();
smartsheetResponse = new HttpResponse();
smartsheetResponse.setStatusCode(this.currentResponse.code());
if (this.currentResponse.body().contentLength() != 0) {
// Package response details
HttpEntity entity = new HttpEntity();
entity.setContentType(this.currentResponse.body().contentType().toString());
entity.setContentLength(this.currentResponse.body().contentLength());
entity.setContent(this.currentResponse.body().byteStream());
smartsheetResponse.setEntity(entity);
}
long responseTime = endTime - startTime;
logRequest(request, this.currentResponse, responseTime);
if (smartsheetResponse.getStatusCode() == 200) {
// call successful, exit the retry loop
break;
}
// the retry logic might consume the content stream so we make sure it supports mark/reset and mark it
InputStream contentStream = smartsheetResponse.getEntity().getContent();
if (!contentStream.markSupported()) {
// wrap the response stream in a input-stream that does support mark/reset
contentStream = new ByteArrayInputStream(StreamUtil.readBytesFromStream(contentStream));
// close the old stream (just to be tidy) and then replace it with a reset-able stream
smartsheetResponse.getEntity().getContent().close();
smartsheetResponse.getEntity().setContent(contentStream);
}
try {
contentStream.mark((int) smartsheetResponse.getEntity().getContentLength());
long timeSpent = System.currentTimeMillis() - start;
if (!shouldRetry(++attempt, timeSpent, smartsheetResponse)) {
// should not retry, or retry time exceeded, exit the retry loop
break;
}
} finally {
if (bodyStream != null) {
bodyStream.reset();
}
contentStream.reset();
}
this.releaseConnection();
} catch (IOException ex) {
throw new HttpClientException(ERROR_OCCURRED, ex);
}
}
return smartsheetResponse;
}
private RequestBody getRequestBody(HttpRequest apiRequest) throws IOException {
int sizRead;
byte[] buffer = new byte[16384];
ByteArrayOutputStream bao = new ByteArrayOutputStream();
while ((sizRead = apiRequest.getEntity().getContent().read(buffer, 0, buffer.length)) != -1) {
bao.write(buffer, 0, sizRead);
}
return RequestBody.create(MEDIA_TYPE_JSON, bao.toByteArray());
}
/**
* Set the max retry time for API calls which fail and are retry-able.
*/
public void setMaxRetryTimeMillis(long maxRetryTimeMillis) {
this.maxRetryTimeMillis = maxRetryTimeMillis;
}
/**
* The backoff calculation routine. Uses exponential backoff. If the maximum elapsed time
* has expired, this calculation returns -1 causing the caller to fall out of the retry loop.
* @return -1 to fall out of retry loop, positive number indicates backoff time
*/
public long calcBackoff(int previousAttempts, long totalElapsedTimeMillis, Error error) {
long backoffMillis = (long) (Math.pow(2, previousAttempts) * 1000) + new Random().nextInt(1000);
if (totalElapsedTimeMillis + backoffMillis > maxRetryTimeMillis) {
logger.info("Elapsed time " + totalElapsedTimeMillis + " + backoff time " + backoffMillis +
" exceeds max retry time " + maxRetryTimeMillis + ", exiting retry loop");
return -1;
}
return backoffMillis;
}
/**
* Called when an API request fails to determine if it can retry the request.
* Calls calcBackoff to determine the time to wait in between retries.
*
* @param previousAttempts number of attempts (including this one) to execute request
* @param totalElapsedTimeMillis total time spent in millis for all previous (and this) attempt
* @param response the failed HttpResponse
* @return true if this request can be retried
*/
public boolean shouldRetry(int previousAttempts, long totalElapsedTimeMillis, HttpResponse response) {
String contentType = response.getEntity().getContentType();
if (contentType != null && !contentType.startsWith("application/json")) {
// it's not JSON; don't even try to parse it
return false;
}
Error error;
try {
error = jsonSerializer.deserialize(Error.class, response.getEntity().getContent());
} catch (IOException e) {
return false;
}
switch (error.getErrorCode()) {
// Smartsheet.com is currently offline for system maintenance. Please check back again shortly.
case 4001:
// Server timeout exceeded. Request has failed
case 4002:
// Rate limit exceeded.
case 4003:
// An unexpected error has occurred. Please retry your request.
// If you encounter this error repeatedly, please contact [email protected] for assistance.
case 4004:
break;
default:
return false;
}
long backoffMillis = calcBackoff(previousAttempts, totalElapsedTimeMillis, error);
if (backoffMillis < 0) {
return false;
}
logger.info("HttpError StatusCode=" + response.getStatusCode() + ": Retrying in " + backoffMillis + " milliseconds");
try {
Thread.sleep(backoffMillis);
} catch (InterruptedException e) {
logger.warn("sleep interrupted", e);
return false;
}
return true;
}
/**
* Close the HttpClient.
*/
@Override
public void close() {
this.client.connectionPool().evictAll();
}
/* (non-Javadoc)
* @see com.smartsheet.api.internal.http.HttpClient#releaseConnection()
*/
@Override
public void releaseConnection() {
this.closeCurrentResponse();
}
private void closeCurrentResponse() {
Response response = this.currentResponse;
if (response != null) {
if (response.body() != null) {
response.body().close();
}
this.currentResponse = null;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy