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

com.nutanix.net.java.client.ApiClient Maven / Gradle / Ivy

package com.nutanix.net.java.client;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.nutanix.json.deserializers.ObjectTypeTypedObject;
import lombok.Data;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.ParseException;
import java.time.Duration;
import java.util.*;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import com.nutanix.net.java.client.auth.Authentication;
import com.nutanix.net.java.client.auth.HttpBasicAuth;
import com.nutanix.net.java.client.auth.ApiKeyAuth;
import com.nutanix.net.java.client.auth.OAuth;

import javax.net.ssl.SSLContext;

@Slf4j
@javax.annotation.Generated(value = "com.nutanix.swagger.codegen.generators.JavaClientSDKGenerator", date = "2023-08-18T03:03:05.126Z[Etc/UTC]")@Component("com.nutanix.net.java.client.ApiClient")
public class ApiClient {
    public enum CollectionFormat {
        CSV(","), TSV("\t"), SSV(" "), PIPES("|"), MULTI(null);

        private final String separator;
        private CollectionFormat(String separator) {
            this.separator = separator;
        }

        private String collectionToString(Collection collection) {
            return collection == null ? "" : StringUtils.join(collection, separator);
        }
    }

    private final int MAX_RETRY = 5;

    private final int RETRY_DELAY = 3000;

    private final long DEFAULT_READ_TIMEOUT = 30000;

    private final long DEFAULT_CONNECT_TIMEOUT = 30000;

    private final long MAX_DEFAULT_TIMEOUT = 1800000;
    
    private boolean debugging = false;
    
    private HttpHeaders defaultHeaders = new HttpHeaders();

    private String cookie;

    private boolean refreshCookie = true;
    
    private String scheme = "https";

    private String host = "localhost";

    private int port = 9440;

    private RestTemplate restTemplate;

    private RetryTemplate retryTemplate;

    private int maxRetryAttempts = MAX_RETRY;

    private int retryInterval = RETRY_DELAY;

    private long readTimeout = DEFAULT_READ_TIMEOUT;

    private long connectTimeout = DEFAULT_CONNECT_TIMEOUT;

    private Map authentications;

    private HttpStatus statusCode;

    private MultiValueMap responseHeaders;
    
    private DateFormat dateFormat;

    private boolean verifySsl = true;

    public ApiClient() {
        this.restTemplate = buildRestTemplate();
        this.retryTemplate = buildRetryTemplate();
        init();
    }
    
    @Autowired
    public ApiClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
        this.retryTemplate = buildRetryTemplate();
        init();
    }

    private void init() {
        // Use RFC3339 format for date and datetime.
        // See http://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14
        this.dateFormat = new RFC3339DateFormat();

        // Use UTC as the default time zone.
        this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

        // Set default User-Agent.
        addDefaultHeader("User-Agent", "Nutanix-networking-java-client/4.0.1-beta-1");

        // Setup authentications (key: authentication name, value: authentication).
        authentications = new HashMap();
        authentications.put("basicAuthScheme", new HttpBasicAuth());
        // Prevent the authentications from being modified.
        authentications = Collections.unmodifiableMap(authentications);
    }

    /**
     * Enable/Disable SSL Verification
     * @param verifySsl flag
     * @throws KeyStoreException if key is not found.
     * @throws NoSuchAlgorithmException if requested cryptographic algorithm is not found.
     * @return ApiClient this client
     */
    public ApiClient setVerifySsl(boolean verifySsl) throws KeyStoreException, NoSuchAlgorithmException {
        this.verifySsl = verifySsl;
        this.restTemplate = buildRestTemplate();
        return this;
    }

    /**
    * Get the current URL scheme for making a connection to the cluster
    * @return String URL scheme
    */
    public String getScheme() {
        return scheme;
    }

    /**
    * Set a URI scheme for connecting to the cluster (HTTP or HTTPS using SSL/TLS)
    * @param scheme URL scheme
    * @return ApiClient this client
    */
    public ApiClient setScheme(String scheme) {
        this.scheme = scheme;
        return this;
    }

    /**
     * Get the current hostname or base URL
     * @return String the hostname
     */
    public String getHost() {
        return host;
    }

    /**
     * Set the hostname for base URL
     * @param host the hostname
     * @return ApiClient this client
     */
    public ApiClient setHost(String host) {
        this.host = host;
        return this;
    }

    /**
     * Get the current response read timeout
     * @return long the response read timeout
     */
    public long getReadTimeout() {
        return readTimeout;
    }

    /**
     * Set the response read timeout
     * @param readTimeout the response read timeout
     * @return ApiClient this client
     */
    public ApiClient setReadTimeout(long readTimeout) {
        this.readTimeout = readTimeout;
        this.restTemplate = buildRestTemplate();
        return this;
    }

    /**
    * Get the current connection timeout
    * @return long the connection timeout
    */
    public long getConnectTimeout() {
        return connectTimeout;
    }

    /**
    * Set the connection timeout
    * @param connectTimeout the connection timeout
    * @return ApiClient this client
    */
    public ApiClient setConnectTimeout(long connectTimeout) {
        this.connectTimeout = connectTimeout;
        this.restTemplate = buildRestTemplate();
        return this;
    }

    private long getValidTimeout(long timeout, long defaultTimeout) {
        if (timeout <= 0) {
            timeout = defaultTimeout;
        } else if (timeout > MAX_DEFAULT_TIMEOUT) {
            timeout = MAX_DEFAULT_TIMEOUT;
        }

        return timeout;
    }

    /**
     * Get the current port for base URL
     * @return int the port
     */
    public int getPort() {
        return port;
    }

    /**
     * Set the port for base URL, which should exclude the semicolon (Default port: 9440)
     * @param port the port
     * @return ApiClient this client
     */
    public ApiClient setPort(int port) {
        this.port = port;
        return this;
    }

    /**
     * Get the number of max retry attempts
     * @return int number of max retry attempts
     */
    public int getMaxRetryAttempts() {
      return maxRetryAttempts;
    }

    /**
     * Set the number of max retry attempts
     * @param maxRetryAttempts number of max retry attempts
     * @return ApiClient this client
     */
    public ApiClient setMaxRetryAttempts(int maxRetryAttempts) {
      this.maxRetryAttempts = maxRetryAttempts;
      this.retryTemplate = buildRetryTemplate();
      return this;
    }

    /**
     * Get the delay between each retry attempt
     * @return int back off period in milliseconds
     */
    public int getRetryInterval() {
      return retryInterval;
    }

    /**
     * Set the delay between each retry attempt
     * @param retryInterval delay between each retry attempt in milliseconds
     * @return ApiClient this client
     */
    public ApiClient setRetryInterval(int retryInterval) {
      this.retryInterval = retryInterval;
      this.retryTemplate = buildRetryTemplate();
      return this;
    }

    /**
     * Gets the status code of the previous request
     * @return HttpStatus the status code
     */
    public HttpStatus getStatusCode() {
        return statusCode;
    }

    /**
     * Gets the response headers of the previous request
     * @return MultiValueMap a map of response headers
     */
    public MultiValueMap getResponseHeaders() {
        return responseHeaders;
    }

    /**
     * Get authentications (key: authentication name, value: authentication).
     * @return Map the currently configured authentication types
     */
    public Map getAuthentications() {
        return authentications;
    }

    /**
     * Get authentication for the given name.
     *
     * @param authName The authentication name
     * @return The authentication, null if not found
     */
    public Authentication getAuthentication(String authName) {
        return authentications.get(authName);
    }

    /**
     * Helper method to set username for the first HTTP basic authentication.
     * @param username the username
     */
    public void setUsername(String username) {
        for (Authentication auth : authentications.values()) {
            if (auth instanceof HttpBasicAuth) {
                ((HttpBasicAuth) auth).setUsername(username);
                return;
            }
        }
        throw new RuntimeException("No HTTP basic authentication configured!");
    }

    /**
     * Helper method to set password for the first HTTP basic authentication.
     * @param password the password
     */
    public void setPassword(String password) {
        for (Authentication auth : authentications.values()) {
            if (auth instanceof HttpBasicAuth) {
                ((HttpBasicAuth) auth).setPassword(password);
                return;
            }
        }
        throw new RuntimeException("No HTTP basic authentication configured!");
    }


    /**
     * Add a default header.
     *
     * @param name The header's name
     * @param value The header's value
     * @return ApiClient this client
     */
    public ApiClient addDefaultHeader(String name, String value) {
        if ("Authorization".equals(name)) {
            this.cookie = null;
        }

        defaultHeaders.add(name, value);
        if (!"Authorization".equals(name)) {
            log.info("Default header {}:{} added to the api client", name, value);
        }

        return this;
    }

    /**
    * Enable or disable debugging
    * @param debugging debug value true or false
    */
    public void setDebugging(boolean debugging) {
        List currentInterceptors = this.restTemplate.getInterceptors();
        if(debugging) {
            if (currentInterceptors == null) {
                currentInterceptors = new ArrayList();
            }
            ClientHttpRequestInterceptor interceptor = new ApiClientHttpRequestInterceptor();
            currentInterceptors.add(interceptor);
            this.restTemplate.setInterceptors(currentInterceptors);
        }
        else {
            if (currentInterceptors != null && !currentInterceptors.isEmpty()) {
                Iterator iter = currentInterceptors.iterator();
                while (iter.hasNext()) {
                    ClientHttpRequestInterceptor interceptor = iter.next();
                    if (interceptor instanceof ApiClientHttpRequestInterceptor) {
                        iter.remove();
                    }
                }
                this.restTemplate.setInterceptors(currentInterceptors);
            }
        }
        this.debugging = debugging;
    }

    /**
     * Check that whether debugging is enabled for this API client.
     * @return boolean true if this client is enabled for debugging, false otherwise
     */
    public boolean isDebugging() {
        return debugging;
    }

    /**
     * Get the date format used to parse/format date parameters.
     * @return DateFormat format
     */
    public DateFormat getDateFormat() {
        return dateFormat;
    }

    /**
     * Set the date format used to parse/format date parameters.
     * @param dateFormat Date format
     * @return ApiClient
     */
    public ApiClient setDateFormat(DateFormat dateFormat) {
        this.dateFormat = dateFormat;
        return this;
    }
    
    /**
     * Parse the given string into Date object.
     * @param str date to parse
     * @return Date parsed date
     */
    public Date parseDate(String str) {
        try {
            return dateFormat.parse(str);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

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

        /**
     * Format the given parameter object into string.
     * @param param the object to convert
     * @return String the parameter represented as a 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);
        }
    }

    /**
     * Converts a parameter to a {@link MultiValueMap} for use in REST requests
     * @param collectionFormat The format to convert to
     * @param name The name of the parameter
     * @param value The parameter's value
     * @return a Map containing the String value(s) of the input parameter
     */
    public MultiValueMap parameterToMultiValueMap(CollectionFormat collectionFormat, String name, Object value) {
        final MultiValueMap params = new LinkedMultiValueMap();

        if (name == null || name.isEmpty() || value == null) {
            return params;
        }

        if(collectionFormat == null) {
            collectionFormat = CollectionFormat.CSV;
        }

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

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

        if (collectionFormat.equals(CollectionFormat.MULTI)) {
            for (Object item : valueCollection) {
                params.add(name, parameterToString(item));
            }
            return params;
        }

        List values = new ArrayList();
        for(Object o : valueCollection) {
            values.add(parameterToString(o));
        }
        params.add(name, collectionFormat.collectionToString(values));

        return params;
    }

    /*
    * Check if the given {@code String} is a JSON MIME.
    * @param mediaType the input MediaType
    * @return boolean true if the MediaType represents JSON, false otherwise
    */
    public boolean isJsonMime(String mediaType) {
        // "* / *" is default to JSON
        if ("*/*".equals(mediaType)) {
            return true;
        }

        try {
            return isJsonMime(MediaType.parseMediaType(mediaType));
        } catch (InvalidMediaTypeException e) {
        }
        return false;
    }

    /**
     * Check if the given MIME is a JSON MIME.
     * JSON MIME examples:
     *     application/json
     *     application/json; charset=UTF8
     *     APPLICATION/JSON
     * @param mediaType the input MediaType
     * @return boolean true if the MediaType represents JSON, false otherwise
     */
    public boolean isJsonMime(MediaType mediaType) {
        return mediaType != null && (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType) || mediaType.getSubtype().matches("^.*\\+json[;]?\\s*$"));
    }

    /**
     * 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 List The list of MediaTypes to use for the Accept header
     */
    public List selectHeaderAccept(String[] accepts) {
        if (accepts.length == 0) {
            return null;
        }
        for (String accept : accepts) {
            MediaType mediaType = MediaType.parseMediaType(accept);
            if (isJsonMime(mediaType)) {
                return Collections.singletonList(mediaType);
            }
        }
        return MediaType.parseMediaTypes(StringUtils.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 MediaType The Content-Type header to use. If the given array is empty, JSON will be used.
     */
    public MediaType selectHeaderContentType(String[] contentTypes) {
        if (contentTypes.length == 0) {
            return MediaType.APPLICATION_JSON;
        }
        for (String contentType : contentTypes) {
            MediaType mediaType = MediaType.parseMediaType(contentType);
            if (isJsonMime(mediaType)) {
                return mediaType;
            }
        }
        return MediaType.parseMediaType(contentTypes[0]);
    }

    /**
     * Select the body to use for the request
     * @param obj the body object
     * @param formParams the form parameters
     * @param contentType the content type of the request
     * @return Object the selected body
     */
    protected Object selectBody(Object obj, MultiValueMap formParams, MediaType contentType) {
        boolean isForm = MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType) || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType);
        return isForm ? formParams : obj;
    }

    /**
     * Invoke API by sending HTTP request with the given options.
     *
     * @param  the return type to use
     * @param path The sub-path of the HTTP URL
     * @param method The request method
     * @param queryParams The query parameters
     * @param body The request body object
     * @param headerParams The header parameters
     * @param formParams The form parameters
     * @param accept The request's Accept header
     * @param contentType The request's Content-Type header
     * @param authNames The authentications to apply
     * @param returnType The return type into which to deserialize the response
     * @return The response body in chosen type
     * @throws org.springframework.web.client.RestClientException
     */
    public  T invokeAPI(String path, HttpMethod method, MultiValueMap queryParams, Object body, HttpHeaders headerParams, MultiValueMap formParams, List accept, MediaType contentType, String[] authNames, ParameterizedTypeReference returnType) throws RestClientException {
        try {
            return invokeAPIInternal(path, method, queryParams, body, headerParams, formParams, accept, contentType, authNames, returnType);
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().equals(HttpStatus.UNAUTHORIZED)) {
                log.warn("Intercepting a 401 response and retrying without cookies...");
                this.cookie = null;
                refreshCookie = true;
                return invokeAPIInternal(path, method, queryParams, body, headerParams, formParams, accept, contentType, authNames, returnType);
            } else {
                log.error("Unable to make a successful request", e);
                throw e;
            }
        } catch (Exception e) {
            log.error("Unable to make a successful request", e);
            throw e;
        }
    }


    private  T invokeAPIInternal(String path, HttpMethod method, MultiValueMap queryParams, Object body, HttpHeaders headerParams, MultiValueMap formParams, List accept, MediaType contentType, String[] authNames, ParameterizedTypeReference returnType) throws RestClientException {

        final UriComponentsBuilder builder = UriComponentsBuilder.newInstance().scheme(scheme).host(host).port(port).path(path);
        if (queryParams != null) {
            builder.queryParams(queryParams);
        }
        
        final BodyBuilder requestBuilder = RequestEntity.method(method, builder.build().toUri());
        if(accept != null) {
            requestBuilder.accept(accept.toArray(new MediaType[accept.size()]));
        }
        if(contentType != null) {
            requestBuilder.contentType(contentType);
        }

        if(defaultHeaders.getOrDefault("NTNX-Request-Id", null) == null
            && headerParams.getOrDefault("NTNX-Request-Id", null) == null) {
            UUID generatedID = UUID.randomUUID();
            headerParams.add("NTNX-Request-Id", String.valueOf(generatedID));
        }
        if(!headerParams.containsKey(HttpHeaders.IF_MATCH)) {
          addEtagReferenceToHeader(body, requestBuilder);
        }
        headerParams.putAll(defaultHeaders);

        if(cookie != null) {
            requestBuilder.header(HttpHeaders.COOKIE, cookie);
            if (headerParams.containsKey(HttpHeaders.AUTHORIZATION)) {
                headerParams.remove(HttpHeaders.AUTHORIZATION);
            }
        }
        else {
            updateParamsForAuth(authNames, queryParams, headerParams);
        }
        addHeadersToRequest(headerParams, requestBuilder);

        RequestEntity requestEntity = requestBuilder.body(selectBody(body, formParams, contentType));

        return sendRequest(requestEntity, returnType);
    }

    private  T sendRequest(RequestEntity requestEntity, ParameterizedTypeReference returnType) {
        List retryStatusList = Arrays.asList(HttpStatus.SERVICE_UNAVAILABLE, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.REQUEST_TIMEOUT);
        AtomicReference body = new AtomicReference<>();
        retryTemplate.execute(context -> {
            ResponseEntity responseEntity;
            try {
                responseEntity  = restTemplate.exchange(requestEntity, returnType);
                statusCode = responseEntity.getStatusCode();
                body.set(getResponseBody(responseEntity, returnType));
            } catch (HttpStatusCodeException e) {
                if(!retryStatusList.contains(e.getStatusCode())) {
                    context.setExhaustedOnly();
                }

                log.error("Unable to make a successful request", e);
                throw e;
            }

            return null;
        });

        return body.get();
    }

    private  T getResponseBody(ResponseEntity responseEntity, ParameterizedTypeReference returnType) {
        // Set Cookie information to reuse in subsequent requests for a valid response
        List responseCookies = responseEntity.getHeaders().get(HttpHeaders.SET_COOKIE);
        if (refreshCookie && responseCookies != null && cookie == null) {
            StringBuilder sb = new StringBuilder();
            for (String responseCookie : responseCookies) {
                sb.append(responseCookie.split(";")[0]).append(";");
            }
            sb.deleteCharAt(sb.length() - 1);
            cookie = sb.toString();
            refreshCookie = false;
        }
        if (responseEntity.getStatusCode() == HttpStatus.NO_CONTENT) {
            return null;
        }
        else if (responseEntity.getStatusCode().is2xxSuccessful()) {
            if (returnType == null) {
                return null;
            }
            if (responseEntity.getHeaders().getOrDefault(HttpHeaders.ETAG, null) != null && responseEntity.getBody() instanceof ObjectTypeTypedObject) {
                ((ObjectTypeTypedObject) responseEntity.getBody()).get$reserved().put(HttpHeaders.ETAG, responseEntity.getHeaders().get(HttpHeaders.ETAG).get(0));
            }
            return responseEntity.getBody();
        }
        else {
            // The error handler built into the RestTemplate should handle 400 and 500 series errors.
            cookie = null;
            String message = "API returned " + statusCode + " and it wasn't handled by the RestTemplate error handler";
            log.error(message);
            throw new RestClientException(message);
        }
    }

    private void addEtagReferenceToHeader(Object body, BodyBuilder requestBuilder) {
        if (body instanceof ObjectTypeTypedObject) {
            ObjectTypeTypedObject bodyInstance = (ObjectTypeTypedObject) body;
            Map reservedFields = bodyInstance.get$reserved();
            if (MapUtils.isNotEmpty(reservedFields) && reservedFields.containsKey(HttpHeaders.ETAG)) {
                String etagReference = String.valueOf(reservedFields.get(HttpHeaders.ETAG));
                requestBuilder.header(HttpHeaders.IF_MATCH, etagReference);
            }
        }
    }

    /**
    * Get ETag from an object if exists.
    *
    * The ETag is usually provided in the response of the GET API calls,
    * which can further be used in other HTTP operations.
    *
    * @param object Object from which ETag needs to be retrieved
    * @return String ETag header in the object if it's an API response object, otherwise null
    */
    public static String getEtag(Object object) {
        String etag = null;
        if (object != null && object instanceof ObjectTypeTypedObject) {
            ObjectTypeTypedObject typedObject = (ObjectTypeTypedObject) object;
            if (MapUtils.isNotEmpty(typedObject.get$reserved())) {
                Optional> etagEntry = typedObject.get$reserved().entrySet().stream().filter(x -> {
                                                                return HttpHeaders.ETAG.toLowerCase()
                                                                            .equals(x.getKey().toLowerCase());
                                                            }).findFirst();
                etag = etagEntry.isPresent() ? (String) etagEntry.get().getValue() : null;
            }
        }

        return etag;
    }

    /**
     * Add headers to the request that is being built
     * @param headers The headers to add
     * @param requestBuilder The current request
     */
    private void addHeadersToRequest(HttpHeaders headers, BodyBuilder requestBuilder) {
        for (Entry> entry : headers.entrySet()) {
            List values = entry.getValue();
            for(String value : values) {
                if (value != null) {
                    requestBuilder.header(entry.getKey(), value);
                }
            }
        }
    }

    /**
     * Build the RestTemplate used to make HTTP requests.
     * @return RestTemplate
     */
    private RestTemplate buildRestTemplate() {
        RestTemplateBuilder restTemplateBuilder= new RestTemplateBuilder();
        if (this.verifySsl == false) {
            try {
                TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
                SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
                                                                   .loadTrustMaterial(null, acceptingTrustStrategy)
                                                                   .build();
                SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext,
                                                                                         new NoopHostnameVerifier());
                CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();
                restTemplateBuilder = restTemplateBuilder.requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient));
            } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
                String message = "Cannot disable SSL Verification, perform setVerifySsl(true) or retry";
                log.error(message, e);
                throw new RestClientException(message, e);
            }
        } else {
            restTemplateBuilder = restTemplateBuilder.requestFactory(restTemplateBuilder::buildRequestFactory);
                    }
        ObjectMapper customMapper = new ObjectMapper();
        customMapper.registerModule(new JavaTimeModule());
        customMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        customMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY);
        customMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        restTemplateBuilder = restTemplateBuilder.additionalMessageConverters(new MappingJackson2HttpMessageConverter(customMapper));
        Duration readTimeoutDuration = Duration.ofMillis(getValidTimeout(this.readTimeout, DEFAULT_READ_TIMEOUT));
        Duration connectTimeoutDuration = Duration.ofMillis(getValidTimeout(this.connectTimeout, DEFAULT_CONNECT_TIMEOUT));
        restTemplateBuilder = restTemplateBuilder.setConnectTimeout(connectTimeoutDuration).setReadTimeout(readTimeoutDuration);
        return restTemplateBuilder.build();
    }

    /**
     * Build the RetryTemplate used to retry failed HTTP requests.
     * @return RetryTemplate
     */
    private RetryTemplate buildRetryTemplate(){
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(maxRetryAttempts);
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(retryInterval);
        RetryTemplate template = new RetryTemplate();
        template.setRetryPolicy(retryPolicy);
        template.setBackOffPolicy(backOffPolicy);
        return template;
    }

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

        @Slf4j
    private static class ApiClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            logRequest(request, body);
            ClientHttpResponse response = execution.execute(request, body);
            response = new BufferedClientHttpResponse(response);
            logResponse(response);
            return response;
        }

        private void logRequest(HttpRequest request, byte[] body) throws UnsupportedEncodingException {
            log.info("{} {}" , request.getMethod(), request.getURI());
            log.debug("Request Headers:\n{}" , headersToString(request.getHeaders()));
            log.debug("Request Body:\n{}" , new String(body, StandardCharsets.UTF_8));
        }

        private void logResponse(ClientHttpResponse response) throws IOException {
            log.info("Reply: {} {}" , response.getRawStatusCode(), response.getStatusText());
            log.debug("Response Headers:\n{}" , headersToString(response.getHeaders()));
            log.debug("Response Body:\n{}" , bodyToString(response.getBody()));
        }

        private String headersToString(HttpHeaders headers) {
            StringBuilder builder = new StringBuilder();
            for(Entry> entry : headers.entrySet()) {
                builder.append(entry.getKey()).append("=[");
                for(String value : entry.getValue()) {
                    builder.append(value).append(",");
                }
                builder.setLength(builder.length() - 1); // Get rid of trailing comma
                builder.append("]\n");
            }
            builder.setLength(builder.length() - 1); // Get rid of trailing comma
            return builder.toString();
        }
        
        private String bodyToString(InputStream body) throws IOException {
            StringBuilder builder = new StringBuilder();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body, StandardCharsets.UTF_8));
            String line = bufferedReader.readLine();
            while (line != null) {
                builder.append(line).append(System.lineSeparator());
                line = bufferedReader.readLine();
            }
            bufferedReader.close();
            return builder.toString();
        }


        private class BufferedClientHttpResponse implements ClientHttpResponse {

            private final ClientHttpResponse response;
            private byte[] body;

            public BufferedClientHttpResponse(ClientHttpResponse response) {
                this.response = response;
            }

            @Override
            public HttpStatus getStatusCode() throws IOException {
                return response.getStatusCode();
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return response.getRawStatusCode();
            }

            @Override
            public String getStatusText() throws IOException {
                return response.getStatusText();
            }

            @Override
            public void close() {
                response.close();
            }

            @Override
            public HttpHeaders getHeaders() {
                return response.getHeaders();
            }

            @Override
            public InputStream getBody() throws IOException {
                if (body == null) {
                    body = StreamUtils.copyToByteArray(response.getBody());
                }

                return new ByteArrayInputStream(body);
            }
        }
    }
}