com.datastax.stargate.sdk.utils.HttpApisClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of stargate-sdk Show documentation
Show all versions of stargate-sdk Show documentation
Connect to Stargate Data Gateway
package com.datastax.stargate.sdk.utils;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.client5.http.classic.methods.HttpPatch;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpTrace;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures;
import com.datastax.stargate.sdk.audit.ApiInvocationEvent;
import com.datastax.stargate.sdk.audit.ApiInvocationObserver;
import com.datastax.stargate.sdk.core.ApiConstants;
import com.datastax.stargate.sdk.core.ApiResponseHttp;
import com.datastax.stargate.sdk.exception.AuthenticationException;
import com.datastax.stargate.sdk.loadbalancer.UnavailableResourceException;
import com.evanlennick.retry4j.CallExecutorBuilder;
import com.evanlennick.retry4j.Status;
import com.evanlennick.retry4j.config.RetryConfig;
import com.evanlennick.retry4j.config.RetryConfigBuilder;
/**
* Wrapping the HttpClient and provide helpers
*
* @author Cedrick LUNVEN (@clunven)
*/
public class HttpApisClient implements ApiConstants {
/** Logger for our Client. */
private static final Logger LOGGER = LoggerFactory.getLogger(HttpApisClient.class);
/** Default settings in Request and Retry */
public static final int DEFAULT_TIMEOUT_REQUEST = 20;
/** Default settings in Request and Retry */
public static final int DEFAULT_TIMEOUT_CONNECT = 20;
/** Default settings in Request and Retry */
public static final int DEFAULT_RETRY_COUNT = 3;
/** Default settings in Request and Retry */
public static final Duration DEFAULT_RETRY_DELAY = Duration.ofMillis(100);
// -------------------------------------------
// ---------------- Settings --------------
// -------------------------------------------
/** Singleton pattern. */
private static HttpApisClient _instance = null;
/** HttpComponent5. */
protected CloseableHttpClient httpClient = null;
/** Observers. */
protected static Map apiInvocationsObserversMap = new ConcurrentHashMap<>();
/** Default request configuration. */
protected static RequestConfig requestConfig = RequestConfig.custom()
.setCookieSpec(StandardCookieSpec.STRICT)
.setExpectContinueEnabled(true)
.setConnectionRequestTimeout(Timeout.ofSeconds(DEFAULT_TIMEOUT_REQUEST))
.setConnectTimeout(Timeout.ofSeconds(DEFAULT_TIMEOUT_CONNECT))
.setTargetPreferredAuthSchemes(Arrays.asList(StandardAuthScheme.NTLM, StandardAuthScheme.DIGEST))
.build();
/** Default retry configuration. */
protected static RetryConfig retryConfig = new RetryConfigBuilder()
//.retryOnSpecificExceptions(ConnectException.class, IOException.class)
.retryOnAnyException()
.withDelayBetweenTries(DEFAULT_RETRY_DELAY)
.withExponentialBackoff()
.withMaxNumberOfTries(DEFAULT_RETRY_COUNT)
.build();
/**
* Update Retry configuration of the HTTPClient.
*
* @param conf
* retryConfiguration
*/
public static void withRetryConfig(RetryConfig conf) {
retryConfig= conf;
}
/**
* Update RequestConfig configuration of the HTTPClient.
*
* @param conf
* RequestConfig
*/
public static void withRequestConfig(RequestConfig conf) {
requestConfig = conf;
}
/**
* Register a new listener.
*
* @param name
* current name
* @param listener
* current listener
*/
public static void registerListener(String name, ApiInvocationObserver listener) {
apiInvocationsObserversMap.put(name, listener);
}
// -------------------------------------------
// ----------------- Singleton ---------------
// -------------------------------------------
/**
* Hide default constructor
*/
private HttpApisClient() {}
/**
* Singleton Pattern.
*
* @return
* singleton for the class
*/
public static synchronized HttpApisClient getInstance() {
if (_instance == null) {
_instance = new HttpApisClient();
final PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
connManager.setValidateAfterInactivity(TimeValue.ofSeconds(10));
connManager.setMaxTotal(100);
connManager.setDefaultMaxPerRoute(10);
_instance.httpClient = HttpClients.custom().setConnectionManager(connManager).build();
}
return _instance;
}
// -------------------------------------------
// ---------- Working with HTTP --------------
// -------------------------------------------
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @return
* http request
*/
public ApiResponseHttp GET(String url, String token) {
return executeHttp(Method.GET, url, token, null, CONTENT_TYPE_JSON, false);
}
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @return
* http request
*/
public ApiResponseHttp HEAD(String url, String token) {
return executeHttp(Method.HEAD, url, token, null, CONTENT_TYPE_JSON, false);
}
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @return
* http request
*/
public ApiResponseHttp POST(String url, String token) {
return executeHttp(Method.POST, url, token, null, CONTENT_TYPE_JSON, true);
}
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @param body
* request body
* @return
* http request
*/
public ApiResponseHttp POST(String url, String token, String body) {
return executeHttp(Method.POST, url, token, body, CONTENT_TYPE_JSON, true);
}
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @param body
* request body
* @return
* http request
*/
public ApiResponseHttp POST_GRAPHQL(String url, String token, String body) {
return executeHttp(Method.POST, url, token, body, CONTENT_TYPE_GRAPHQL, true);
}
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @return
* http request
*/
public ApiResponseHttp DELETE(String url, String token) {
return executeHttp(Method.DELETE, url, token, null, CONTENT_TYPE_JSON, true);
}
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @param body
* request body
* @return
* http request
*/
public ApiResponseHttp PUT(String url, String token, String body) {
return executeHttp(Method.PUT, url, token, body, CONTENT_TYPE_JSON, false);
}
/**
* Helper to build the HTTP request.
*
* @param url
* target url
* @param token
* authentication token
* @param body
* request body
* @return
* http request
*/
public ApiResponseHttp PATCH(String url, String token, String body) {
return executeHttp(Method.PATCH, url, token, body, CONTENT_TYPE_JSON, true);
}
/**
* Main Method executting HTTP Request.
*
* @param method
* http method
* @param url
* url
* @param token
* authentication token
* @param contentType
* request content type
* @param reqBody
* request body
* @param mandatory
* allow 404 errors
* @return
* basic request
*/
public ApiResponseHttp executeHttp(final Method method, final String url, final String token, String reqBody, String contentType, boolean mandatory) {
return executeHttp(buildRequest(method, url, token, reqBody, contentType), mandatory);
}
/**
* Execute a request coming from elsewhere.
*
* @param req
* current request
* @param mandatory
* mandatory
* @return
* api response
*/
public ApiResponseHttp executeHttp(HttpUriRequestBase req, boolean mandatory) {
// Initializing the invocation event
ApiInvocationEvent event = new ApiInvocationEvent(req);
// Invoking the expected endpoint
Status status = executeWithRetries(req);
try {
// Parsing result as expected bean
ApiResponseHttp res = mapResponse(status, event);
// Error managment
if (HttpURLConnection.HTTP_NOT_FOUND == res.getCode() && !mandatory) {
return res;
}
if (res.getCode() >= 300) {
LOGGER.error("Error for request [{}], url={}, method={}, code={}, body={}",
event.getRequestId(),
req.getUri().toString(), req.getMethod(),
res.getCode(), res.getBody());
processErrors(res, mandatory);
}
return res;
} catch (UnavailableResourceException e) {
event.setErrorClass(e.getClass().getName());
event.setErrorMessage(e.getMessage());
throw e;
} catch (IllegalArgumentException e) {
event.setErrorClass(e.getClass().getName());
event.setErrorMessage(e.getMessage());
throw e;
} catch (Exception e) {
e.printStackTrace();
event.setErrorClass(e.getClass().getName());
event.setErrorMessage(e.getMessage());
throw new RuntimeException("Error in HTTP Request", e);
} finally {
CompletableFuture.runAsync(()-> notifyAsync(listener->listener.onCall(event)));
}
}
/**
* Mapping HTTP Response to framework HTTP BEAN.
*
* @param status
* current result of the retries
* @param event
* event to be sent
* @return
* bean populated
* @throws ParseException
* error in parsing
* @throws IOException
* error in accessing payload
*/
private ApiResponseHttp mapResponse(Status status, ApiInvocationEvent event)
throws ParseException, IOException {
// Evaluate output
ApiResponseHttp res = null;
event.setTotalTries(status.getTotalTries());
event.setLastException(status.getLastExceptionThatCausedRetry());
event.setResponseElapsedTime(status.getTotalElapsedDuration().toMillis());
try (CloseableHttpResponse response = status.getResult()) {
event.setResponseTimestamp(status.getEndTime());
if (response == null) {
event.setResponseCode(HttpURLConnection.HTTP_UNAVAILABLE);
res = new ApiResponseHttp("Response is empty, cannot contact endpoint, please check url",
HttpURLConnection.HTTP_UNAVAILABLE, null);
} else {
event.setResponseCode(response.getCode());
Map headers = new HashMap<>();
Arrays.asList(response.getHeaders())
.stream()
.forEach(h -> headers.put(h.getName(), h.getValue()));
event.setResponseHeaders(headers);
// Parse body if present
String body = null;
if (null != response.getEntity()) {
body = EntityUtils.toString(response.getEntity());
EntityUtils.consume(response.getEntity());
}
event.setResponseBody(body);
// Mapping respoonse
res = new ApiResponseHttp(body, response.getCode(), headers);
}
}
return res;
}
/**
* Asynchronously send calls to listener for tracing.
*
* @param lambda
* operations to execute
* @return
* void
*/
private CompletionStage notifyAsync(Consumer lambda) {
return CompletableFutures.allDone(apiInvocationsObserversMap.values().stream()
.map(l -> CompletableFuture.runAsync(() -> lambda.accept(l)))
.collect(Collectors.toList()));
}
/**
* Initialize an HTTP request against Stargate.
*
* @param method
* http Method
* @param url
* target URL
* @param token
* current token
* @return
* default http with header
*/
private HttpUriRequestBase buildRequest(final Method method, final String url, final String token, String body, String contentType) {
HttpUriRequestBase req;
switch(method) {
case GET: req = new HttpGet(url); break;
case POST: req = new HttpPost(url); break;
case PUT: req = new HttpPut(url); break;
case DELETE: req = new HttpDelete(url); break;
case PATCH: req = new HttpPatch(url); break;
case HEAD: req = new HttpHead(url); break;
case TRACE: req = new HttpTrace(url); break;
case OPTIONS:
case CONNECT:
default:throw new IllegalArgumentException("Invalid HTTP Method");
}
req.addHeader(HEADER_CONTENT_TYPE, contentType);
req.addHeader(HEADER_ACCEPT, CONTENT_TYPE_JSON);
req.addHeader(HEADER_USER_AGENT, REQUEST_WITH);
req.addHeader(HEADER_REQUEST_ID, UUID.randomUUID().toString());
req.addHeader(HEADER_REQUESTED_WITH, REQUEST_WITH);
req.addHeader(HEADER_CASSANDRA, token);
req.addHeader(HEADER_AUTHORIZATION, "Bearer " + token);
if (null != body) {
req.setEntity(new StringEntity(body, ContentType.TEXT_PLAIN));
}
return req;
}
/**
* Implementing retries.
*
* @param req
* current request
* @return
* the closeable response
*/
@SuppressWarnings("unchecked")
private Status executeWithRetries(ClassicHttpRequest req) {
Callable executeRequest = () -> {
return httpClient.execute(req);
};
return new CallExecutorBuilder()
.config(retryConfig)
.onSuccessListener(s -> {
CompletableFuture.runAsync(()-> notifyAsync(listener->listener.onHttpSuccess(s)));
})
.onCompletionListener(s -> {
CompletableFuture.runAsync(()-> notifyAsync(listener->listener.onHttpCompletion(s)));
})
.onFailureListener(s -> {
LOGGER.error("Calls failed after {} retries", s.getTotalTries());
CompletableFuture.runAsync(()-> notifyAsync(listener->listener.onHttpFailure(s)));
})
.afterFailedTryListener(s -> {
LOGGER.error("Failure on attempt {}/{} ", s.getTotalTries(), retryConfig.getMaxNumberOfTries());
try {
LOGGER.error("Failed request {} on {}{}", req.getMethod() , req.getUri(), req.getRequestUri() );
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
CompletableFuture.runAsync(()-> notifyAsync(listener->listener.onHttpFailedTry(s)));
})
.build()
.execute(executeRequest);
}
/**
* Process ERRORS.Anything above code 300 can be marked as an error Still something
* 404 is expected and should not result in throwing expection (=not find)
* @param res HttpResponse
*/
private void processErrors(ApiResponseHttp res, boolean mandatory) {
switch(res.getCode()) {
// 400
case HttpURLConnection.HTTP_BAD_REQUEST:
throw new IllegalArgumentException("Error Code=" + res.getCode() +
" (HTTP_BAD_REQUEST) Invalid Parameters: "
+ res.getBody());
// 401
case HttpURLConnection.HTTP_UNAUTHORIZED:
throw new AuthenticationException("Error Code=" + res.getCode() +
", (HTTP_UNAUTHORIZED) Invalid Credentials Check your token: " +
res.getBody());
// 403
case HttpURLConnection.HTTP_FORBIDDEN:
throw new AuthenticationException("Error Code=" + res.getCode() +
", (HTTP_FORBIDDEN) Invalid permissions, check your token: " +
res.getBody());
// 404
case HttpURLConnection.HTTP_NOT_FOUND:
if (mandatory) {
throw new IllegalArgumentException("Error Code=" + res.getCode() +
"(HTTP_NOT_FOUND) Object not found: "
+ res.getBody());
}
break;
// 409
case HttpURLConnection.HTTP_CONFLICT:
throw new AuthenticationException("Error Code=" + res.getCode() +
", (HTTP_CONFLICT) Object may alreayd exist with same identifiers: " +
res.getBody());
case 422:
throw new IllegalArgumentException("Error Code=" + res.getCode() +
"(422) Invalid information provided to create DB: "
+ res.getBody());
default:
if (res.getCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
throw new UnavailableResourceException(res.getBody() + " (http:" + res.getCode() + ")");
}
throw new RuntimeException(res.getBody() + " (http:" + res.getCode() + ")");
}
}
/**
* Getter accessor for attribute 'requestConfig'.
*
* @return
* current value of 'requestConfig'
*/
public static RequestConfig getRequestConfig() {
return requestConfig;
}
/**
* Getter accessor for attribute 'retryConfig'.
*
* @return
* current value of 'retryConfig'
*/
public static RetryConfig getRetryConfig() {
return retryConfig;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy