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

cloud.genesys.webmessaging.sdk.ApiClient Maven / Gradle / Ivy

package cloud.genesys.webmessaging.sdk;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.net.Proxy;
import java.text.DateFormat;
import java.time.LocalDate;
import java.util.*;
import java.util.Map.Entry;

import com.google.common.base.Stopwatch;
import java.util.concurrent.TimeUnit;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cloud.genesys.webmessaging.sdk.auth.Authentication;
import cloud.genesys.webmessaging.sdk.auth.OAuth;
import cloud.genesys.webmessaging.sdk.connector.*;


public class ApiClient implements AutoCloseable {
    private static final String DEFAULT_BASE_PATH = "https://api.mypurecloud.com";
    private static final String DEFAULT_USER_AGENT = "PureCloud SDK/webmessagingjava";
    private static final String USER_AGENT_HEADER = "User-Agent";

    private static Map buildAuthentications() {
        Map authentications = new HashMap<>();

        return Collections.unmodifiableMap(authentications);
    }

    private final Map defaultHeaderMap;
    private final String basePath;
    private final Boolean shouldThrowErrors;

    private final DateFormat dateFormat;
    private final ObjectMapper objectMapper;

    private final ConnectorProperties properties;

    private final Map authentications;
    private final ApiClientConnector connector;

    private final RetryConfiguration retryConfiguration;
    private static final RetryConfiguration DEFAULT_RETRY_CONFIG = new RetryConfiguration();

    public ApiClient() {
        this(Builder.standard());
    }

    private ApiClient(Builder builder) {
        String basePath = builder.basePath;
        if (basePath == null) {
            basePath = DEFAULT_BASE_PATH;
        }
        this.basePath = basePath;

        RetryConfiguration retryConfig = builder.retryConfiguration;
        if (retryConfig == null) {
            retryConfig = DEFAULT_RETRY_CONFIG;
        }
        this.retryConfiguration = retryConfig;

        this.defaultHeaderMap = new HashMap<>(builder.defaultHeaderMap);
        this.properties = builder.properties.copy();
        this.shouldThrowErrors = builder.shouldThrowErrors == null ? true : builder.shouldThrowErrors;

        DateFormat dateFormat = builder.dateFormat;
        if (dateFormat == null) {
            dateFormat = buildDateFormat();
        }
        this.dateFormat = dateFormat;

        ObjectMapper objectMapper = builder.objectMapper;
        if (objectMapper == null) {
            objectMapper = buildObjectMapper(dateFormat);
        }
        this.objectMapper = objectMapper;

        this.authentications = buildAuthentications(builder);

        this.connector = buildHttpConnector(builder);
    }

    @Override
    public void close() throws Exception {
        connector.close();
    }

    public static ObjectMapper buildObjectMapper(DateFormat dateFormat) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
        objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
        objectMapper.registerModule(new JodaModule());
        objectMapper.setDateFormat(dateFormat);
        SimpleModule localDateModule = new SimpleModule();
        localDateModule.addSerializer(LocalDate.class, new LocalDateSerializer());
        localDateModule.addDeserializer(LocalDate.class, new LocalDateDeserializer());
        objectMapper.registerModule(localDateModule);
        return objectMapper;
    }

    private DateFormat buildDateFormat() {
        return new ApiDateFormat();
    }

    private Map buildAuthentications(Builder builder) {
        Map authentications = buildAuthentications();
        String accessToken = builder.accessToken;
        for (Authentication authentication : authentications.values()) {
            if (authentication instanceof OAuth && accessToken != null) {
                ((OAuth)authentication).setAccessToken(accessToken);
            }
        }
        return authentications;
    }

    private ApiClientConnector buildHttpConnector(Builder builder) {
        return ApiClientConnectorLoader.load(properties);
    }

    public boolean getShouldThrowErrors() {
        return shouldThrowErrors;
    }

    public String getBasePath() {
        return basePath;
    }

    public ObjectMapper getObjectMapper() {
        return this.objectMapper;
    }

    /**
     * Helper method to set access token for the first OAuth2 authentication.
     */
    public void setAccessToken(String accessToken) {
        for (Authentication auth : authentications.values()) {
            if (auth instanceof OAuth) {
                ((OAuth) auth).setAccessToken(accessToken);
                return;
            }
        }
        throw new RuntimeException("No OAuth2 authentication configured!");
    }

    /**
     * Connect timeout (in milliseconds).
     */
    public int getConnectTimeout() {
        return properties.getProperty(ApiClientConnectorProperty.CONNECTION_TIMEOUT, Integer.class, 0);
    }

    /**
     * Parse the given string into Date object.
     */
    public Date parseDate(String str) {
        try {
            synchronized (dateFormat) {
                return dateFormat.parse(str);
            }
        } catch (java.text.ParseException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Format the given Date object into string.
     */
    public String formatDate(Date date) {
        synchronized (dateFormat) {
            return dateFormat.format(date);
        }
    }

    /**
     * Format the given parameter object into string.
     */
    public String parameterToString(Object param) {
        if (param == null) {
            return "";
        } else if (param instanceof Date) {
            return formatDate((Date) param);
        } else if (param instanceof Collection) {
            StringBuilder b = new StringBuilder();
            for(Object o : (Collection)param) {
                if(b.length() > 0) {
                    b.append(",");
                }
                b.append(String.valueOf(o));
            }
            return b.toString();
        } else {
            return String.valueOf(param);
        }
    }

    /*
      Format to {@code Pair} objects.
    */
    public List parameterToPairs(String collectionFormat, String name, Object value){
        List params = new ArrayList();

        // preconditions
        if (name == null || name.isEmpty() || value == null) return params;

        Collection valueCollection = null;
        if (value instanceof Collection) {
            valueCollection = (Collection) value;
        } else {
            params.add(new Pair(name, parameterToString(value)));
            return params;
        }

        if (valueCollection.isEmpty()){
            return params;
        }

        // get the collection format
        collectionFormat = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat); // default: csv

        // create the params based on the collection format
        if (collectionFormat.equals("multi")) {
            for (Object item : valueCollection) {
                params.add(new Pair(name, parameterToString(item)));
            }

            return params;
        }

        String delimiter = ",";

        if (collectionFormat.equals("csv")) {
            delimiter = ",";
        } else if (collectionFormat.equals("ssv")) {
            delimiter = " ";
        } else if (collectionFormat.equals("tsv")) {
            delimiter = "\t";
        } else if (collectionFormat.equals("pipes")) {
            delimiter = "|";
        }

        StringBuilder sb = new StringBuilder() ;
        for (Object item : valueCollection) {
            sb.append(delimiter);
            sb.append(parameterToString(item));
        }

        params.add(new Pair(name, sb.substring(1)));

        return params;
    }

    /**
     * Check if the given MIME is a JSON MIME.
     * JSON MIME examples:
     *   application/json
     *   application/json; charset=UTF8
     *   APPLICATION/JSON
     */
    public boolean isJsonMime(String mime) {
        return mime != null && mime.matches("(?i)application\\/json(;.*)?");
    }

    /**
     * Select the Accept header's value from the given accepts array:
     *   if JSON exists in the given array, use it;
     *   otherwise use all of them (joining into a string)
     *
     * @param accepts The accepts array to select from
     * @return The Accept header to use. If the given array is empty,
     *   null will be returned (not to set the Accept header explicitly).
     */
    public String selectHeaderAccept(String[] accepts) {
        if (accepts.length == 0) {
            return null;
        }
        for (String accept : accepts) {
            if (isJsonMime(accept)) {
                return accept;
            }
        }
        return StringUtil.join(accepts, ",");
    }

    /**
     * Select the Content-Type header's value from the given array:
     *   if JSON exists in the given array, use it;
     *   otherwise use the first one of the array.
     *
     * @param contentTypes The Content-Type array to select from
     * @return The Content-Type header to use. If the given array is empty,
     *   JSON will be used.
     */
    public String selectHeaderContentType(String[] contentTypes) {
        if (contentTypes.length == 0) {
            return "application/json";
        }
        for (String contentType : contentTypes) {
            if (isJsonMime(contentType)) {
                return contentType;
            }
        }
        return contentTypes[0];
    }

    /**
     * Escape the given string to be used as URL query value.
     */
    public String escapeString(String str) {
        try {
            return URLEncoder.encode(str, "utf8").replaceAll("\\+", "%20");
        } catch (UnsupportedEncodingException e) {
            return str;
        }
    }

/**
     * Serialize the given Java object into string according the given
     * Content-Type (only JSON is supported for now).
     */
    public String serialize(Object obj) throws IOException {
        return objectMapper.writeValueAsString(obj);
    }

    /**
     * Deserialize the string into the provided type
     * 
     * @param obj the string to deserialize
     * @param type the target type for deserialization
     */
    public  T deserialize(String obj, Class type) throws IOException {
        return objectMapper.readValue(obj, type);
    }

    /**
     * Build full URL by concatenating base path, the given sub path and query parameters.
     *
     * @param path The sub path
     * @param queryParams The query parameters
     * @return The full URL
     */
    private String buildUrl(String path, Map pathParams, List queryParams, boolean isAuthRequest) {
        path = path.replaceAll("\\{format\\}", "json");
        if (pathParams != null && !pathParams.isEmpty()) {
            for (Map.Entry entry : pathParams.entrySet()) {
                path = path.replaceAll("\\{" + entry.getKey() + "\\}", entry.getValue());
            }
        }

        final StringBuilder url = new StringBuilder();
        if (isAuthRequest) {
            String[] parts = basePath.split("\\.", 2);
            url.append("https://login.").append(parts[1]).append(path);
        } else {
            url.append(basePath).append(path);
        }

        if (queryParams != null && !queryParams.isEmpty()) {
            // support (constant) query string in `path`, e.g. "/posts?draft=1"
            String prefix = path.contains("?") ? "&" : "?";
            for (Pair param : queryParams) {
                if (param.getValue() != null) {
                    if (prefix != null) {
                        url.append(prefix);
                        prefix = null;
                    } else {
                        url.append("&");
                    }
                    String value = parameterToString(param.getValue());
                    url.append(escapeString(param.getName())).append("=").append(escapeString(value));
                }
            }
        }

        return url.toString();
    }

    private ApiClientConnectorRequest prepareConnectorRequest(final ApiRequest request, boolean isAuthRequest) throws IOException {
        final String path = request.getPath();
        final List queryParams = new ArrayList<>(request.getQueryParams());

        // Add headers
        final Map headers = new HashMap<>();
        String accept = request.getAccepts();
        if (accept != null && !accept.isEmpty()) {
            headers.put("Accept", accept);
        }
        String contentType = request.getContentType();
        if (contentType != null && !contentType.isEmpty()) {
            headers.put("Content-Type", contentType);
        }
        Map headerParams = request.getHeaderParams();
        if (headerParams != null && !headerParams.isEmpty()) {
            for (Map.Entry headerParam : headerParams.entrySet()) {
                headers.put(headerParam.getKey(), headerParam.getValue());
            }
        }
        Map customHeaders = request.getCustomHeaders();
        if (customHeaders != null && !customHeaders.isEmpty()) {
            for (Map.Entry customHeader : customHeaders.entrySet()) {
                headers.put(customHeader.getKey(), customHeader.getValue());
            }
        }
        for (Map.Entry defaultHeader : defaultHeaderMap.entrySet()) {
            if (!headers.containsKey(defaultHeader.getKey()))
                headers.put(defaultHeader.getKey(), defaultHeader.getValue());
        }

        updateParamsForAuth(request.getAuthNames(), queryParams, headers);
        final String url = buildUrl(path, request.getPathParams(), queryParams, isAuthRequest);

        final Object body = request.getBody();
        final Map formParams = request.getFormParams();
        final String serializedBody;
        if (body != null && !formParams.isEmpty()) {
            throw new IllegalStateException("Request cannot have both form and body parameters.");
        }
        else if (body != null) {
            serializedBody = serialize(body);
        }
        else if (formParams != null) {
            serializedBody = getXWWWFormUrlencodedParams(formParams);
        }
        else {
            serializedBody = null;
        }

        return new ApiClientConnectorRequest() {
            @Override
            public String getMethod() {
                return request.getMethod();
            }

            @Override
            public String getUrl() {
                return url;
            }

            @Override
            public Map getHeaders() {
                return headers;
            }

            @Override
            public boolean hasBody() {
                return (serializedBody != null);
            }

            @Override
            public String readBody() throws IOException {
                return serializedBody;
            }

            @Override
            public InputStream getBody() throws IOException {
                return (serializedBody != null) ? new ByteArrayInputStream(serializedBody.getBytes("UTF8")) : null;
            }
        };
    }

    private  ApiResponse interpretConnectorResponse(ApiClientConnectorResponse response, TypeReference returnType) throws WebMessagingException, IOException {
        int statusCode = response.getStatusCode();
        String reasonPhrase = response.getStatusReasonPhrase();
        Map headers = response.getHeaders();

        if (statusCode >= 200 && statusCode < 300) {
            String body = null;
            T entity = null;
            if (statusCode != 204 && returnType != null && returnType.getType() != Void.class && response.hasBody()) {
                body = response.readBody();
                if (body != null && body.length() > 0 && returnType.getType() == String.class) {
                    entity = (T)body;
                } else if (body != null && body.length() > 0) {
                    entity = objectMapper.readValue(body, returnType);
                }
            }
            return new ApiResponseWrapper<>(statusCode, reasonPhrase, headers, body, entity);
        }
        else {
            String message = "error";
            String body = response.readBody();
            throw new WebMessagingException(statusCode, message, headers, body);
        }
    }

    private  ApiResponse getAPIResponse(ApiRequest request, TypeReference returnType, boolean isAuthRequest) throws IOException, WebMessagingException {
        ApiClientConnectorRequest connectorRequest = prepareConnectorRequest(request, isAuthRequest);
        ApiClientConnectorResponse connectorResponse = null;
        try {
            Retry retry = new Retry(retryConfiguration);
            do {
                connectorResponse = connector.invoke(connectorRequest);
            } while (retry.shouldRetry(connectorResponse));

            return interpretConnectorResponse(connectorResponse, returnType);

        } finally {
            if (connectorResponse != null) {
                try {
                    connectorResponse.close();
                } catch (Throwable exception) {
                    throw new RuntimeException(exception);
                }
            }
        }
    }

    public  ApiResponse invoke(ApiRequest request, TypeReference returnType) throws WebMessagingException, IOException {
        return getAPIResponse(request, returnType, false);
    }

    /**
     * Update query and header parameters based on authentication settings.
     *
     * @param authNames The authentications to apply
     */
    private void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams) {
        for (String authName : authNames) {
            Authentication auth = authentications.get(authName);
            if (auth == null) throw new RuntimeException("Authentication undefined: " + authName);
            auth.applyToParams(queryParams, headerParams);
        }
    }

    /**
     * Encode the given form parameters as request body.
     */
    private String getXWWWFormUrlencodedParams(Map formParams) {
        StringBuilder formParamBuilder = new StringBuilder();

        for (Entry param : formParams.entrySet()) {
            String valueStr = parameterToString(param.getValue());
            try {
                formParamBuilder.append(URLEncoder.encode(param.getKey(), "utf8"))
                        .append("=")
                        .append(URLEncoder.encode(valueStr, "utf8"));
                formParamBuilder.append("&");
            } catch (UnsupportedEncodingException e) {
                // move on to next
            }
        }

        String encodedFormParams = formParamBuilder.toString();
        if (encodedFormParams.endsWith("&")) {
            encodedFormParams = encodedFormParams.substring(0, encodedFormParams.length() - 1);
        }

        return encodedFormParams;
    }

    public static class Builder {
        public static Builder standard() {
            return new Builder(new ConnectorProperties());
        }

        public static Builder from(ApiClient client) {
            if (client == null) {
                throw new NullPointerException();
            }
            Builder builder = new Builder(client.properties);
            builder.defaultHeaderMap.putAll(client.defaultHeaderMap);
            for (Authentication authentication : client.authentications.values()) {
                if (authentication instanceof OAuth) {
                    builder.accessToken = ((OAuth)authentication).getAccessToken();
                }
            }
            builder.dateFormat = client.dateFormat;
            builder.objectMapper = client.objectMapper;
            builder.basePath = client.basePath;
            builder.retryConfiguration = client.retryConfiguration;
            builder.shouldThrowErrors = client.shouldThrowErrors;
            return builder;
        }

        public static ApiClient defaultClient() {
            return standard().build();
        }

        private final Map defaultHeaderMap = new HashMap<>();
        private final ConnectorProperties properties;
        private String accessToken;
        private ObjectMapper objectMapper;
        private DateFormat dateFormat;
        private String basePath;
        private RetryConfiguration retryConfiguration;
        private Boolean shouldThrowErrors = true;

        private Builder(ConnectorProperties properties) {
            this.properties = (properties != null) ? properties.copy() : new ConnectorProperties();
            withUserAgent(DEFAULT_USER_AGENT);
            withDefaultHeader("purecloud-sdk", "10.3.0");
        }

        public Builder withDefaultHeader(String header, String value) {
            defaultHeaderMap.put(header, value);
            return this;
        }

        public Builder withAccessToken(String accessToken) {
            this.accessToken = accessToken;
            return this;
        }

        public Builder withUserAgent(String userAgent) {
            return withDefaultHeader(USER_AGENT_HEADER, userAgent);
        }

        public Builder withObjectMapper(ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
            return this;
        }

        public Builder withDateFormat(DateFormat dateFormat) {
            this.dateFormat = dateFormat;
            return this;
        }

        public Builder withBasePath(String basePath) {
            this.basePath = basePath;
            return this;
        }
         public Builder withBasePath(GenesysCloudRegionWebSocketHosts region){
            this.basePath = region.asApiHost();
            return this;
        }

        public Builder withRetryConfiguration(RetryConfiguration retryConfiguration) {
        this.retryConfiguration = retryConfiguration;
        return this;
        }

        public Builder withConnectionTimeout(int connectionTimeout) {
            properties.setProperty(ApiClientConnectorProperty.CONNECTION_TIMEOUT, connectionTimeout);
            return this;
        }

        public Builder withShouldThrowErrors(boolean shouldThrowErrors) {
            this.shouldThrowErrors = shouldThrowErrors;
            return this;
        }

        public Builder withDetailLevel(DetailLevel detailLevel) {
            properties.setProperty(ApiClientConnectorProperty.DETAIL_LEVEL, detailLevel);
            return this;
        }

        public Builder withProxy(Proxy proxy) {
            properties.setProperty(ApiClientConnectorProperty.PROXY, proxy);
            return this;
        }

        public Builder withAuthenticatedProxy(Proxy proxy, String user, String pass) {
            withProxy(proxy);
            properties.setProperty(ApiClientConnectorProperty.PROXY_USER, user);
            properties.setProperty(ApiClientConnectorProperty.PROXY_PASS, pass);
            return this;
        }

        public Builder withProperty(String name, Object value) {
            properties.setProperty(name, value);
            return this;
        }

        public ApiClient build() {
            return new ApiClient(this);
        }
    }

    private static class ConnectorProperties implements ApiClientConnectorProperties {
        private final Map properties;

        public ConnectorProperties() {
            this.properties = new HashMap<>();
        }

        public ConnectorProperties(Map properties) {
            this.properties = new HashMap<>();
            if (properties != null) {
                this.properties.putAll(properties);
            }
        }

        @Override
        public  T getProperty(String key, Class propertyClass, T defaultValue) {
            Object value = properties.get(key);
            if (propertyClass.isInstance(value)) {
                return propertyClass.cast(value);
            }
            return defaultValue;
        }

        public void setProperty(String key, Object value) {
            if (key != null) {
                if (value != null) {
                    properties.put(key, value);
                }
                else {
                    properties.remove(key);
                }
            }
        }

        public ConnectorProperties copy() {
            return new ConnectorProperties(properties);
        }
    }

    private static class ApiRequestWrapper implements ApiRequest {
        private final String path;
        private final String method;
        private final List queryParams;
        private final T body;
        private final Map headerParams;
        private final Map formParams;
        private final String accept;
        private final String contentType;
        private final String[] authNames;

        public ApiRequestWrapper(String path, String method, List queryParams, T body, Map headerParams, Map formParams, String accept, String contentType, String[] authNames) {
            this.path = path;
            this.method = method;
            this.queryParams = queryParams;
            this.body = body;
            this.headerParams = headerParams;
            this.formParams = formParams;
            this.accept = accept;
            this.contentType = contentType;
            this.authNames = authNames;
        }

        @Override
        public String getPath() {
            return path;
        }

        @Override
        public String getMethod() {
            return method;
        }

        @Override
        public Map getPathParams() {
            return Collections.emptyMap();
        }

        @Override
        public List getQueryParams() {
            return queryParams;
        }

        @Override
        public Map getFormParams() {
            return formParams;
        }

        @Override
        public Map getHeaderParams() {
            return headerParams;
        }

        @Override
        public Map getCustomHeaders() {
            return Collections.emptyMap();
        }

        @Override
        public String getContentType() {
            return contentType;
        }

        @Override
        public String getAccepts() {
            return accept;
        }

        @Override
        public T getBody() {
            return body;
        }

        @Override
        public String[] getAuthNames() {
            return authNames;
        }
    }

    private static class ApiResponseWrapper implements ApiResponse {
        private final int statusCode;
        private final String reasonPhrase;
        private final Map headers;
        private final String body;
        private final T entity;

        public ApiResponseWrapper(int statusCode, String reasonPhrase, Map headers, String body, T entity) {
            this.statusCode = statusCode;
            this.reasonPhrase = reasonPhrase;
            Map caseInsensitiveMap = new TreeMap(String.CASE_INSENSITIVE_ORDER);
            caseInsensitiveMap.putAll(headers);
            this.headers = Collections.unmodifiableMap(caseInsensitiveMap);
            this.body = body;
            this.entity = entity;
        }

        @Override
        public Exception getException() {
            return null;
        }

        @Override
        public int getStatusCode() {
            return statusCode;
        }

        @Override
        public String getStatusReasonPhrase() {
            return reasonPhrase;
        }

        @Override
        public boolean hasRawBody() {
            return (body != null && !body.isEmpty());
        }

        @Override
        public String getRawBody() {
            return body;
        }

        @Override
        public T getBody() {
            return entity;
        }

        @Override
        public Map getHeaders() {
            return headers;
        }

        @Override
        public String getHeader(String key) {
            return headers.get(key);
        }

        @Override
        public String getCorrelationId() {
            return headers.get("ININ-Correlation-ID");
        }

        @Override
        public void close() throws Exception { }
    }

    public static class RetryConfiguration {
        private long backoffIntervalMs = 300000L;
        private long retryAfterDefaultMs = 3000L;
        private int maxRetryTimeSec = 0;
        private int maxRetriesBeforeBackoff = 5;

        public void setBackoffIntervalMs(long backoffIntervalMs) {
            if (backoffIntervalMs < 0) {
                throw new IllegalArgumentException("backoffIntervalMs should be a positive integer");
            }
            this.backoffIntervalMs = backoffIntervalMs;
        }

        public void setRetryAfterDefaultMs(long retryAfterDefaultMs) {
            if (retryAfterDefaultMs < 0) {
                throw new IllegalArgumentException("retryAfterDefaultMs should be a positive integer");
            }
            this.retryAfterDefaultMs = retryAfterDefaultMs;
        }

        public void setMaxRetryTimeSec(int maxRetryTimeSec) {
            if (maxRetryTimeSec < 0) {
                throw new IllegalArgumentException("maxRetryTimeSec should be a positive integer");
            }
            this.maxRetryTimeSec = maxRetryTimeSec;
        }

        public void setMaxRetriesBeforeBackoff(int maxRetriesBeforeBackoff) {
            if (maxRetriesBeforeBackoff < 0) {
                throw new IllegalArgumentException("maxRetriesBeforeBackoff can not be a negative integer");
            }
            this.maxRetriesBeforeBackoff = maxRetriesBeforeBackoff;
        }
    }

    public static class Retry {
        private long backoffIntervalMs;
        private long retryAfterDefaultMs;
        private int maxRetryTimeSec;
        private int maxRetriesBeforeBackoff;
        private int retryCountBeforeBackOff = 0;
        private long retryAfterMs;
        private Stopwatch stopwatch = null;

        private final List statusCodes = Arrays.asList(429, 502, 503, 504);
        private static Logger LOGGER = LoggerFactory.getLogger(Retry.class);

        public Retry(RetryConfiguration retryConfiguration) {
            this.backoffIntervalMs = retryConfiguration.backoffIntervalMs;
            this.retryAfterDefaultMs = retryConfiguration.retryAfterDefaultMs;
            this.maxRetryTimeSec = retryConfiguration.maxRetryTimeSec;
            this.maxRetriesBeforeBackoff = retryConfiguration.maxRetriesBeforeBackoff;
            stopwatch = Stopwatch.createStarted();
        }

        public boolean shouldRetry(ApiClientConnectorResponse connectorResponse) {
            if (stopwatch.elapsed(TimeUnit.MILLISECONDS) < maxRetryTimeSec * 1000L && statusCodes.contains(connectorResponse.getStatusCode())) {

                if (connectorResponse.getHeaders().containsKey("Retry-After")) {
                    retryAfterMs = Integer.parseInt(connectorResponse.getHeaders().getOrDefault("Retry-After", "3")) * 1000L;
                } else {
                    retryAfterMs = retryAfterDefaultMs;
                }
                //If status code is 429 then wait until retry-after time and retry. OR If status code is retryable then for the first 5 times: wait until retry-after time and retry.
                if (connectorResponse.getStatusCode() == 429 || retryCountBeforeBackOff++ < maxRetriesBeforeBackoff) {
                    return waitBeforeRetry(retryAfterMs);
                }

                //If status code is 50x then wait for every 3 Sec and retry until 5 minutes then after wait for every 9 Sec and retry until next 5 minutes afterwards wait for every 27 Sec and retry.
                return waitBeforeRetry(getWaitTimeExp(Math.min(3, Math.floor(stopwatch.elapsed(TimeUnit.MILLISECONDS) / backoffIntervalMs) + 1)));

            }
            stopwatch.stop();
            return false;
        }

        private boolean waitBeforeRetry(long retryAfterMs){
            try {
                LOGGER.info("SDK will be sleeping for: " + retryAfterMs + " milliseconds before retrying.");
                Thread.sleep(retryAfterMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return true;
        }

        private long getWaitTimeExp(double bucketCount) {
            return (long) Math.pow(3, bucketCount) * 1000L;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy