synapticloop.scaleway.api.ScalewayApiClient.java Source code

Java tutorial

Introduction

Here is the source code for synapticloop.scaleway.api.ScalewayApiClient.java

Source

package synapticloop.scaleway.api;

/*
 * Copyright (c) 2016-2017 synapticloop.
 *
 * All rights reserved.
 *
 * This code may contain contributions from other parties which, where
 * applicable, will be listed in the default build file for the project
 * ~and/or~ in a file named CONTRIBUTORS.txt in the root of the project.
 *
 * This source code and any derived binaries are covered by the terms and
 * conditions of the Licence agreement ("the Licence").  You may not use this
 * source code or any derived binaries except in compliance with the Licence.
 * A copy of the Licence is available in the file named LICENSE.txt shipped with
 * this source code or binaries.
 */

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import synapticloop.scaleway.api.exception.ScalewayApiException;
import synapticloop.scaleway.api.model.*;
import synapticloop.scaleway.api.request.*;
import synapticloop.scaleway.api.response.*;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;

/**
 * This is the Scaleway API client to interact with the cloud provider
 * <p>
 * https://www.scaleway.com/
 */
public class ScalewayApiClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(ScalewayApiClient.class);

    private final String accessToken;
    private final Region region;
    private final String computeUrl;
    private final CloseableHttpClient httpclient;

    /**
     * Instantiate a new API Client for the Scaleway API Provider, each API
     * client points to a specific Region.  Once this region is set, it cannot
     * be changed.  Instead instantiate a new API client for the new region,
     *
     * @param accessToken the access token that is authorised to invoke the API -
     *                    see the credentials section: <a href="https://cloud.scaleway.com/#/credentials">https://cloud.scaleway.com/#/credentials</a>
     *                    for access tokens
     * @param region      the scaleway datacentre region that this API points to (see
     *                    {@link Region} for all of the regions available at the moment)
     */
    public ScalewayApiClient(String accessToken, Region region) {
        this.accessToken = accessToken;
        this.region = region;
        this.computeUrl = String.format(Constants.COMPUTE_URL, region);

        HttpClientBuilder httpBuilder = HttpClients.custom();
        httpBuilder.setUserAgent(Constants.USER_AGENT);
        this.httpclient = httpBuilder.build();

        Version currentJacksonVersion = new ObjectMapper().version();
        Version earliestSupportedVersion = new Version(2, 8, 7, null, currentJacksonVersion.getGroupId(),
                currentJacksonVersion.getArtifactId());
        int versionCompared = currentJacksonVersion.compareTo(earliestSupportedVersion);
        if (versionCompared < 0) {
            LOGGER.error("Jackson version: {}, version compared: {}", currentJacksonVersion.toString(),
                    versionCompared);
            throw new RuntimeException(
                    "Sorry, scaleway-api-client requires Jackson version 2.8.7 due to bugs in earlier versions.");
        }
    }

    /**
     * get the region that this API is pointing to
     *
     * @return The region that this API is pointing to
     */
    public Region getRegion() {
        return region;
    }

    /**
     * List all of the organizations
     *
     * @return a list of all of the organizations
     * @throws ScalewayApiException If there was an error with the API call
     */
    public List<Organization> getAllOrganizations() throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, Constants.ACCOUNT_URL, Constants.PATH_ORGANIZATIONS, 200,
                OrganizationsResponse.class).getOrganizations());
    }

    /**
     * Get the user information
     *
     * @param userId the ID of the user to retrieve
     * @return the user identified by the ID
     * @throws ScalewayApiException If there was an error in the API call
     */
    public User getUser(String userId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, Constants.ACCOUNT_URL,
                String.format(Constants.PATH_USERS_SLASH, userId), 200, UserResponse.class).getUser());
    }

    /**
     * A convenience method to create a server, which has a dynamic IP attached to
     * it.
     *
     * @param serverName     The name of the server
     * @param imageId        the ID of the image to use as the base
     * @param organizationId the organization id
     * @param serverType     the Type of Server
     * @param tags           The tags to apply to this server
     * @return The newly created server
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Server createServer(String serverName, String imageId, String organizationId, ServerType serverType,
            String... tags) throws ScalewayApiException {
        ServerDefinitionRequest serverDefinitionRequest = new ServerDefinitionRequest(serverName, imageId,
                organizationId, serverType, tags);
        return createServer(serverDefinitionRequest);
    }

    /**
     * Create a server
     *
     * @param serverDefinitionRequest The server definition to create
     * @return The created server
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Server createServer(ServerDefinitionRequest serverDefinitionRequest) throws ScalewayApiException {
        HttpPost request = (HttpPost) buildRequest(Constants.HTTP_METHOD_POST,
                new StringBuilder(computeUrl).append(Constants.PATH_SERVERS).toString(), serverDefinitionRequest);

        return (executeAndGetResponse(request, 201, ServerResponse.class).getServer());
    }

    /**
     * Get the server details with the passed in server ID
     *
     * @param serverId The ID of the server to retrieve the details for
     * @return The server object
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Server getServer(String serverId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl,
                String.format(Constants.PATH_SERVERS_SLASH, serverId), 200, ServerResponse.class).getServer());
    }

    /**
     * Get a list of all of the servers paginated, pages start at 1, maximum
     * number of results per page is 100.
     *
     * @param numPage    the page number that you are requesting (starts at 1)
     * @param numPerPage the number of results per page - (maximum value of 100)
     * @return The list of all avilable servers
     * @throws ScalewayApiException If there was an error with the API call
     */
    public ServersResponse getAllServers(int numPage, int numPerPage) throws ScalewayApiException {
        HttpRequestBase request = buildRequest(Constants.HTTP_METHOD_GET, new StringBuilder(computeUrl)
                .append(String.format(Constants.PATH_SERVERS, numPage, numPerPage)).toString());
        HttpResponse response = executeRequest(request);
        if (response.getStatusLine().getStatusCode() == 200) {
            Header[] allHeaders = response.getAllHeaders();
            ServersResponse serversResponse = parseResponse(response, ServersResponse.class);
            serversResponse.parsePaginationHeaders(allHeaders);
            return (serversResponse);
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    /*
     * Update a server
     *
     * @param server The server with the updated information
     *
     * @return The returned server object
     *
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Server updateServer(Server server) throws ScalewayApiException {
        HttpPut request = (HttpPut) buildRequest(Constants.HTTP_METHOD_PUT, new StringBuilder(computeUrl)
                .append(String.format(Constants.PATH_SERVERS_SLASH, server.getId())).toString());

        // cleanup the server object, since we can't send all keys
        server.cleanForPut();
        server.getImage().cleanForPut();
        for (String volumeKey : server.getVolumes().keySet()) {
            Volume volume = server.getVolumes().get(volumeKey);
            volume.cleanForPut();
        }
        server.getSecurityGroup().cleanForPut();

        try {
            StringEntity entity = new StringEntity(serializeObject(server));
            request.setEntity(entity);
            return (executeAndGetResponse(request, 200, ServerResponse.class).getServer());
        } catch (UnsupportedEncodingException | JsonProcessingException ex) {
            throw new ScalewayApiException(ex);
        }
    }

    /**
     * Get a list of all of the available images - with the results coming back
     * paginated, pages start at 1, maximum number of results per page is 100.
     *
     * @param numPage    the page number that you are requesting (starts at 1)
     * @param numPerPage the number of results per page - (maximum value of 100)
     * @return The list of all available images
     * @throws ScalewayApiException If there was an error with the call
     */
    public ImagesResponse getAllImages(int numPage, int numPerPage) throws ScalewayApiException {
        HttpRequestBase request = buildRequest(Constants.HTTP_METHOD_GET, new StringBuilder(computeUrl)
                .append(String.format(Constants.PATH_IMAGES_PAGING, numPage, numPerPage)).toString());
        HttpResponse response = executeRequest(request);
        if (response.getStatusLine().getStatusCode() == 200) {
            Header[] allHeaders = response.getAllHeaders();
            ImagesResponse imagesResponse = parseResponse(response, ImagesResponse.class);
            imagesResponse.parsePaginationHeaders(allHeaders);
            return (imagesResponse);
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    /**
     * Get the image details with the specified id
     *
     * @param imageId The ID of the image
     * @return The image
     * @throws ScalewayApiException If there was an error calling the API
     */
    public Image getImage(String imageId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl, String.format(Constants.PATH_IMAGES_SLASH, imageId),
                200, ImageResponse.class).getImage());
    }

    /**
     * Delete a server - this will only delete the actual server instance, but
     * not any of the underlying resources.  If you wish to kill all resources
     * (apart from reserved IP addresses) try:
     * <p>
     * scalewayApiClient.executeServerAction(server.getId(), ServerAction.TERMINATE);
     *
     * @param serverId The ID of the server to delete
     * @throws ScalewayApiException If there was an error with the API call
     */
    public void deleteServer(String serverId) throws ScalewayApiException {
        execute(Constants.HTTP_METHOD_DELETE, computeUrl, String.format(Constants.PATH_SERVERS_SLASH, serverId),
                204, null);
    }

    /**
     * Get the actions that are available for the server
     *
     * @param serverId The id of the server to get the actions
     * @return The list of available actions for the server
     * @throws ScalewayApiException If there was an error with the API calls
     */
    public List<ServerAction> getServerActions(String serverId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl,
                String.format(Constants.PATH_SERVERS_SLASH_ACTION, serverId), 200, ServerActionsResponse.class)
                        .getServerActions());
    }

    /**
     * Create a volume
     *
     * @param name           The name of the volume to create
     * @param organizationId The organization ID
     * @param size           the size of the volume which must be more than 1000000000 (1GB)
     *                       and less than 150000000000 (150GB).
     * @param volumeType     the type of the volume
     * @return The newly created volume
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Volume createVolume(String name, String organizationId, long size, VolumeType volumeType)
            throws ScalewayApiException {
        HttpPost request = (HttpPost) buildRequest(Constants.HTTP_METHOD_POST,
                new StringBuilder(computeUrl).append(Constants.PATH_VOLUMES).toString(),
                new VolumeRequest(name, organizationId, size, volumeType));

        return (executeAndGetResponse(request, 201, VolumeResponse.class).getVolume());
    }

    /**
     * Get a list of all of the available volumes - with the results coming back
     * paginated, pages start at 1, maximum number of results per page is 100.
     *
     * @param numPage    the page number that you are requesting (starts at 1)
     * @param numPerPage the number of results per page - (maximum value of 100)
     * @return The list of all available volumes
     * @throws ScalewayApiException If there was an error with the call
     */
    public VolumesResponse getAllVolumes(int numPage, int numPerPage) throws ScalewayApiException {
        HttpRequestBase request = buildRequest(Constants.HTTP_METHOD_GET, new StringBuilder(computeUrl)
                .append(String.format(Constants.PATH_VOLUMES_PAGING, numPage, numPerPage)).toString());
        HttpResponse response = executeRequest(request);

        if (response.getStatusLine().getStatusCode() == 200) {
            Header[] allHeaders = response.getAllHeaders();
            VolumesResponse volumesResponse = parseResponse(response, VolumesResponse.class);
            volumesResponse.parsePaginationHeaders(allHeaders);
            return (volumesResponse);
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }

    }

    /**
     * Get a volume from the passed in volume ID
     *
     * @param volumeId The ID of the volume
     * @return the volume for the passed in ID
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Volume getVolume(String volumeId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl,
                String.format(Constants.PATH_VOLUMES_SLASH, volumeId), 200, VolumeResponse.class).getVolume());

    }

    /**
     * Delete a volume by its ID
     *
     * @param volumeId the id of the volume to delete
     * @throws ScalewayApiException If there was an error with the API call
     */
    public void deleteVolume(String volumeId) throws ScalewayApiException {
        execute(Constants.HTTP_METHOD_DELETE, computeUrl, String.format(Constants.PATH_VOLUMES_SLASH, volumeId),
                204, null);
    }

    /**
     * Execute the server action for the server identified by the ID
     *
     * @param serverId     The ID of the server to execute the action on
     * @param serverAction the server action to perform
     * @return The task associated with the action
     * @throws ScalewayApiException If there was an error with the API call
     */
    public ServerTask executeServerAction(String serverId, ServerAction serverAction) throws ScalewayApiException {

        HttpPost request = (HttpPost) buildRequest(
                Constants.HTTP_METHOD_POST, new StringBuilder(computeUrl)
                        .append(String.format(Constants.PATH_SERVERS_SLASH_ACTION, serverId)).toString(),
                new ServerActionRequest(serverAction));

        return (executeAndGetResponse(request, 202, ServerTaskResponse.class).getServerTask());
    }

    /**
     * Get the status of a task
     *
     * @param taskId The id of the task
     * @return The server task which includes the status
     * @throws ScalewayApiException If there was an error with the API call
     */
    public ServerTask getTaskStatus(String taskId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl, String.format(Constants.PATH_TASKS_SLASH, taskId),
                200, ServerTaskResponse.class).getServerTask());
    }

    /**
     * Create an access token for the Scaleway API
     *
     * @param emailAddress the email address
     * @param password     the password
     * @param expires      whether this token expires
     * @return The newly created token details
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Token createToken(String emailAddress, String password, boolean expires) throws ScalewayApiException {
        HttpPost request = (HttpPost) buildRequest(Constants.HTTP_METHOD_POST,
                new StringBuilder(Constants.ACCOUNT_URL).append(Constants.PATH_TOKENS).toString(),
                new TokenRequest(emailAddress, password, expires));

        return (executeAndGetResponse(request, 201, TokenResponse.class).getToken());
    }

    /**
     * Get the details for a token identified by its token ID
     *
     * @param tokenId The unique identifier for the token
     * @return The token details
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Token getToken(String tokenId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, Constants.ACCOUNT_URL,
                String.format(Constants.PATH_TOKENS_SLASH, tokenId), 200, TokenResponse.class).getToken());
    }

    /**
     * Delete a token with the specified ID
     *
     * @param tokenId The token id
     * @throws ScalewayApiException If there was an error in with the API call
     */
    public void deleteToken(String tokenId) throws ScalewayApiException {
        execute(Constants.HTTP_METHOD_DELETE, Constants.ACCOUNT_URL,
                String.format(Constants.PATH_TOKENS_SLASH, tokenId), 204, null);
    }

    /**
     * Update a token to extend its expiration time by 30 minutes
     *
     * @param tokenId The ID of the token to update
     * @return The updated token with the new expiry date/time
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Token updateToken(String tokenId) throws ScalewayApiException {
        HttpPatch request = (HttpPatch) buildRequest(
                Constants.HTTP_METHOD_PATCH, new StringBuilder(Constants.ACCOUNT_URL)
                        .append(String.format(Constants.PATH_TOKENS_SLASH, tokenId)).toString(),
                new TokenPatchRequest());

        return (executeAndGetResponse(request, 200, TokenResponse.class).getToken());
    }

    /**
     * Retrieve a list of all tokens with pagination, pages start at 1 and the
     * maximum number of results per page is 100.
     * <p>
     * <strong>WARNING</strong> - you will get a list of tokens far longer than
     * the number of available tokens - some, if not all of the tokens will no
     * longer be valid and will return a '410' Gone if you use this token with
     * the getTokens(String tokenId) call.
     *
     * @param numPage    the page number to retrieve (starting at 1)
     * @param numPerPage The number of results per page (maximum 100)
     * @return The list of tokens
     * @throws ScalewayApiException If there was an error with the API call
     */
    public TokensResponse getAllTokens(int numPage, int numPerPage) throws ScalewayApiException {
        HttpRequestBase request = buildRequest(Constants.HTTP_METHOD_GET, new StringBuilder(Constants.ACCOUNT_URL)
                .append(String.format(Constants.PATH_TOKENS_PAGING, numPage, numPerPage)).toString());

        HttpResponse response = executeRequest(request);

        if (response.getStatusLine().getStatusCode() == 200) {
            Header[] allHeaders = response.getAllHeaders();
            TokensResponse tokensResponse = parseResponse(response, TokensResponse.class);
            tokensResponse.parsePaginationHeaders(allHeaders);
            return (tokensResponse);
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    /**
     * Create a new reserved IP Address
     *
     * @param organizationId The ID of the organization to attach this IP address to
     * @return The newly created IP Address
     * @throws ScalewayApiException If there was an error with the API call
     */
    public IP createIP(String organizationId) throws ScalewayApiException {
        HttpPost request = (HttpPost) buildRequest(Constants.HTTP_METHOD_POST,
                new StringBuilder(computeUrl).append(Constants.PATH_IPS).toString(), new IPRequest(organizationId));

        return (executeAndGetResponse(request, 201, IPResponse.class).getIP());

    }

    /**
     * Return a paginated list of the reserved IP addresses associated with the
     * account
     *
     * @param numPage    the page number to retrieve (starting at 1)
     * @param numPerPage The number of results per page (maximum 100)
     * @return The list of reserved IP address associated with the account
     * @throws ScalewayApiException If there was an error with the API call
     */
    public IPsResponse getAllIPs(int numPage, int numPerPage) throws ScalewayApiException {
        HttpRequestBase request = buildRequest(Constants.HTTP_METHOD_GET, new StringBuilder(computeUrl)
                .append(String.format(Constants.PATH_IPS_PAGING, numPage, numPerPage)).toString());

        HttpResponse response = executeRequest(request);

        if (response.getStatusLine().getStatusCode() == 200) {
            Header[] allHeaders = response.getAllHeaders();
            IPsResponse ipsResponse = parseResponse(response, IPsResponse.class);
            ipsResponse.parsePaginationHeaders(allHeaders);
            return (ipsResponse);
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    /**
     * Get the reserved IP address details
     *
     * @param ipId the unique identifier for the IP address
     * @return The reserved IP address details
     * @throws ScalewayApiException If there was an error with the API call
     */
    public IP getIP(String ipId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl, String.format(Constants.PATH_IPS_SLASH, ipId), 200,
                IPResponse.class).getIP());
    }

    /**
     * Attach an existing reserved IP address to a server
     *
     * @param ipId           The id of the IP Address
     * @param organizationId The ID of the organization
     * @param ipAddress      The IP Address
     * @param serverId       The ID of the Server
     * @return The IP address that was attached with updated information
     * @throws ScalewayApiException If there was an error with the API call
     */
    public IP attachIP(String ipId, String organizationId, String ipAddress, String serverId)
            throws ScalewayApiException {
        HttpPut request = (HttpPut) buildRequest(Constants.HTTP_METHOD_PUT,
                new StringBuilder(computeUrl).append(String.format(Constants.PATH_IPS_SLASH, ipId)).toString(),
                new IPPutRequest(ipAddress, ipId, serverId, organizationId));

        return (executeAndGetResponse(request, 200, IPResponse.class).getIP());
    }

    /**
     * Delete an IP with the associated IP unique identifier
     *
     * @param ipId The IP ID of the address
     * @throws ScalewayApiException If there was an error with the API call
     */
    public void deleteIP(String ipId) throws ScalewayApiException {
        execute(Constants.HTTP_METHOD_DELETE, computeUrl, String.format(Constants.PATH_IPS_SLASH, ipId), 204, null);
    }

    /**
     * Create a security group
     *
     * @param organizationId The ID of the organization to attach this to
     * @param name           The name of the security group
     * @param description    The description of the security group
     * @return The newly created security group details
     * @throws ScalewayApiException If there was an error with the API call
     */
    public SecurityGroup createSecurityGroup(String organizationId, String name, String description)
            throws ScalewayApiException {
        HttpPost request = (HttpPost) buildRequest(Constants.HTTP_METHOD_POST,
                new StringBuilder(computeUrl).append(Constants.PATH_SECURITY_GROUPS).toString(),
                new SecurityGroupRequest(organizationId, name, description));

        return (executeAndGetResponse(request, 201, SecurityGroupResponse.class).getSecurityGroup());
    }

    /**
     * Delete a security Group
     *
     * @param securityGroupId The ID of the security group to delete
     * @throws ScalewayApiException If there was an error with the API call
     */
    public void deleteSecurityGroup(String securityGroupId) throws ScalewayApiException {
        execute(Constants.HTTP_METHOD_DELETE, computeUrl,
                String.format(Constants.PATH_SECURITY_GROUPS_SLASH, securityGroupId), 204, null);
    }

    /**
     * Return a paginated list of the security groups associated with the account
     *
     * @param numPage    the page number to retrieve (starting at 1)
     * @param numPerPage The number of results per page (maximum 100)
     * @return The list of security groups associated with the account
     * @throws ScalewayApiException If there was an error with the API call
     */
    public SecurityGroupsResponse getAllSecurityGroups(int numPage, int numPerPage) throws ScalewayApiException {
        HttpRequestBase request = buildRequest(Constants.HTTP_METHOD_GET, new StringBuilder(computeUrl)
                .append(String.format(Constants.PATH_SECURITY_GROUPS_PAGING, numPage, numPerPage)).toString());

        HttpResponse response = executeRequest(request);

        if (response.getStatusLine().getStatusCode() == 200) {
            Header[] allHeaders = response.getAllHeaders();
            SecurityGroupsResponse securityGroupsResponse = parseResponse(response, SecurityGroupsResponse.class);
            securityGroupsResponse.parsePaginationHeaders(allHeaders);
            return (securityGroupsResponse);
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    /**
     * Return the security group details from the passed in ID
     *
     * @param securityGroupId The ID of the security group to look up
     * @return The details for the security group
     * @throws ScalewayApiException If there was an error with the API call
     */
    public SecurityGroup getSecurityGroup(String securityGroupId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl,
                String.format(Constants.PATH_SECURITY_GROUPS_SLASH, securityGroupId), 200,
                SecurityGroupResponse.class).getSecurityGroup());
    }

    /**
     * Update a security group with new details
     *
     * @param securityGroupId The ID of the security group to update
     * @param organizationId  The ID of the organization
     * @param name            The updated name to set
     * @param description     The updated description to set
     * @return The updated security group details
     * @throws ScalewayApiException If there was an error with the API call
     */
    public SecurityGroup updateSecurityGroup(String securityGroupId, String organizationId, String name,
            String description) throws ScalewayApiException {
        HttpPut request = (HttpPut) buildRequest(Constants.HTTP_METHOD_PUT,
                new StringBuilder(computeUrl)
                        .append(String.format(Constants.PATH_SECURITY_GROUPS_SLASH, securityGroupId)).toString(),
                new SecurityGroupRequest(organizationId, name, description));

        return (executeAndGetResponse(request, 200, SecurityGroupResponse.class).getSecurityGroup());
    }

    /**
     * Create a new rule
     *
     * @param securityGroupId The security group that this rule will be attached to
     * @param ruleAction      The action (DROP/ACCEPT)
     * @param ruleDirection   The direction (inbound/outbound)
     * @param ipRange         the IP ranges (i.e. 0.0.0.0/0
     * @param ruleProtocol    The protocol (TCP/UDP/ICMP)
     * @param destPortFrom    The destination port from
     * @return The newly created rule
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Rule createRule(String securityGroupId, RuleAction ruleAction, RuleDirection ruleDirection,
            String ipRange, RuleProtocol ruleProtocol, int destPortFrom) throws ScalewayApiException {
        HttpPost request = (HttpPost) buildRequest(Constants.HTTP_METHOD_POST,
                new StringBuilder(computeUrl)
                        .append(String.format(Constants.PATH_SECURITY_GROUPS_RULES, securityGroupId)).toString(),
                new RuleRequest(ruleAction, ruleDirection, ipRange, ruleProtocol, destPortFrom));

        return (executeAndGetResponse(request, 201, RuleResponse.class).getRule());
    }

    /**
     * Delete a rule
     *
     * @param securityGroupId The security group that this rule will be attached to
     * @param ruleId          The ID of the rule to delete
     * @throws ScalewayApiException If there was an error with the API call
     */
    public void deleteRule(String securityGroupId, String ruleId) throws ScalewayApiException {
        execute(Constants.HTTP_METHOD_DELETE, computeUrl,
                String.format(Constants.PATH_SECURITY_GROUPS_RULES_SLASH, securityGroupId, ruleId), 204, null);
    }

    /**
     * Return a paginated list of the rules associated with the security group
     *
     * @param securityGroupId The security group that this rule will be attached to
     * @param numPage         the page number to retrieve (starting at 1)
     * @param numPerPage      The number of results per page (maximum 100)
     * @return The list of security groups associated with the account
     * @throws ScalewayApiException If there was an error with the API call
     */
    public RulesResponse getAllRules(String securityGroupId, int numPage, int numPerPage)
            throws ScalewayApiException {
        HttpRequestBase request = buildRequest(Constants.HTTP_METHOD_GET, new StringBuilder(computeUrl).append(
                String.format(Constants.PATH_SECURITY_GROUPS_RULES_PAGING, securityGroupId, numPage, numPerPage))
                .toString());

        HttpResponse response = executeRequest(request);

        if (response.getStatusLine().getStatusCode() == 200) {
            Header[] allHeaders = response.getAllHeaders();
            RulesResponse rulesResponse = parseResponse(response, RulesResponse.class);
            rulesResponse.parsePaginationHeaders(allHeaders);
            return (rulesResponse);
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    /**
     * Return the rule details from the passed in ID
     *
     * @param securityGroupId The security group that this rule will be attached to
     * @param ruleId          The ID of the rule to look up
     * @return The details for the rule
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Rule getRule(String securityGroupId, String ruleId) throws ScalewayApiException {
        return (execute(Constants.HTTP_METHOD_GET, computeUrl,
                String.format(Constants.PATH_SECURITY_GROUPS_RULES_SLASH, securityGroupId, ruleId), 200,
                RuleResponse.class).getRule());
    }

    /**
     * Update a rule with new details
     *
     * @param securityGroupId The security group that this rule will be attached to
     * @param ruleId          The ID of the rule to update
     * @param ruleAction      The action (DROP/ACCEPT)
     * @param ruleDirection   The direction (inbound/outbound)
     * @param ipRange         the IP ranges (i.e. 0.0.0.0/0
     * @param ruleProtocol    The protocol (TCP/UDP/ICMP)
     * @param destPortFrom    The destination port from
     * @return The newly created rule
     * @throws ScalewayApiException If there was an error with the API call
     */
    public Rule updateRule(String securityGroupId, String ruleId, RuleAction ruleAction,
            RuleDirection ruleDirection, String ipRange, RuleProtocol ruleProtocol, int destPortFrom)
            throws ScalewayApiException {
        HttpPut request = (HttpPut) buildRequest(Constants.HTTP_METHOD_PUT,
                new StringBuilder(computeUrl)
                        .append(String.format(Constants.PATH_SECURITY_GROUPS_RULES_SLASH, securityGroupId, ruleId))
                        .toString(),
                new RuleRequest(ruleAction, ruleDirection, ipRange, ruleProtocol, destPortFrom));

        return (executeAndGetResponse(request, 200, RuleResponse.class).getRule());
    }

    /**
     * Serialize an object to JSON
     *
     * @param object The object to serialize
     * @return The object serialized as a JSON String
     * @throws JsonProcessingException if there was an error serializing
     */
    private String serializeObject(Object object) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(Include.NON_NULL);
        return (objectMapper.writeValueAsString(object));
    }

    /**
     * Execute the request, returning the parsed response object
     *
     * @param httpMethod          The HTTP method to execute (GET/POST/PATCH/DELETE/PUT)
     * @param url                 The URL to hit
     * @param path                the path to request
     * @param allowableStatusCode the allowable return HTTP status code
     * @param returnClass         the return class type
     * @return The returned and parsed response
     * @throws ScalewayApiException If there was an error calling the api
     */
    private <T> T execute(String httpMethod, String url, String path, int allowableStatusCode, Class<T> returnClass)
            throws ScalewayApiException {
        String requestPath = new StringBuilder(url).append(path).toString();

        HttpRequestBase request = buildRequest(httpMethod, requestPath);

        request.setHeader(Constants.HEADER_KEY_AUTH_TOKEN, accessToken);
        request.setHeader(HttpHeaders.CONTENT_TYPE, Constants.HEADER_VALUE_JSON_APPLICATION);

        HttpResponse response;
        try {
            LOGGER.debug("Executing '{}' for url '{}'", httpMethod, requestPath);
            response = httpclient.execute(request);
        } catch (IOException ex) {
            throw new ScalewayApiException(ex);
        }

        int statusCode = response.getStatusLine().getStatusCode();

        if (statusCode == allowableStatusCode) {
            LOGGER.debug("Status code received: {}, wanted: {}.", statusCode, allowableStatusCode);
            if (null != returnClass) {
                return parseResponse(response, returnClass);
            } else {
                return (null);
            }
        } else {
            LOGGER.error("Invalid status code received: {}, wanted: {}.", statusCode, allowableStatusCode);
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    /**
     * Build the request for the URI - which just returns a HttpRequestBase Object
     * of the correct matching type for the passed in httpMethod
     *
     * @param httpMethod  The HTTP method to build for
     * @param requestPath The full URI to build
     * @return the base HTTP object
     * @throws ScalewayApiException If there was an error with the API call
     */
    private HttpRequestBase buildRequest(String httpMethod, String requestPath) throws ScalewayApiException {
        return (buildRequest(httpMethod, requestPath, null));
    }

    private HttpRequestBase buildRequest(String httpMethod, String requestPath, Object entityContent)
            throws ScalewayApiException {
        LOGGER.debug("Building request for method '{}' and URL '{}'", httpMethod, requestPath);

        HttpRequestBase request = null;
        switch (httpMethod) {
        case Constants.HTTP_METHOD_GET:
            request = new HttpGet(requestPath);
            break;
        case Constants.HTTP_METHOD_POST:
            request = new HttpPost(requestPath);
            break;
        case Constants.HTTP_METHOD_DELETE:
            request = new HttpDelete(requestPath);
            break;
        case Constants.HTTP_METHOD_PATCH:
            request = new HttpPatch(requestPath);
            break;
        case Constants.HTTP_METHOD_PUT:
            request = new HttpPut(requestPath);
            break;
        }

        request.setHeader(Constants.HEADER_KEY_AUTH_TOKEN, accessToken);
        request.setHeader(HttpHeaders.CONTENT_TYPE, Constants.HEADER_VALUE_JSON_APPLICATION);

        if (null != entityContent) {
            if (request instanceof HttpEntityEnclosingRequestBase) {
                try {
                    StringEntity entity = new StringEntity(serializeObject(entityContent));
                    ((HttpEntityEnclosingRequestBase) request).setEntity(entity);
                } catch (UnsupportedEncodingException | JsonProcessingException ex) {
                    throw new ScalewayApiException(ex);
                }
            } else {
                LOGGER.error("Attempting to set entity on non applicable base class of '{}'", request.getClass());
            }
        }
        return request;
    }

    private <T> T executeAndGetResponse(HttpRequestBase request, int allowableStatusCode, Class<T> returnClass)
            throws ScalewayApiException {
        HttpResponse response = executeRequest(request);
        if (response.getStatusLine().getStatusCode() == allowableStatusCode) {
            if (null != returnClass) {
                return parseResponse(response, returnClass);
            } else {
                return (null);
            }
        } else {
            try {
                throw new ScalewayApiException(IOUtils.toString(response.getEntity().getContent()));
            } catch (UnsupportedOperationException | IOException ex) {
                throw new ScalewayApiException(ex);
            }
        }
    }

    private HttpResponse executeRequest(HttpRequestBase request) throws ScalewayApiException {
        try {
            return httpclient.execute(request);
        } catch (IOException ex) {
            throw new ScalewayApiException(ex);
        }
    }

    private <T> T parseResponse(HttpResponse response, Class<T> entityClass) throws ScalewayApiException {
        try {
            return parseJson(response.getEntity(), entityClass);
        } catch (IOException ex) {
            throw new ScalewayApiException(ex);
        }
    }

    private ObjectMapper initializeObjectMapperJson() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
        mapper.enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT);
        mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
        mapper.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return mapper;
    }

    private <T> T parseJson(HttpEntity responseEntity, Class<T> type) throws IOException {
        String encoding = responseEntity.getContentEncoding() != null
                ? responseEntity.getContentEncoding().getValue()
                : "UTF-8";
        String jsonString = IOUtils.toString(responseEntity.getContent(), encoding);

        LOGGER.debug("JSON response was '{}'", jsonString);

        try {
            return initializeObjectMapperJson().readValue(jsonString, type);
        } catch (Exception ex) {
            LOGGER.error("%s", jsonString);
            throw ex;
        }
    }
}