com.myjeeva.digitalocean.impl.DigitalOceanClient.java Source code

Java tutorial

Introduction

Here is the source code for com.myjeeva.digitalocean.impl.DigitalOceanClient.java

Source

/*
 * The MIT License
 * 
 * Copyright (c) 2010-2014 Jeevanandam M. (myjeeva.com)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.myjeeva.digitalocean.impl;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.myjeeva.digitalocean.DigitalOcean;
import com.myjeeva.digitalocean.common.ActionType;
import com.myjeeva.digitalocean.common.ApiAction;
import com.myjeeva.digitalocean.common.Constants;
import com.myjeeva.digitalocean.common.RequestMethod;
import com.myjeeva.digitalocean.exception.DigitalOceanException;
import com.myjeeva.digitalocean.exception.RequestUnsuccessfulException;
import com.myjeeva.digitalocean.pojo.Action;
import com.myjeeva.digitalocean.pojo.Actions;
import com.myjeeva.digitalocean.pojo.Backups;
import com.myjeeva.digitalocean.pojo.Domain;
import com.myjeeva.digitalocean.pojo.DomainRecord;
import com.myjeeva.digitalocean.pojo.DomainRecords;
import com.myjeeva.digitalocean.pojo.Domains;
import com.myjeeva.digitalocean.pojo.Droplet;
import com.myjeeva.digitalocean.pojo.DropletAction;
import com.myjeeva.digitalocean.pojo.Droplets;
import com.myjeeva.digitalocean.pojo.Image;
import com.myjeeva.digitalocean.pojo.ImageAction;
import com.myjeeva.digitalocean.pojo.Images;
import com.myjeeva.digitalocean.pojo.Kernels;
import com.myjeeva.digitalocean.pojo.Key;
import com.myjeeva.digitalocean.pojo.Keys;
import com.myjeeva.digitalocean.pojo.Regions;
import com.myjeeva.digitalocean.pojo.Sizes;
import com.myjeeva.digitalocean.pojo.Snapshots;
import com.myjeeva.digitalocean.serializer.DropletSerializer;

/**
 * DigitalOcean API client wrapper methods Implementation
 * 
 * @author Jeevanandam M. (jeeva@myjeeva.com)
 */
public class DigitalOceanClient implements DigitalOcean, Constants {

    private final Logger LOG = LoggerFactory.getLogger(DigitalOceanClient.class);

    /**
     * Http client
     */
    protected HttpClient httpClient;

    /**
     * OAuth Authorization Token for Accessing DigitalOcean API
     */
    protected String authToken;

    /**
     * DigitalOcean API version. defaults to v2 from constructor
     */
    protected String apiVersion;

    /**
     * DigitalOcean API Host is <code>api.digitalocean.com</code>
     */
    protected String apiHost = "api.digitalocean.com";

    /**
     * Gson Parser instance for deserialize
     */
    private Gson deserialize;

    /**
     * Gson Parser instance for serialize
     */
    private Gson serialize;

    /**
     * JSON Parser instance
     */
    private JsonParser jsonParser;

    public DigitalOceanClient(String authToken) {
        this("v2", authToken);
    }

    /**
     * DigitalOcean Client Constructor
     * 
     * @param apiVersion a {@link String} object
     * @param authToken a {@link String} object
     */
    public DigitalOceanClient(String apiVersion, String authToken) {
        this(apiVersion, authToken, null);
    }

    /**
     * DigitalOcean Client Constructor
     * 
     * @param apiVersion a {@link String} object
     * @param authToken a {@link String} object
     * @param httpClient a {@link HttpClient} object
     */
    public DigitalOceanClient(String apiVersion, String authToken, HttpClient httpClient) {

        if (!"v2".equalsIgnoreCase(apiVersion)) {
            throw new IllegalArgumentException("Only API version 2 is supported.");
        }

        this.apiVersion = apiVersion;
        this.authToken = authToken;
        this.httpClient = httpClient;
        initialize();
    }

    /**
     * @return the httpClient
     */
    public HttpClient getHttpClient() {
        return httpClient;
    }

    /**
     * @param httpClient the httpClient to set
     */
    public void setHttpClient(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    /**
     * @return the authToken
     */
    public String getAuthToken() {
        return authToken;
    }

    /**
     * @param authToken the authToken to set
     */
    public void setAuthToken(String authToken) {
        this.authToken = authToken;
    }

    /**
     * @return the apiVersion
     */
    public String getApiVersion() {
        return apiVersion;
    }

    /**
     * @param apiVersion the apiVersion to set
     */
    public void setApiVersion(String apiVersion) {
        this.apiVersion = apiVersion;
    }

    // =======================================
    // Droplet access/manipulation methods
    // =======================================

    @Override
    public Droplets getAvailableDroplets(Integer pageNo)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validatePageNo(pageNo);

        return (Droplets) perform(new ApiRequest(ApiAction.AVAILABLE_DROPLETS, pageNo)).getData();
    }

    @Override
    public Kernels getAvailableKernels(Integer dropletId, Integer pageNo)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletIdAndPageNo(dropletId, pageNo);

        Object[] params = { dropletId };
        return (Kernels) perform(new ApiRequest(ApiAction.AVAILABLE_DROPLETS_KERNELS, params, pageNo)).getData();
    }

    @Override
    public Snapshots getAvailableSnapshots(Integer dropletId, Integer pageNo)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletIdAndPageNo(dropletId, pageNo);

        Object[] params = { dropletId };
        return (Snapshots) perform(new ApiRequest(ApiAction.GET_DROPLET_SNAPSHOTS, params, pageNo)).getData();
    }

    @Override
    public Backups getAvailableBackups(Integer dropletId, Integer pageNo)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletIdAndPageNo(dropletId, pageNo);

        Object[] params = { dropletId };
        return (Backups) perform(new ApiRequest(ApiAction.GET_DROPLET_BACKUPS, params, pageNo)).getData();
    }

    @Override
    public Droplet getDropletInfo(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Droplet) perform(new ApiRequest(ApiAction.GET_DROPLET_INFO, params)).getData();
    }

    @Override
    public Droplet createDroplet(Droplet droplet) throws DigitalOceanException, RequestUnsuccessfulException {
        if (null == droplet || null == droplet.getName() || null == droplet.getRegion() || null == droplet.getSize()
                || (null == droplet.getImage()
                        || (null == droplet.getImage().getId() && null == droplet.getImage().getSlug()))) {
            throw new IllegalArgumentException(
                    "Missing required parameters [Name, Region Slug, Size Slug, Image Id/Slug] for create droplet.");
        }

        return (Droplet) perform(new ApiRequest(ApiAction.CREATE_DROPLET, droplet)).getData();
    }

    @Override
    public Boolean deleteDroplet(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Boolean) perform(new ApiRequest(ApiAction.DELETE_DROPLET, params)).getData();
    }

    // Droplet action methods

    @Override
    public Action rebootDroplet(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(
                new ApiRequest(ApiAction.REBOOT_DROPLET, new DropletAction(ActionType.REBOOT), params)).getData();
    }

    @Override
    public Action powerCycleDroplet(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(
                new ApiRequest(ApiAction.POWER_CYCLE_DROPLET, new DropletAction(ActionType.POWER_CYCLE), params))
                        .getData();
    }

    @Override
    public Action shutdownDroplet(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(
                new ApiRequest(ApiAction.SHUTDOWN_DROPLET, new DropletAction(ActionType.SHUTDOWN), params))
                        .getData();
    }

    @Override
    public Action powerOffDroplet(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(
                new ApiRequest(ApiAction.POWER_OFF_DROPLET, new DropletAction(ActionType.POWER_OFF), params))
                        .getData();
    }

    @Override
    public Action powerOnDroplet(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(
                new ApiRequest(ApiAction.POWER_ON_DROPLET, new DropletAction(ActionType.POWER_ON), params))
                        .getData();
    }

    @Override
    public Action resetDropletPassword(Integer dropletId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(new ApiRequest(ApiAction.RESET_DROPLET_PASSWORD,
                new DropletAction(ActionType.PASSWORD_RESET), params)).getData();
    }

    @Override
    public Action resizeDroplet(Integer dropletId, String size)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        DropletAction action = new DropletAction(ActionType.RESIZE);
        action.setSize(size);
        return (Action) perform(new ApiRequest(ApiAction.RESIZE_DROPLET, action, params)).getData();
    }

    @Override
    public Action takeDropletSnapshot(Integer dropletId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(
                new ApiRequest(ApiAction.SNAPSHOT_DROPLET, new DropletAction(ActionType.SNAPSHOT), params))
                        .getData();
    }

    @Override
    public Action takeDropletSnapshot(Integer dropletId, String snapshotName)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        DropletAction action = new DropletAction(ActionType.SNAPSHOT);
        action.setName(snapshotName);
        return (Action) perform(new ApiRequest(ApiAction.SNAPSHOT_DROPLET, action, params)).getData();
    }

    @Override
    public Action restoreDroplet(Integer dropletId, Integer imageId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        DropletAction action = new DropletAction(ActionType.RESTORE);
        action.setImage(imageId);
        return (Action) perform(new ApiRequest(ApiAction.RESTORE_DROPLET, action, params)).getData();
    }

    @Override
    public Action rebuildDroplet(Integer dropletId, Integer imageId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        DropletAction action = new DropletAction(ActionType.REBUILD);
        action.setImage(imageId);
        return (Action) perform(new ApiRequest(ApiAction.REBUILD_DROPLET, action, params)).getData();
    }

    @Override
    public Action disableDropletBackups(Integer dropletId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(new ApiRequest(ApiAction.DISABLE_DROPLET_BACKUPS,
                new DropletAction(ActionType.DISABLE_BACKUPS), params)).getData();
    }

    @Override
    public Action renameDroplet(Integer dropletId, String name)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        DropletAction action = new DropletAction(ActionType.RENAME);
        action.setName(name);
        return (Action) perform(new ApiRequest(ApiAction.RENAME_DROPLET, action, params)).getData();
    }

    @Override
    public Action changeDropletKernel(Integer dropletId, Integer kernelId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        DropletAction action = new DropletAction(ActionType.CHANGE_KERNEL);
        action.setKernel(kernelId);
        return (Action) perform(new ApiRequest(ApiAction.CHANGE_DROPLET_KERNEL, action, params)).getData();
    }

    @Override
    public Action enableDropletIpv6(Integer dropletId) throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(
                new ApiRequest(ApiAction.ENABLE_DROPLET_IPV6, new DropletAction(ActionType.ENABLE_IPV6), params))
                        .getData();
    }

    @Override
    public Action enableDropletPrivateNetworking(Integer dropletId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletId(dropletId);

        Object[] params = { dropletId };
        return (Action) perform(new ApiRequest(ApiAction.ENABLE_DROPLET_PRIVATE_NETWORKING,
                new DropletAction(ActionType.ENABLE_PRIVATE_NETWORKING), params)).getData();
    }

    // ==============================================
    // Actions manipulation/access methods
    // ==============================================

    @Override
    public Actions getAvailableActions(Integer pageNo) throws DigitalOceanException, RequestUnsuccessfulException {
        validatePageNo(pageNo);
        return (Actions) perform(new ApiRequest(ApiAction.AVAILABLE_ACTIONS, pageNo)).getData();
    }

    @Override
    public Action getActionInfo(Integer actionId) throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(actionId, "Missing required parameter - actionId");

        Object[] params = { actionId };
        return (Action) perform(new ApiRequest(ApiAction.GET_ACTION_INFO, params)).getData();
    }

    @Override
    public Actions getAvailableDropletActions(Integer dropletId, Integer pageNo)
            throws DigitalOceanException, RequestUnsuccessfulException {
        validateDropletIdAndPageNo(dropletId, pageNo);

        Object[] params = { dropletId };
        return (Actions) perform(new ApiRequest(ApiAction.GET_DROPLET_ACTIONS, params, pageNo)).getData();
    }

    @Override
    public Actions getAvailableImageActions(Integer imageId, Integer pageNo)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(imageId, "Missing required parameter - imageId.");
        validatePageNo(pageNo);

        Object[] params = { imageId };
        return (Actions) perform(new ApiRequest(ApiAction.GET_IMAGE_ACTIONS, params, pageNo)).getData();
    }

    // =======================================
    // Images access/manipulation methods
    // =======================================

    @Override
    public Images getAvailableImages(Integer pageNo) throws DigitalOceanException, RequestUnsuccessfulException {
        validatePageNo(pageNo);
        return (Images) perform(new ApiRequest(ApiAction.AVAILABLE_IMAGES, pageNo)).getData();
    }

    @Override
    public Image getImageInfo(Integer imageId) throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(imageId, "Missing required parameter - imageId.");

        Object[] params = { imageId };
        return (Image) perform(new ApiRequest(ApiAction.GET_IMAGE_INFO, params)).getData();
    }

    @Override
    public Image getImageInfo(String slug) throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(slug, "Missing required parameter - slug.");

        Object[] params = { slug };
        return (Image) perform(new ApiRequest(ApiAction.GET_IMAGE_INFO, params)).getData();
    }

    @Override
    public Image updateImage(Image image) throws DigitalOceanException, RequestUnsuccessfulException {
        if (null == image || null == image.getName()) {
            throw new IllegalArgumentException("Missing required parameter - image object.");
        }

        Object[] params = { image.getId() };
        return (Image) perform(new ApiRequest(ApiAction.UPDATE_IMAGE_INFO, image, params)).getData();
    }

    @Override
    public Boolean deleteImage(Integer imageId) throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(imageId, "Missing required parameter - imageId.");

        Object[] params = { imageId };
        return (Boolean) perform(new ApiRequest(ApiAction.DELETE_IMAGE, params)).getData();
    }

    @Override
    public Action transferImage(Integer imageId, String regionSlug)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(imageId, "Missing required parameter - imageId.");
        checkEmptyAndThrowError(regionSlug, "Missing required parameter - regionSlug.");

        Object[] params = { imageId };
        return (Action) perform(
                new ApiRequest(ApiAction.TRANSFER_IMAGE, new ImageAction(ActionType.TRANSFER, regionSlug), params))
                        .getData();
    }

    // =======================================
    // Regions methods
    // =======================================

    @Override
    public Regions getAvailableRegions(Integer pageNo) throws DigitalOceanException, RequestUnsuccessfulException {
        validatePageNo(pageNo);
        return (Regions) perform(new ApiRequest(ApiAction.AVAILABLE_REGIONS, pageNo)).getData();
    }

    // =======================================
    // Sizes methods
    // =======================================

    @Override
    public Sizes getAvailableSizes(Integer pageNo) throws DigitalOceanException, RequestUnsuccessfulException {
        validatePageNo(pageNo);
        return (Sizes) perform(new ApiRequest(ApiAction.AVAILABLE_SIZES, pageNo)).getData();
    }

    // =======================================
    // Domain methods
    // =======================================

    @Override
    public Domains getAvailableDomains(Integer pageNo) throws DigitalOceanException, RequestUnsuccessfulException {
        return (Domains) perform(new ApiRequest(ApiAction.AVAILABLE_DOMAINS, pageNo)).getData();
    }

    @Override
    public Domain getDomainInfo(String domainName) throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domainName, "Missing required parameter - domainName.");

        Object[] params = { domainName };
        return (Domain) perform(new ApiRequest(ApiAction.GET_DOMAIN_INFO, params)).getData();
    }

    @Override
    public Domain createDomain(Domain domain) throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domain.getName(), "Missing required parameter - domainName.");
        checkEmptyAndThrowError(domain.getIpAddress(), "Missing required parameter - ipAddress.");

        return (Domain) perform(new ApiRequest(ApiAction.CREATE_DOMAIN, domain)).getData();
    }

    @Override
    public Boolean deleteDomain(String domainName) throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domainName, "Missing required parameter - domainName.");

        Object[] params = { domainName };
        return (Boolean) perform(new ApiRequest(ApiAction.DELETE_DOMAIN, params)).getData();
    }

    @Override
    public DomainRecords getDomainRecords(String domainName)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domainName, "Missing required parameter - domainName.");

        Object[] params = { domainName };
        return (DomainRecords) perform(new ApiRequest(ApiAction.GET_DOMAIN_RECORDS, params)).getData();
    }

    @Override
    public DomainRecord getDomainRecordInfo(String domainName, Integer recordId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domainName, "Missing required parameter - domainName.");
        checkNullAndThrowError(recordId, "Missing required parameter - recordId.");

        Object[] params = { domainName, recordId };
        return (DomainRecord) perform(new ApiRequest(ApiAction.GET_DOMAIN_RECORD_INFO, params)).getData();
    }

    @Override
    public DomainRecord createDomainRecord(String domainName, DomainRecord domainRecord)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domainName, "Missing required parameter - domainName.");
        if (null == domainRecord) {
            throw new IllegalArgumentException("Missing required parameter - domainRecord");
        }

        Object[] params = { domainName };
        return (DomainRecord) perform(new ApiRequest(ApiAction.CREATE_DOMAIN_RECORD, domainRecord, params))
                .getData();
    }

    @Override
    public DomainRecord updateDomainRecord(String domainName, Integer recordId, String name)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domainName, "Missing required parameter - domainName.");
        checkNullAndThrowError(recordId, "Missing required parameter - recordId.");
        checkEmptyAndThrowError(name, "Missing required parameter - name.");

        Object[] params = { domainName, recordId };
        return (DomainRecord) perform(
                new ApiRequest(ApiAction.UPDATE_DOMAIN_RECORD, new DomainRecord(name), params)).getData();
    }

    @Override
    public Boolean deleteDomainRecord(String domainName, Integer recordId)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(domainName, "Missing required parameter - domainName.");
        checkNullAndThrowError(recordId, "Missing required parameter - recordId.");

        Object[] params = { domainName, recordId };
        return (Boolean) perform(new ApiRequest(ApiAction.DELETE_DOMAIN_RECORD, params)).getData();
    }

    @Override
    public Keys getAvailableKeys(Integer pageNo) throws DigitalOceanException, RequestUnsuccessfulException {
        return (Keys) perform(new ApiRequest(ApiAction.AVAILABLE_KEYS, pageNo)).getData();
    }

    @Override
    public Key getKeyInfo(Integer sshKeyId) throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(sshKeyId, "Missing required parameter - sshKeyId.");

        Object[] params = { sshKeyId };
        return (Key) perform(new ApiRequest(ApiAction.GET_KEY_INFO, params)).getData();
    }

    @Override
    public Key getKeyInfo(String fingerprint) throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(fingerprint, "Missing required parameter - fingerprint.");

        Object[] params = { fingerprint };
        return (Key) perform(new ApiRequest(ApiAction.GET_KEY_INFO, params)).getData();
    }

    @Override
    public Key createKey(Key newKey) throws DigitalOceanException, RequestUnsuccessfulException {
        if (null == newKey) {
            throw new IllegalArgumentException("Missing required parameter - newKey");
        }
        checkEmptyAndThrowError(newKey.getName(), "Missing required parameter - name.");
        checkEmptyAndThrowError(newKey.getPublicKey(), "Missing required parameter - publicKey.");

        return (Key) perform(new ApiRequest(ApiAction.CREATE_KEY, newKey)).getData();
    }

    @Override
    public Key updateKey(Integer sshKeyId, String newSshKeyName)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(sshKeyId, "Missing required parameter - sshKeyId.");
        checkEmptyAndThrowError(newSshKeyName, "Missing required parameter - newSshKeyName.");

        Object[] params = { sshKeyId };
        return (Key) perform(new ApiRequest(ApiAction.UPDATE_KEY, new Key(newSshKeyName), params)).getData();
    }

    @Override
    public Key updateKey(String fingerprint, String newSshKeyName)
            throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(fingerprint, "Missing required parameter - fingerprint.");
        checkEmptyAndThrowError(newSshKeyName, "Missing required parameter - newSshKeyName.");

        Object[] params = { fingerprint };
        return (Key) perform(new ApiRequest(ApiAction.UPDATE_KEY, new Key(newSshKeyName), params)).getData();
    }

    @Override
    public Boolean deleteKey(Integer sshKeyId) throws DigitalOceanException, RequestUnsuccessfulException {
        checkNullAndThrowError(sshKeyId, "Missing required parameter - sshKeyId.");

        Object[] params = { sshKeyId };
        return (Boolean) perform(new ApiRequest(ApiAction.DELETE_KEY, params)).getData();
    }

    @Override
    public Boolean deleteKey(String fingerprint) throws DigitalOceanException, RequestUnsuccessfulException {
        checkEmptyAndThrowError(fingerprint, "Missing required parameter - fingerprint.");

        Object[] params = { fingerprint };
        return (Boolean) perform(new ApiRequest(ApiAction.DELETE_KEY, params)).getData();
    }

    private ApiResponse perform(ApiRequest request) throws DigitalOceanException, RequestUnsuccessfulException {

        URI uri = createUri(request);
        String response = null;

        if (RequestMethod.GET == request.getMethod()) {
            response = doGet(uri);
        } else if (RequestMethod.POST == request.getMethod()) {
            response = doPost(uri, createRequestData(request));
        } else if (RequestMethod.PUT == request.getMethod()) {
            response = doPut(uri, createRequestData(request));
        } else if (RequestMethod.DELETE == request.getMethod()) {
            response = doDelete(uri);
        }

        ApiResponse apiResponse = new ApiResponse(request.getApiAction(), true);

        try {
            if ("true".equals(response) || "false".equals(response)) {
                apiResponse.setData(Boolean.valueOf(response));
            } else {
                if (request.getElementName().endsWith("s")) {
                    apiResponse.setData(deserialize.fromJson(response, request.getClazz()));
                } else {
                    JsonElement element = jsonParser.parse(response).getAsJsonObject()
                            .get(request.getElementName());
                    apiResponse.setData(deserialize.fromJson(element, request.getClazz()));
                }
            }
        } catch (JsonSyntaxException jse) {
            LOG.error("Error occurred while parsing response", jse);
            apiResponse.setRequestSuccess(false);
        }

        LOG.debug("API Response:: " + apiResponse.toString());

        return apiResponse;
    }

    private String doGet(URI uri) throws DigitalOceanException, RequestUnsuccessfulException {
        HttpGet get = new HttpGet(uri);
        get.setHeaders(getRequestHeaders());
        return executeHttpRequest(get);
    }

    private String doPost(URI uri, StringEntity entity) throws DigitalOceanException, RequestUnsuccessfulException {
        HttpPost post = new HttpPost(uri);
        post.setHeaders(getRequestHeaders());

        if (null != entity) {
            post.setEntity(entity);
        }

        return executeHttpRequest(post);
    }

    private String doPut(URI uri, StringEntity entity) throws DigitalOceanException, RequestUnsuccessfulException {
        HttpPut put = new HttpPut(uri);
        put.setHeaders(getRequestHeaders());

        if (null != entity) {
            put.setEntity(entity);
        }

        return executeHttpRequest(put);
    }

    private String doDelete(URI uri) throws DigitalOceanException, RequestUnsuccessfulException {
        HttpDelete delete = new HttpDelete(uri);
        delete.setHeaders(getRequestHeaders());
        delete.setHeader("Content-Type", FORM_URLENCODED_CONTENT_TYPE);
        return executeHttpRequest(delete);
    }

    private String executeHttpRequest(HttpRequestBase request)
            throws DigitalOceanException, RequestUnsuccessfulException {
        String response = "";
        try {
            HttpResponse httpResponse = httpClient.execute(request);
            LOG.debug("HTTP Response Object:: " + httpResponse);

            response = appendRateLimitValues(evaluateResponse(httpResponse), httpResponse);
            LOG.debug("Parsed Response:: " + response);
        } catch (ClientProtocolException cpe) {
            throw new RequestUnsuccessfulException(cpe.getMessage(), cpe);
        } catch (IOException ioe) {
            throw new RequestUnsuccessfulException(ioe.getMessage(), ioe);
        } finally {
            request.releaseConnection();
        }

        return response;
    }

    private String evaluateResponse(HttpResponse httpResponse) throws DigitalOceanException {
        int statusCode = httpResponse.getStatusLine().getStatusCode();
        String response = "";

        if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_CREATED == statusCode
                || HttpStatus.SC_ACCEPTED == statusCode) {
            response = httpResponseToString(httpResponse);
        } else if (HttpStatus.SC_NO_CONTENT == statusCode) {
            response = "true";
        }

        if (statusCode >= 400 && statusCode < 510) {
            String jsonStr = httpResponseToString(httpResponse);
            LOG.debug("JSON Response: " + jsonStr);

            JsonObject jsonObj = jsonParser.parse(jsonStr).getAsJsonObject();
            String id = jsonObj.get("id").getAsString();
            String errorMsg = String.format("\nHTTP Status Code: %s\nError Id: %s\nError Message: %s", statusCode,
                    id, jsonObj.get("message").getAsString());
            LOG.debug(errorMsg);
            throw new DigitalOceanException(errorMsg, id, statusCode);
        }

        return response;
    }

    private String httpResponseToString(HttpResponse httpResponse) {
        String response = "";
        if (null != httpResponse.getEntity()) {
            try {
                response = EntityUtils.toString(httpResponse.getEntity(), UTF_8);
            } catch (ParseException pe) {
                LOG.error(pe.getMessage(), pe);
            } catch (IOException ioe) {
                LOG.error(ioe.getMessage(), ioe);
            }
        }
        return response;
    }

    private URI createUri(ApiRequest request) {
        URIBuilder ub = new URIBuilder();
        ub.setScheme(HTTPS_SCHEME);
        ub.setHost(apiHost);
        ub.setPath(createPath(request));

        if (null != request.getPageNo()) {
            ub.setParameter(PARAM_PAGE_NO, request.getPageNo().toString());
        }

        URI uri = null;
        try {
            uri = ub.build();
        } catch (URISyntaxException use) {
            LOG.error(use.getMessage(), use);
        }

        return uri;
    }

    private Header[] getRequestHeaders() {
        Header[] headers = { new BasicHeader("X-User-Agent", "DigitalOcean API Client by myjeeva.com"),
                new BasicHeader("Content-Type", JSON_CONTENT_TYPE),
                new BasicHeader("Authorization", "Bearer " + authToken) };
        return headers;
    }

    private String createPath(ApiRequest request) {
        String path = URL_PATH_SEPARATOR + apiVersion + request.getApiAction().getPath();
        return (null == request.getParams() ? path : String.format(path, request.getParams()));
    }

    private StringEntity createRequestData(ApiRequest request) {
        StringEntity data = null;

        if (null != request.getData()) {
            String inputData = serialize.toJson(request.getData());
            data = new StringEntity(inputData, ContentType.create(JSON_CONTENT_TYPE));
        }

        return data;
    }

    private String appendRateLimitValues(String response, HttpResponse httpResponse) {
        if (StringUtils.isEmpty(response)) {
            return "";
        }

        String rateLimitData = String.format(RATE_LIMIT_JSON_STRUCT,
                httpResponse.getFirstHeader("RateLimit-Limit").getValue(),
                httpResponse.getFirstHeader("RateLimit-Remaining").getValue(),
                getDateString(httpResponse.getFirstHeader("RateLimit-Reset").getValue(), DATE_FORMAT));

        return StringUtils.substringBeforeLast(response, "}") + ", " + rateLimitData + "}";
    }

    private String getDateString(String epochString, String dateFormat) {
        long epoch = Long.parseLong(epochString);
        Date expiry = new Date(epoch * 1000);

        SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);
        String dateString = formatter.format(expiry);
        LOG.debug(dateString);
        return dateString;
    }

    // =======================================
    // Validation methods
    // =======================================

    private void validateDropletIdAndPageNo(Integer dropletId, Integer pageNo) {
        validateDropletId(dropletId);
        validatePageNo(pageNo);
    }

    private void validateDropletId(Integer dropletId) {
        checkNullAndThrowError(dropletId, "Missing required parameter - dropletId.");
    }

    private void validatePageNo(Integer pageNo) {
        checkNullAndThrowError(pageNo, "Missing required parameter - pageNo.");
    }

    private void checkNullAndThrowError(Integer integer, String msg) {
        if (null == integer) {
            LOG.error(msg);
            throw new IllegalArgumentException(msg);
        }
    }

    private void checkEmptyAndThrowError(String str, String msg) {
        if (StringUtils.isEmpty(str)) {
            LOG.error(msg);
            throw new IllegalArgumentException(msg);
        }
    }

    private void initialize() {
        this.deserialize = new GsonBuilder().setDateFormat(DATE_FORMAT).create();

        this.serialize = new GsonBuilder().setDateFormat(DATE_FORMAT)
                .registerTypeAdapter(Droplet.class, new DropletSerializer()).excludeFieldsWithoutExposeAnnotation()
                .create();

        this.jsonParser = new JsonParser();

        if (null == this.httpClient) {
            this.httpClient = new DefaultHttpClient(new PoolingClientConnectionManager());
        }
    }
}