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

com.seeq.ApiClient Maven / Gradle / Ivy

There is a newer version: 66.0.0-v202407310200
Show newest version
package com.seeq;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.MultiPart;
import org.glassfish.jersey.media.multipart.MultiPartFeature;

import java.io.IOException;
import java.io.InputStream;

import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.TimeZone;

import java.net.URLEncoder;

import java.io.File;
import java.io.UnsupportedEncodingException;

import java.text.DateFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.security.auth.x500.X500Principal;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.FileSystems;

import java.io.FileInputStream;
import java.io.IOException;

import com.seeq.auth.Authentication;
import com.seeq.auth.HttpBasicAuth;
import com.seeq.auth.ApiKeyAuth;
import com.seeq.auth.OAuth;

public class ApiClient {
  private Map defaultHeaderMap = new HashMap();
  private String basePath = "http://localhost:34218/api";
  private boolean debugging = false;
  private int connectTimeout = 0;
  private int readTimeout = 0;

  private Client httpClient;
  private JSON json;
  private String tempFolderPath = null;

  private Map authentications;

  private DateFormat dateFormat;

  private volatile String identityPath;
  private volatile String authToken;
  private volatile String csrfToken;

  private volatile Path certificatePath;
  private volatile KeyStore keyStore;

  public ApiClient() {
    this.json = new JSON();
    this.httpClient = buildHttpClient(this.debugging);

    this.dateFormat = new RFC3339DateFormat();

    // Set default User-Agent.
    setUserAgent("Swagger-Codegen/64.3.0-v202405012032/java");

    // Setup authentications (key: authentication name, value: authentication).
    this.authentications = new HashMap();
    this.authentications.put("api_key", new ApiKeyAuth("header", "x-sq-csrf"));
    // Prevent the authentications from being modified.
    this.authentications = Collections.unmodifiableMap(this.authentications);
  }
  
    public static class ApiResponse
    {
        private int statusCode;
        private Map> headers;
        private T data;
        
        /**
         * Gets or sets the status code (HTTP status code)
         */
        public int getStatusCode() {
            return this.statusCode;
        }

        /**
         * Gets or sets the HTTP headers
         */
        public Map> getHeaders() {
            return this.headers;
        }

        /**
         * Gets or sets the data (parsed HTTP body)
         */
        public T getData() {
            return this.data;
        }

        /**
         * Initializes a new instance of the ApiResponse class.
         *
         * @param statusCode HTTP status code.
         * @param headers HTTP headers.
         * @param data Data (parsed HTTP body)
         */
        public ApiResponse(int statusCode, Map> headers, T data)
        {
            this.statusCode = statusCode;
            this.headers = headers;
            this.data = data;
        }
    }

  /**
   * Gets the JSON instance to do JSON serialization and deserialization.
   * @return JSON
   */
  public JSON getJSON() {
    return this.json;
  }

  public Client getHttpClient() {
    return this.httpClient;
  }

  public ApiClient setHttpClient(Client httpClient) {
    this.httpClient = httpClient;
    return this;
  }

  public String getBasePath() {
    return this.basePath;
  }

  public ApiClient setBasePath(String basePath) {
    this.basePath = basePath;
    return this;
  }

  /**
   * Get authentications (key: authentication name, value: authentication).
   * @return Map of authentication object
   */
  public Map getAuthentications() {
    return this.authentications;
  }

  /**
   * Logs the current user out by clearing the authorization token.
   */
  public void logout() {
    this.identityPath = null;
    this.authToken = null;
    this.csrfToken = null;
  }

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

  /**
   * Helper method to set username for the first HTTP basic authentication.
   * @param username Username
   */
  public void setUsername(String username) {
    for (Authentication auth : this.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 Password
   */
  public void setPassword(String password) {
    for (Authentication auth : this.authentications.values()) {
      if (auth instanceof HttpBasicAuth) {
        ((HttpBasicAuth) auth).setPassword(password);
        return;
      }
    }
    throw new RuntimeException("No HTTP basic authentication configured!");
  }

  /**
   * Helper method to set API key value for the first API key authentication.
   * @param accessKey API key
   */
  public void setApiKey(String accessKey) {
    for (Authentication auth : this.authentications.values()) {
      if (auth instanceof ApiKeyAuth) {
        ((ApiKeyAuth) auth).setApiKey(accessKey);
        return;
      }
    }
    throw new RuntimeException("No API key authentication configured!");
  }

  /**
   * Sets the path to a custom certificate to be used for connecting using an
   * encrypted connection. If connecting against an unknown CA/certificate
   * chain or using self-signed certificates, setting the certificate path will
   * be necessary. If connecting against an official CA/certificate chain,
   * setting the certificate path shouldn't be necessary as it'll be handled by
   * the OS.
   *
   * @param certificatePath The path to a certificate (PEM) file containing the
   *   public key/cert and any necessary CA chains
   */
  public ApiClient setCertificatePath(Path certificatePath) {
    this.certificatePath = certificatePath;
    // Rebuild HTTP Client according to the new SSL setting.
    this.httpClient = buildHttpClient(this.debugging);
    return this;
  }

  /**
   * Sets the KeyStore with custom certificate to be used for connecting using an
   * encrypted connection. This is an alternative to setCertificatePath().
   *
   * @param keyStore A key store populated with the certificates to be trusted.
   */
  public ApiClient setKeyStore(KeyStore keyStore) {
    this.keyStore = keyStore;
    // Rebuild HTTP Client according to the new SSL setting.
    this.httpClient = buildHttpClient(this.debugging);
    return this;
  }

  /**
   * Helper method to set API key prefix for the first API key authentication.
   * @param apiKeyPrefix API key prefix
   */
  public void setApiKeyPrefix(String apiKeyPrefix) {
    for (Authentication auth : this.authentications.values()) {
      if (auth instanceof ApiKeyAuth) {
        ((ApiKeyAuth) auth).setApiKeyPrefix(apiKeyPrefix);
        return;
      }
    }
    throw new RuntimeException("No API key authentication configured!");
  }

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

  /**
   * Set the User-Agent header's value (by adding to the default header map).
   * @param userAgent Http user agent
   * @return API client
   */
  public ApiClient setUserAgent(String userAgent) {
    addDefaultHeader("User-Agent", userAgent);
    return this;
  }

  /**
   * Add a default header.
   *
   * @param key The header's key
   * @param value The header's value
   * @return API client
   */
  public ApiClient addDefaultHeader(String key, String value) {
    this.defaultHeaderMap.put(key, value);
    return this;
  }

  /**
   * Check that whether debugging is enabled for this API client.
   * @return True if debugging is switched on
   */
  public boolean isDebugging() {
    return this.debugging;
  }

  /**
   * Enable/disable debugging for this API client.
   *
   * @param debugging To enable (true) or disable (false) debugging
   * @return API client
   */
  public ApiClient setDebugging(boolean debugging) {
    this.debugging = debugging;
    // Rebuild HTTP Client according to the new "debugging" value.
    this.httpClient = buildHttpClient(this.debugging);
    return this;
  }

  /**
   * The path of temporary folder used to store downloaded files from endpoints
   * with file response. The default value is null, i.e. using
   * the system's default tempopary folder.
   *
   * @return Temp folder path
   */
  public String getTempFolderPath() {
    return this.tempFolderPath;
  }

  /**
   * Set temp folder path
   * @param tempFolderPath Temp folder path
   * @return API client
   */
  public ApiClient setTempFolderPath(String tempFolderPath) {
    this.tempFolderPath = tempFolderPath;
    return this;
  }

  /**
   * Connect timeout (in milliseconds).
   * @return Connection timeout
   */
  public int getConnectTimeout() {
    return this.connectTimeout;
  }

  /**
   * Set the connect timeout (in milliseconds).
   * A value of 0 means no timeout, otherwise values must be between 1 and
   * {@link Integer#MAX_VALUE}.
   * @param connectTimeout Connection timeout in milliseconds
   * @return API client
   */
  public ApiClient setConnectTimeout(int connectTimeout) {
    this.connectTimeout = connectTimeout;
    this.httpClient.property(ClientProperties.CONNECT_TIMEOUT, this.connectTimeout);
    return this;
  }

  /**
   * Read timeout (in milliseconds).
   * @return Read timeout
   */
  public int getReadTimeout() {
    return this.readTimeout;
  }

  /**
   * Set the read timeout (in milliseconds).
   * A value of 0 means no timeout, otherwise values must be between 1 and
   * {@link Integer#MAX_VALUE}.
   * @param readTimeout Read timeout in milliseconds
   * @return API client
   */
  public ApiClient setReadTimeout(int readTimeout) {
    this.readTimeout = readTimeout;
    this.httpClient.property(ClientProperties.READ_TIMEOUT, this.readTimeout);
    return this;
  }

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

  /**
   * Set the date format used to parse/format date parameters.
   * @param dateFormat Date format
   * @return API client
   */
  public ApiClient setDateFormat(DateFormat dateFormat) {
    this.dateFormat = dateFormat;
    // also set the date format for model (de)serialization with Date properties
    this.json.setDateFormat((DateFormat) this.dateFormat.clone());
    return this;
  }

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

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

  /**
   * Format the given parameter object into string.
   * @param param Object
   * @return Object in string format
   */
  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.
   * @param collectionFormat Collection format
   * @param name Name
   * @param value Value
   * @return List of pairs
   */
  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;
    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 (default: csv)
    String format = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat);

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

      return params;
    }

    String delimiter = ",";

    if ("csv".equals(format)) {
      delimiter = ",";
    } else if ("ssv".equals(format)) {
      delimiter = " ";
    } else if ("tsv".equals(format)) {
      delimiter = "\t";
    } else if ("pipes".equals(format)) {
      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
   *   application/vnd.company+json
   * @param mime MIME
   * @return True if the MIME type is JSON
   */
  public boolean isJsonMime(String mime) {
    String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$";
    return mime != null && (mime.matches(jsonMime) || mime.equalsIgnoreCase("application/json-patch+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.
   * @param str String
   * @return Escaped string
   */
  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 entity according the given
   * Content-Type (only JSON is supported for now).
   * @param obj Object
   * @param formParams Form parameters
   * @param contentType Context type
   * @return Entity
   * @throws ApiException API exception
   */
  public Entity serialize(Object obj, Map formParams, String contentType) throws ApiException {
    Entity entity;
    if (contentType.startsWith("multipart/form-data")) {
      MultiPart multiPart = new MultiPart();
      for (Entry param: formParams.entrySet()) {
        if (param.getValue() instanceof File) {
          File file = (File) param.getValue();
          FormDataContentDisposition contentDisp = FormDataContentDisposition.name(param.getKey())
              .fileName(file.getName()).size(file.length()).build();
          multiPart.bodyPart(new FormDataBodyPart(contentDisp, file, MediaType.APPLICATION_OCTET_STREAM_TYPE));
        } else {
          FormDataContentDisposition contentDisp = FormDataContentDisposition.name(param.getKey()).build();
          multiPart.bodyPart(new FormDataBodyPart(contentDisp, parameterToString(param.getValue())));
        }
      }
      entity = Entity.entity(multiPart, MediaType.MULTIPART_FORM_DATA_TYPE);
    } else if (contentType.startsWith("application/x-www-form-urlencoded")) {
      Form form = new Form();
      for (Entry param: formParams.entrySet()) {
        form.param(param.getKey(), parameterToString(param.getValue()));
      }
      entity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE);
    } else {
      // We let jersey handle the serialization
      entity = Entity.entity(obj, contentType);
    }
    return entity;
  }

  /**
   * Deserialize response body to Java object according to the Content-Type.
   * @param  Type
   * @param response Response
   * @param returnType Return type
   * @return Deserialize object
   * @throws ApiException API exception
   */
  @SuppressWarnings("unchecked")
  public  T deserialize(Response response, GenericType returnType) throws ApiException {
    if (response == null || returnType == null) {
      return null;
    }

    if ("byte[]".equals(returnType.toString())) {
      // Handle binary response (byte array).
      return (T) response.readEntity(byte[].class);
    } else if (returnType.getRawType() == File.class) {
      // Handle file downloading.
      T file = (T) downloadFileFromResponse(response);
      return file;
    }

    String contentType = null;
    List contentTypes = response.getHeaders().get("Content-Type");
    if (contentTypes != null && !contentTypes.isEmpty())
      contentType = String.valueOf(contentTypes.get(0));
    if (contentType == null)
      throw new ApiException(500, "missing Content-Type in response");

    return response.readEntity(returnType);
  }

  /**
   * Download file from the given response.
   * @param response Response
   * @return File
   * @throws ApiException If fail to read file content from response and write to disk
   */
  public File downloadFileFromResponse(Response response) throws ApiException {
    try {
      File file = prepareDownloadFile(response);
      Files.copy(response.readEntity(InputStream.class), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
      return file;
    } catch (IOException e) {
      throw new ApiException(e);
    }
  }

  public File prepareDownloadFile(Response response) throws IOException {
    String filename = null;
    String contentDisposition = (String) response.getHeaders().getFirst("Content-Disposition");
    if (contentDisposition != null && !"".equals(contentDisposition)) {
      // Get filename from the Content-Disposition header.
      Pattern pattern = Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
      Matcher matcher = pattern.matcher(contentDisposition);
      if (matcher.find())
        filename = matcher.group(1);
    }

    String prefix;
    String suffix = null;
    if (filename == null) {
      prefix = "download-";
      suffix = "";
    } else {
      int pos = filename.lastIndexOf('.');
      if (pos == -1) {
        prefix = filename + "-";
      } else {
        prefix = filename.substring(0, pos) + "-";
        suffix = filename.substring(pos);
      }
      // File.createTempFile requires the prefix to be at least three characters long
      if (prefix.length() < 3)
        prefix = "download-";
    }

    if (this.tempFolderPath == null)
      return File.createTempFile(prefix, suffix);
    else
      return File.createTempFile(prefix, suffix, new File(this.tempFolderPath));
  }

  /**
   * Invoke API by sending HTTP request with the given options.
   *
   * @param  API return object type
   * @param path The sub-path of the HTTP URL
   * @param method The request method, one of "GET", "POST", "PUT", and "DELETE"
   * @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 data object from the API response
   * @throws ApiException API exception
   */  
  public  T invokeAPI(String path, String method, List queryParams, Object body, Map headerParams, Map formParams, String accept, String contentType, String[] authNames, GenericType returnType) throws ApiException {  
    ApiResponse apiResponse = this.invokeAPIWithHttpInfo(path, method, queryParams, body, headerParams, formParams, accept, contentType, authNames, returnType);
    return apiResponse != null ? apiResponse.getData() : null;
  }
  
  /**
   * Invoke API by sending HTTP request with the given options.
   *
   * @param  API return object type
   * @param path The sub-path of the HTTP URL
   * @param method The request method, one of "GET", "POST", "PUT", and "DELETE"
   * @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 An ApiResponse object with the status code, response headers and data object
   * @throws ApiException API exception
   */  
  public  ApiResponse invokeAPIWithHttpInfo(String path, String method, List queryParams, Object body, Map headerParams, Map formParams, String accept, String contentType, String[] authNames, GenericType returnType) throws ApiException {  
    updateParamsForAuth(authNames, queryParams, headerParams);

    addAuthorizationHeader(headerParams); // Add authorization header, if we have an authorization token

    // Not using `.target(this.basePath).path(path)` below,
    // to support (constant) query string in `path`, e.g. "/posts?draft=1"
    WebTarget target = this.httpClient.target(this.basePath + path);

    if (queryParams != null) {
      for (Pair queryParam : queryParams) {
        if (queryParam.getValue() != null) {
          target = target.queryParam(queryParam.getName(), queryParam.getValue());
        }
      }
    }

    Invocation.Builder invocationBuilder = target.request().accept(accept);

    for (Entry entry : headerParams.entrySet()) {
      String value = entry.getValue();
      if (value != null) {
        invocationBuilder = invocationBuilder.header(entry.getKey(), value);
      }
    }

    for (Entry entry : this.defaultHeaderMap.entrySet()) {
      String key = entry.getKey();
      if (!headerParams.containsKey(key)) {
        String value = entry.getValue();
        if (value != null) {
          invocationBuilder = invocationBuilder.header(key, value);
        }
      }
    }

    Entity entity = serialize(body, formParams, contentType);

    Response response = null;

    try {
      if ("GET".equals(method)) {
        response = invocationBuilder.get();
      } else if ("POST".equals(method)) {
        response = invocationBuilder.post(entity);
      } else if ("PUT".equals(method)) {
        response = invocationBuilder.put(entity);
      } else if ("DELETE".equals(method)) {
        response = invocationBuilder.delete();
      } else if ("PATCH".equals(method)) {
        response = invocationBuilder.header("X-HTTP-Method-Override", "PATCH").post(entity);
      } else {
        throw new ApiException(500, "unknown method type " + method);
      }

      int statusCode = response.getStatusInfo().getStatusCode();
      Map> responseHeaders = buildResponseHeaders(response);

      extractAuthorizationToken(responseHeaders); // Extract the authorization token, if found.
      extractIdentityPath(responseHeaders); // Extract the identity path, if found.
      extractCsrfToken(responseHeaders); // Extract the CSRF token, if found.

      if (response.getStatus() == Status.NO_CONTENT.getStatusCode()) {
        return null;
      } else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) {
        if (returnType == null)
          return null;
        else
          return new ApiResponse(statusCode, responseHeaders, deserialize(response, returnType));
      } else {
        String message = "error";
        String respBody = null;
        if (response.hasEntity()) {
          try {
            respBody = String.valueOf(response.readEntity(String.class));
            message = respBody;
          } catch (RuntimeException e) {
            // e.printStackTrace();
          }
        }
        throw new ApiException(
          response.getStatus(),
          message,
          buildResponseHeaders(response),
          respBody);
      }
    } finally {
      try {
        response.close();
      } catch (Exception e) {
        // it's not critical, since the response object is local in method invokeAPI; that's fine, just continue
      }
    }
  }

  /**
   * Build the Client used to make HTTP requests.
   * @param debugging Debug setting
   * @return Client
   */
  private Client buildHttpClient(boolean debugging) {
    final ClientConfig clientConfig = new ClientConfig();
    clientConfig.register(MultiPartFeature.class);
    clientConfig.register(json);
    clientConfig.register(JacksonFeature.class);

    return getClientWithSSLSupport(clientConfig);
  }

  private Map> buildResponseHeaders(Response response) {
    Map> responseHeaders = new HashMap>();
    for (Entry> entry: response.getHeaders().entrySet()) {
      List values = entry.getValue();
      List headers = new ArrayList();
      for (Object o : values) {
        headers.add(String.valueOf(o));
      }
      responseHeaders.put(entry.getKey(), headers);
    }
    return responseHeaders;
  }

  /**
   * 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 = this.authentications.get(authName);
      if (auth == null) throw new RuntimeException("Authentication undefined: " + authName);
      auth.applyToParams(queryParams, headerParams);
    }
  }

  /**
   * Utility function that allows self-signed certificates to work without adding them to the OS using keytool. Does
   * not authorize all certificates, just the one that the user specified using #setCertificatePath().
   *
   * @param certPath - The path to the certificate to allow as an authorized certificate for its corresponding key
   * @return A KeyStore that has the certificate loaded
   */
  private static KeyStore getKeyStore(Path certPath) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    X509Certificate cert = (X509Certificate) cf.generateCertificate(new FileInputStream(certPath.toString()));
    X500Principal x509principal = cert.getSubjectX500Principal();
    String alias = x509principal.getName("RFC2253");

    keyStore.load(null);
    keyStore.setCertificateEntry(alias, cert);
    return keyStore;
  }

  /**
   * Builds an HTTP client with SSL support, if enabled via #setCertificatePath().
   * See: https://stackoverflow.com/questions/2145431/https-using-jersey-client
   *
   * @param config - The base HTTP config
   * @return An HTTP client that has been built with SSL support, if enabled.
   */
  private Client getClientWithSSLSupport(ClientConfig config) {
    ClientBuilder builder = new JerseyClientBuilder().withConfig(config);
    if (this.basePath.substring(0, "https://".length()).equalsIgnoreCase("https://") &&
        (this.certificatePath != null || this.keyStore != null)) {
      // We need to enable SSL for a custom certificate, so load certificate and set up everything needed to enable encryption...
      try {
        SSLContext sc = SSLContext.getInstance("TLS");

        KeyStore keyStore;
        if (this.keyStore == null) {
            keyStore = getKeyStore(this.certificatePath);
        } else {
            keyStore = this.keyStore;
        }
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, null);

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        sc.init(
          keyManagerFactory.getKeyManagers(),
          trustManagerFactory.getTrustManagers(),
          SecureRandom.getInstanceStrong()  // Use strong random values for long-lived secrets...
        );
        return builder
          .sslContext(sc)
          .build();
      } catch (Exception e) {
        System.err.println("Failed setting up SSL context! Does the certificate exist and is it a valid .pem/.cer file?");
        e.printStackTrace();
      }
    }

    return builder.build();
  }

  /**
   * Extracts the authorization token from response headers, if it exists, and stores it for subsequent requests.
   *
   * @param responseHeaders Map of response headers
   */
  private void extractAuthorizationToken(Map> responseHeaders) {
    // If response header contains an updated authorization token, extract and store it for subsequent requests
    List headerValues = responseHeaders.get("x-sq-auth");
    if (headerValues != null) {
      this.authToken = headerValues.get(0);
    }
  }

  /**
   * Extracts the identity token from response headers, if it exists, and stores it for subsequent requests.
   *
   * @param responseHeaders Map of response headers
   */
  private void extractIdentityPath(Map> responseHeaders) {
    // If response header contains an identity token, extract and store it for subsequent requests
    List headerValues = responseHeaders.get("x-sq-identity-path");
    if (headerValues != null) {
      this.identityPath = headerValues.get(0);
    }
  }

  /**
   * Extracts the CSRF token from response headers, if it exists, and stores it for subsequent requests.
   *
   * @param responseHeaders Map of response headers
   */
  private void extractCsrfToken(Map> responseHeaders) {
    // If response header contains an updated CSRF token, extract and store it for subsequent requests
    List headerValues = responseHeaders.get("x-sq-csrf");
    if (headerValues != null) {
      this.csrfToken = headerValues.get(0);
    }
  }

  /**
   * If this client has logged in, we've stored an authorization token that can serve as proof of the client's
   * identity for future requests. This method retrieves the authorization token.
   * Note: the authorization token is secret and should be protected like a password, but unlike a password it
   * will expire after the client logs out.
   *
   * @return the authorization token, or null if the client isn't logged in.
   */
  public String getAuthToken() {
    return this.authToken;
  }

  /**
   * Sets or overrides the authorization token for the client.
   * Note: If an invalid authorization token is provided. This client will not be usable until reauthenticated.
   */
  public void setAuthToken(String authToken) {
    this.authToken = authToken;
  }

  /**
   * If this client has logged in, we've stored the user's identity (and sub-identity, if it exists).
   *
   * @return the full identity path, or null if the client isn't logged in.
   */
  public String getIdentityPath() {
    return this.identityPath;
  }

  /** Sets or overrides the user's identity (and sub-identity, if it exists). */
  public void setIdentityPath(String identityPath) {
    this.identityPath = identityPath;
  }

  /**
   * If this client has logged in, we've stored a CSRF token that can serve as proof of the client's
   * identity for future requests. This method retrieves the CSRF token.
   * Note: the CSRF token is secret and should be protected like a password, but unlike a password it
   * will expire after the client logs out.
   *
   * @return the CSRF token, or null if the client isn't logged in.
   */
  public String getCsrfToken() {
    return this.csrfToken;
  }

  /**
   * Adds authorization header to the specified map if an authorization token is available.
   *
   * @param requestHeaders Map of request headers
   */
  private void addAuthorizationHeader(Map requestHeaders) {
    if (this.authToken != null) {
      requestHeaders.put("x-sq-auth", this.authToken);
    }
    if (this.identityPath != null) {
      requestHeaders.put("x-sq-identity-path", this.identityPath);
    }
  }
}