com.cloudbees.api.BeesClient.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudbees.api.BeesClient.java

Source

/*
 * Copyright 2010-2012, CloudBees Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cloudbees.api;

import com.cloudbees.api.config.ConfigParameters;
import com.cloudbees.api.config.ParameterSettings;
import com.cloudbees.api.config.ResourceSettings;
import com.cloudbees.api.oauth.OauthClient;
import com.cloudbees.upload.ArchiveUtils;
import com.cloudbees.upload.JarUtils;
import com.cloudbees.utils.AppConfigHelper;
import com.cloudbees.utils.ZipHelper;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.mapper.MapperWrapper;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.io.IOUtils;
import org.codehaus.jackson.map.DeserializationConfig.Feature;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.introspect.VisibilityChecker.Std;
import org.codehaus.jettison.json.JSONObject;

import java.io.*;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;

import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.ANY;
import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.NONE;

/**
 * Main entry point to the CloudBees API.
 *
 * @author Fabian Donze
 * @author Kohsuke Kawaguchi
 */
public class BeesClient extends BeesClientBase {
    /**
     * The encoded value we send in as the "Authorization" header.
     */
    private String encodedAccountAuthorization;

    /**
     * The API endpoint, such as "https://api.cloudbees.com/"
     */
    private URL base;

    static Logger logger = Logger.getLogger(BeesClient.class.getSimpleName());

    /**
     * Used for mapping JSON to Java objects.
     */
    /*package*/ static final ObjectMapper MAPPER = new ObjectMapper();

    static {
        MAPPER.setVisibilityChecker(new Std(NONE, NONE, NONE, NONE, ANY));
        MAPPER.getDeserializationConfig().set(Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    public BeesClient(BeesClientConfiguration beesClientConfiguration) {
        super(beesClientConfiguration);
        init();
    }

    /**
     * Creates a client that talks to the CloudBees production API endpoint
     *
     * To obtain your API key an the secret, visit https://grandcentral.cloudbees.com/user/keys
     *
     * If you have reigstered an OAuth application and talking to CloudBees as that OAuth
     * application (for example to validate OAuth tokens you received from your users via browser),
     * then you can specify that OAuth client ID and the secret.
     */
    public BeesClient(String apikey, String secret) {
        this("https://api.cloudbees.com/api ", apikey, secret);
    }

    public BeesClient(String server, String apikey, String secret) {
        this(server, apikey, secret, null, null);
    }

    public BeesClient(String server, String apikey, String secret, String format, String version) {
        // TODO: this encodePassword is considered harmful as it creates assymetry between two constructors
        super(server, apikey, encodePassword(secret, version), format, version);
        init();
    }

    /**
     * Common initialization code in this class.
     */
    private void init() {
        BeesClientConfiguration conf = getBeesClientConfiguration();
        try {
            base = new URL(conf.getServerApiUrl());
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Invalid API URL:" + conf.getServerApiUrl(), e);
        }
        encodedAccountAuthorization = conf.getAuthorization();
    }

    /**
     * Obtains another interface of the API client that defines OAuth APIs.
     *
     * In API world, api-staging is not in a separate environment,
     * it is part of the main production environment but offers the latest
     * and greatest interface to the production data model.
     *
     * URL Resolution:
     * api.cloudbees.com         => grandcentral.cloudbees.com
     * api-staging.cloudbees.com => grandcentral.cloudbees.com
     * api.beescloud.com         => grandcentral.beescloud.com
     * api-staging.beescloud.com => grandcentral.beescloud.com
     *
     * <p>
     * This method does not involve any remote call.
     *
     * @return never null
     */
    public OauthClient getOauthClient() {
        String gc = base.toExternalForm();
        gc = gc.replace("//api.", "//grandcentral.");
        gc = gc.replace("//api-staging.", "//grandcentral.");

        if (gc.equals(base.toExternalForm())) {
            throw new RuntimeException("Unable to determine GrandCentral URL from " + base.toExternalForm());
        }

        // cut off the path portion
        while (true) {
            int scheme = gc.indexOf("://");
            int path = gc.lastIndexOf('/');
            if (path > scheme + 3) {
                gc = gc.substring(0, path);
            } else
                break;
        }

        return new OauthClientImpl(this, gc);
    }

    /**
     * Create OAuth client using the given GC URL. It's useful for client apps who want to test their functionality with
     * GC instances running locally or elsewhere for individual testing.
     *
     * It also decouples RUN api instance from GC instance, useful in certain test scenarios
     *
     * @param gcUrl If null defaults to https://grandcentral.cloudbees.com
     *
     */
    public OauthClient getOauthClient(String gcUrl) {
        if (gcUrl == null) {
            gcUrl = "https://grandcentral.cloudbees.com";
        }
        return new OauthClientImpl(this, gcUrl);
    }

    /**
     * Creates an user, including a partial user creation.
     *
     * @see <a href="https://sites.google.com/a/cloudbees
     *      .com/account-provisioning-api/home/user-api#TOC-Create-a-User">API spec</a>
     */
    public CBUser createUser(CBUser user) throws IOException {
        return jsonPOJORequest("v2/users", user, CBUser.class, "POST");
    }

    /**
     * Updates the user record.
     *
     * @param id   The ID of the user to update. Corresponds to {@link CBUser#id}.
     * @param user You should only set fields that you want to update, and leave everything else to null, to indicate
     *             those values should remain untouched.
     */
    public CBUser updateUser(String id, CBUser user) throws IOException {
        return jsonPOJORequest("v2/users/" + id, user, CBUser.class, "PATCH");
    }

    /**
     * Deletes an user.
     */
    public void deleteUser(String id) throws IOException {
        jsonPOJORequest("v2/users/" + id, null, null, "DELETE");
    }

    public CBUser addUserToAccount(CBAccount account, CBUser user) throws IOException {
        return jsonPOJORequest("v2/users/" + user.id + "/accounts/" + account.name + "/users", user, CBUser.class,
                "POST");
    }

    /**
     * The actual engine behind the REST API call.
     * <p/>
     * It sends a request in JSON and expects a JSON response back.
     * Note that for historical reasons, there's the other half of the API that uses query parameters + digital signing.
     *
     * @param apiTail The end point to hit. Appended to {@link #base}. Shouldn't start with '/'
     * @param request JSON-bound POJO object that represents the request payload, or null if none.
     * @param type    JSON-bound POJO class to unmarshal the response into.
     * @param method  HTTP method name like GET or POST.
     * @throws IOException If the communication fails.
     */
    protected <T> T jsonPOJORequest(String apiTail, Object request, Class<T> type, String method)
            throws IOException {
        String content = null;
        if (request != null) {
            content = MAPPER.writeValueAsString(request);
        }
        HttpReply resp = jsonRequest(apiTail, method, null, content);
        return resp.bind(type, this);
    }

    /**
     * Sends a request in JSON and expects a JSON response back.
     *
     * @param urlTail The end point to hit. Appended to {@link #base}. Shouldn't start with '/'
     * @param method  HTTP method name like GET or POST.
     * @param headers
     *@param jsonContent  The json request payload, or null if none.  @throws IOException If the communication fails.
     */
    public HttpReply jsonRequest(String urlTail, String method, Map<String, String> headers, String jsonContent)
            throws IOException {
        HttpMethodBase httpMethod;

        String urlString = absolutize(urlTail);

        trace("API call: " + urlString);
        if (method.equalsIgnoreCase("GET")) {
            httpMethod = new GetMethod(urlString);
        } else if ((method.equalsIgnoreCase("POST"))) {
            httpMethod = new PostMethod(urlString);
        } else if ((method.equalsIgnoreCase("PUT"))) {
            httpMethod = new PutMethod(urlString);
        } else if ((method.equalsIgnoreCase("DELETE"))) {
            httpMethod = new DeleteMethod(urlString);
        } else if ((method.equalsIgnoreCase("PATCH"))) {
            httpMethod = new PatchMethod(urlString);
        } else if ((method.equalsIgnoreCase("HEAD"))) {
            httpMethod = new HeadMethod(urlString);
        } else if ((method.equalsIgnoreCase("TRACE"))) {
            httpMethod = new TraceMethod(urlString);
        } else if ((method.equalsIgnoreCase("OPTIONS"))) {
            httpMethod = new OptionsMethod(urlString);
        } else
            throw new IOException("Method not supported: " + method);

        httpMethod.setRequestHeader("Accept", "application/json");
        if (jsonContent != null && httpMethod instanceof EntityEnclosingMethod) {
            StringRequestEntity requestEntity = new StringRequestEntity(jsonContent, "application/json", "UTF-8");
            ((EntityEnclosingMethod) httpMethod).setRequestEntity(requestEntity);
            trace("Payload: " + jsonContent);
        }

        return executeRequest(httpMethod, headers);
    }

    /**
     * Sends a request in JSON and expects a JSON response back.
     *
     * @param urlTail The end point to hit. Appended to {@link #base}. Shouldn't start with '/'
     * @param headers HTTP headers
     * @param params
     *      Form parameters
     */
    /*package*/ HttpReply formUrlEncoded(String urlTail, Map<String, String> headers,
            Map<String, List<String>> params) throws IOException {
        String urlString = absolutize(urlTail);
        trace("API call: " + urlString);
        PostMethod httpMethod = new PostMethod(urlString);

        httpMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

        for (Entry<String, List<String>> e : params.entrySet()) {
            for (String v : e.getValue()) {
                httpMethod.addParameter(e.getKey(), v);
            }
        }

        return executeRequest(httpMethod, headers);
    }

    /**
     * Computes the absolute URL to send the request to from the tail portion.
     */
    private String absolutize(String urlTail) throws IOException {
        URL url = new URL(base, urlTail);
        String urlString;
        try {
            urlString = url.toURI().toString();
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Invalid API URL:" + url.toString(), e);
        }
        return urlString;
    }

    /**
     * Executes an HTTP method.
     */
    private HttpReply executeRequest(HttpMethod httpMethod, Map<String, String> headers) throws IOException {
        BeesClientConfiguration conf = getBeesClientConfiguration();
        HttpClient httpClient = HttpClientHelper.createClient(conf);

        if (encodedAccountAuthorization != null)
            httpMethod.setRequestHeader("Authorization", encodedAccountAuthorization);

        if (headers != null) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                httpMethod.setRequestHeader(entry.getKey(), entry.getValue());
            }
        }

        int status = 500;
        String rsp = "Error";
        try {
            status = httpClient.executeMethod(httpMethod);
            rsp = IOUtils.toString(httpMethod.getResponseBodyAsStream());
        } catch (IOException e) {
            throw (IOException) new IOException("Failed to " + httpMethod.getName() + " : " + httpMethod.getURI()
                    + " : code=" + status + " response=" + e.getMessage()).initCause(e);
        } finally {
            httpMethod.releaseConnection();
        }

        trace(status + ": " + rsp);

        return new HttpReply(httpMethod, status, rsp);
    }

    public CBAccount getAccount(String name) throws IOException {
        return jsonPOJORequest("v2/accounts/" + name, null, CBAccount.class, "GET");
    }

    /**
     * Looks up the user by ID.
     */
    public CBUser getUser(String id) throws IOException {
        return jsonPOJORequest("v2/users/" + id, null, CBUser.class, "GET");
    }

    /**
     * Retrieves the information of the current user.
     *
     * The "current user" refers to the user who generated the token used to authenticate the method call.
     */
    public CBUser getSelfUser() throws IOException {
        return getUser("self");
    }

    /**
     * Looks up the user by the public key fingerprint
     *
     * @param sshPublicKeyFingerprint Fingerprint formatted as "12:34:56:..:aa:bb:cc" (case insensitive)
     */
    public CBUser getUserByFingerprint(String sshPublicKeyFingerprint) throws IOException {
        return jsonPOJORequest("v2/users/fingerprint/" + sshPublicKeyFingerprint, null, CBUser.class, "GET");
    }

    public SayHelloResponse sayHello(String message) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("message", message);
        String url = getRequestURL("say.hello", params);
        String response = executeRequest(url);
        return (SayHelloResponse) readResponse(response);
    }

    public ApplicationGetSourceUrlResponse applicationGetSourceUrl(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.getSourceUrl", params);
        String response = executeRequest(url);
        return (ApplicationGetSourceUrlResponse) readResponse(response);
    }

    public ApplicationDeleteResponse applicationDelete(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.delete", params);
        String response = executeRequest(url);
        return (ApplicationDeleteResponse) readResponse(response);
    }

    public ApplicationRestartResponse applicationRestart(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.restart", params);
        String response = executeRequest(url);
        return (ApplicationRestartResponse) readResponse(response);
    }

    public ApplicationStatusResponse applicationStart(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.start", params);
        String response = executeRequest(url);
        return (ApplicationStatusResponse) readResponse(response);
    }

    public ApplicationStatusResponse applicationStop(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.stop", params);
        String response = executeRequest(url);
        return (ApplicationStatusResponse) readResponse(response);
    }

    public ApplicationStatusResponse applicationHibernate(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.hibernate", params);
        String response = executeRequest(url);
        ApplicationStatusResponse apiResponse = (ApplicationStatusResponse) readResponse(response);
        return apiResponse;
    }

    /**
     * Returns all the applications in all the account sthat you belong to.
     * <p/>
     * Short-hand for {@code applicationList(null)}.
     */
    public ApplicationListResponse applicationList() throws Exception {
        return applicationList(null);
    }

    /**
     * Returns all the applications in the specified account.
     *
     * @param account if null, returns all the applications from all the accounts that you belong to.
     * @since 1.1.3
     */
    public ApplicationListResponse applicationList(String account) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        if (account != null) {
            params.put("account", account);
        }
        String url = getRequestURL("application.list", params);
        String response = executeRequest(url);
        return (ApplicationListResponse) readResponse(response);
    }

    public ApplicationInfo applicationInfo(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.info", params);
        String response = executeRequest(url);
        ApplicationInfoResponse apiResponse = (ApplicationInfoResponse) readResponse(response);
        return apiResponse.getApplicationInfo();
    }

    public ApplicationSetMetaResponse applicationSetMeta(String appId, Map<String, String> metaAttrs)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.putAll(metaAttrs);
        params.put("app_id", appId);
        String url = getRequestURL("application.setMeta", params);
        String response = executeRequest(url);
        return (ApplicationSetMetaResponse) readResponse(response);
    }

    /**
     * @deprecated
     */
    public ApplicationJarHashesResponse applicationJarCrcs(String appId, Map<String, String> hashes)
            throws Exception {
        return applicationJarHashes(appId, hashes);
    }

    public ApplicationJarHashesResponse applicationJarHashes(String appId, Map<String, String> hashes)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        params.put("hashes", createParameter(hashes));

        String url = getApiUrl("application.jarHashes").toString();
        params.put("action", "application.jarHashes");
        // use the upload method (POST) to handle the potentially large "hashes" parameter payload
        String response = executeUpload(url, params, new HashMap<String, File>(), null);
        return (ApplicationJarHashesResponse) readResponse(response);
    }

    /**
     * @deprecated use {@link #applicationDeployEar(String, String, String, java.io.File, java.io.File, UploadProgress)}
     */
    @Deprecated
    public ApplicationDeployArchiveResponse applicationDeployEar(String appId, String environment,
            String description, String earFile, String srcFile, UploadProgress progress) throws Exception {
        return applicationDeployEar(appId, environment, description, asFile(earFile), asFile(srcFile), progress);
    }

    /**
     * @since 1.1.4
     */
    public ApplicationDeployArchiveResponse applicationDeployEar(String appId, String environment,
            String description, File earFile, File srcFile, UploadProgress progress) throws Exception {
        String archiveType = "ear";
        return applicationDeployArchive(appId, environment, description, earFile, srcFile, archiveType, false,
                progress);
    }

    /**
     * @deprecated use {@link #applicationDeployWar(String, String, String, File, File, UploadProgress)}
     */
    @Deprecated
    public ApplicationDeployArchiveResponse applicationDeployWar(String appId, String environment,
            String description, String warFile, String srcFile, UploadProgress progress) throws Exception {
        return applicationDeployWar(appId, environment, description, asFile(warFile), asFile(srcFile), progress);
    }

    /**
     * @since 1.1.4
     */
    public ApplicationDeployArchiveResponse applicationDeployWar(String appId, String environment,
            String description, File warFile, File srcFile, UploadProgress progress) throws Exception {
        return applicationDeployWar(appId, environment, description, warFile, srcFile, true, progress);
    }

    /**
     * @deprecated use {@link #applicationDeployWar(String, String, String, File, File, boolean, UploadProgress)}
     */
    @Deprecated
    public ApplicationDeployArchiveResponse applicationDeployWar(String appId, String environment,
            String description, String warFile, String srcFile, boolean deltaDeploy, UploadProgress progress)
            throws Exception {
        return applicationDeployWar(appId, environment, description, asFile(warFile), asFile(srcFile), deltaDeploy,
                progress);
    }

    /**
     * @since 1.1.4
     */
    public ApplicationDeployArchiveResponse applicationDeployWar(String appId, String environment,
            String description, File warFile, File srcFile, boolean deltaDeploy, UploadProgress progress)
            throws Exception {
        String archiveType = "war";
        return applicationDeployArchive(appId, environment, description, warFile, srcFile, archiveType, deltaDeploy,
                progress);
    }

    /**
     * @deprecated use {@link #applicationDeployArchive(String, String, String, File, File, String, UploadProgress)}
     */
    @Deprecated
    public ApplicationDeployArchiveResponse applicationDeployArchive(String appId, String environment,
            String description, String earFile, String srcFile, String archiveType, UploadProgress progress)
            throws Exception {
        return applicationDeployArchive(appId, environment, description, asFile(earFile), asFile(srcFile),
                archiveType, progress);
    }

    /**
     * @since 1.1.4
     */
    public ApplicationDeployArchiveResponse applicationDeployArchive(String appId, String environment,
            String description, File earFile, File srcFile, String archiveType, UploadProgress progress)
            throws Exception {
        return applicationDeployArchive(appId, environment, description, earFile, srcFile, archiveType, false,
                progress);
    }

    /**
     * @deprecated use {@link #applicationDeployArchive(String, String, String, File, File, String, boolean,
     *             UploadProgress)}
     */
    @Deprecated
    public ApplicationDeployArchiveResponse applicationDeployArchive(String appId, String environment,
            String description, String earFile, String srcFile, String archiveType, boolean deltaDeploy,
            UploadProgress progress) throws Exception {
        return applicationDeployArchive(appId, environment, description, asFile(earFile), asFile(srcFile),
                archiveType, deltaDeploy, progress);
    }

    /**
     * @since 1.1.4
     */
    public ApplicationDeployArchiveResponse applicationDeployArchive(String appId, String environment,
            String description, File earFile, File srcFile, String archiveType, boolean deltaDeploy,
            UploadProgress progress) throws Exception {
        return applicationDeployArchive(appId, environment, description, earFile, srcFile, archiveType, deltaDeploy,
                null, progress);
    }

    /**
     * @deprecated use {@link #applicationDeployArchive(String, String, String, File, File, String, boolean, Map,
     *             UploadProgress)}
     */
    @Deprecated
    public ApplicationDeployArchiveResponse applicationDeployArchive(String appId, String environment,
            String description, String earFile, String srcFile, String archiveType, boolean deltaDeploy,
            Map<String, String> parameters, UploadProgress progress) throws Exception {
        return applicationDeployArchive(appId, environment, description, asFile(earFile), asFile(srcFile),
                archiveType, deltaDeploy, parameters, progress);
    }

    /**
     * @since 1.1.4
     */
    public ApplicationDeployArchiveResponse applicationDeployArchive(String appId, String environment,
            String description, File earFile, File srcFile, String archiveType, boolean deltaDeploy,
            Map<String, String> parameters, UploadProgress progress) throws Exception {
        return applicationDeployArchive(new ApplicationDeployArgs.Builder(appId).environment(environment)
                .description(description).deployPackage(earFile, archiveType).srcFile(srcFile)
                .incrementalDeployment(deltaDeploy).withParams(parameters).withProgressFeedback(progress).build());
    }

    public ApplicationDeployArchiveResponse applicationDeployArchive(ApplicationDeployArgs args) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        Map<String, File> fileParams = new HashMap<String, File>();
        params.put("app_id", args.appId);

        File archiveFile = args.archiveFile;

        // Currently do not support ear file for delta upload
        boolean deployDelta = false;
        boolean deployJarDelta = false;
        // Create delta deploy File
        if (args.deltaDeploy && !args.archiveType.equals("ear")) {
            trace("Get existing checksums");
            ApplicationCheckSumsResponse applicationCheckSumsResponse = applicationCheckSums(args.appId, false);
            if (logger.isLoggable(Level.FINER)) {
                for (Map.Entry<String, Long> entry : applicationCheckSumsResponse.getCheckSums().entrySet()) {
                    logger.finer("Entry: " + entry.getKey() + " CRC: " + entry.getValue());
                }
            }
            if (applicationCheckSumsResponse.getCheckSums().size() == 0) {
                trace("No existing checksums, upload full archive");
            } else {
                trace("Creating Delta archive for: " + archiveFile);
                archiveFile = ArchiveUtils.createDeltaWarFile(applicationCheckSumsResponse.getCheckSums(),
                        archiveFile, archiveFile.getParent());
                deployDelta = true;
                if (applicationCheckSumsResponse.getSnapshotID() != null)
                    params.put("delta_snapshot_id", applicationCheckSumsResponse.getSnapshotID());
            }
        }

        if (args.deltaDeploy && !args.archiveType.equals("ear")) {
            trace("Get existing jar hashes");
            ApplicationJarHashesResponse applicationJarHashesResponse = applicationJarHashes(args.appId,
                    JarUtils.getJarHashes(archiveFile));
            if (applicationJarHashesResponse.getJarHash().size() == 0) {
                trace("No existing jars");
            } else {
                trace("Creating Delta2 archive for: " + archiveFile);
                File archiveFile2 = JarUtils.createDeltaWarFile(applicationJarHashesResponse.getJarHash(),
                        archiveFile, archiveFile.getParent());
                // Delete the old delta archive
                if (deployDelta) {
                    archiveFile.delete();
                }
                archiveFile = archiveFile2;
                deployJarDelta = true;
            }
        }

        if (deployDelta || deployJarDelta) {
            trace("Uploading delta archive: " + archiveFile);
        }

        File archiveFileSrc = args.srcFile;
        long uploadSize = archiveFile.length();
        if (archiveFileSrc != null) {
            uploadSize += archiveFileSrc.length();
        }

        fileParams.put("archive", archiveFile);
        params.put("archive_type", args.archiveType);

        params.put("create", Boolean.valueOf(args.create).toString());

        if (args.environment != null) {
            params.put("environment", args.environment);
        }

        if (args.description != null) {
            params.put("description", args.description);
        }

        if (archiveFileSrc != null) {
            fileParams.put("src", archiveFileSrc);
        }

        params.put("parameters", createParameter(args.parameters));
        params.put("variables", createParameter(args.variables));

        // extend the deploy invocation timeout to 4 hours
        long expireTime = System.currentTimeMillis() + 4 * 60 * 60 * 1000;
        params.put("expires", Long.toString(expireTime / 1000));

        String url = getApiUrl("application.deployArchive").toString();
        params.put("action", "application.deployArchive");
        String response = executeUpload(url, params, fileParams, args.progress);
        try {
            return (ApplicationDeployArchiveResponse) readResponse(response);
        } finally {
            // Delete the delta archive file
            if (deployDelta || deployJarDelta) {
                archiveFile.delete();
            }
        }
    }

    public ApplicationCheckSumsResponse applicationCheckSums(String appId) throws Exception {
        return applicationCheckSums(appId, true);
    }

    public ApplicationCheckSumsResponse applicationCheckSums(String appId, boolean traceResponse) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.checkSums", params);
        String response = executeRequest(url);
        return (ApplicationCheckSumsResponse) readResponse(response);
    }

    public ApplicationScaleResponse applicationScale(String appId, int unit) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("unit", "" + unit);
        params.put("app_id", appId);
        String url = getRequestURL("application.scale", params);
        String response = executeRequest(url);
        return (ApplicationScaleResponse) readResponse(response);
    }

    public DatabaseCreateResponse databaseCreate(String domain, String dbId, String username, String password)
            throws Exception {
        return databaseCreate(domain, dbId, username, password, null);
    }

    public DatabaseCreateResponse databaseCreate(String domain, String dbId, String username, String password,
            String plan) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        if (username != null)
            params.put("database_username", username);
        if (password != null)
            params.put("database_password", password);
        params.put("domain", domain);
        if (plan != null)
            params.put("database_plan", plan);
        String url = getRequestURL("database.create", params);
        String response = executeRequest(url);
        return (DatabaseCreateResponse) readResponse(response);
    }

    public DatabaseDeleteResponse databaseDelete(String dbId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        String url = getRequestURL("database.delete", params);
        String response = executeRequest(url);
        return (DatabaseDeleteResponse) readResponse(response);
    }

    public DatabaseInfo databaseInfo(String dbId, boolean fetchPassword) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        params.put("fetch_password", ((Boolean) fetchPassword).toString());
        String url = getRequestURL("database.info", params);
        String response = executeRequest(url);
        DatabaseInfoResponse apiResponse = (DatabaseInfoResponse) readResponse(response);
        return apiResponse.getDatabaseInfo();
    }

    /**
     * Returns all the databases in all the account sthat you belong to.
     * <p/>
     * Short-hand for {@code databaseList(null)}.
     */
    public DatabaseListResponse databaseList() throws Exception {
        return databaseList(null);
    }

    /**
     * Returns all the databases in the specified account.
     *
     * @param account if null, returns all the databases from all the accounts that you belong to.
     * @since 1.1.3
     */
    public DatabaseListResponse databaseList(String account) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        if (account != null) {
            params.put("account", account);
        }
        String url = getRequestURL("database.list", params);
        String response = executeRequest(url);
        return (DatabaseListResponse) readResponse(response);
    }

    /**
     * @deprecated use databaseUpdate
     * @param dbId    The database ID
     * @param password  The database password
     * @return   DatabaseSetPasswordResponse
     * @throws Exception
     */
    public DatabaseSetPasswordResponse databaseSetPassword(String dbId, String password) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        params.put("database_password", password);
        String url = getRequestURL("database.setPassword", params);
        String response = executeRequest(url);
        return (DatabaseSetPasswordResponse) readResponse(response);
    }

    public DatabaseInfo databaseUpdate(String dbID, Map<String, String> parameters) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbID);
        params.put("parameters", createParameter(parameters));
        String url = getRequestURL("database.update", params);
        String response = executeRequest(url);
        return (DatabaseInfo) readResponse(response);
    }

    public DatabaseSnapshotListResponse databaseSnapshotList(String dbId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        String url = getRequestURL("database.snapshot.list", params);
        String response = executeRequest(url);
        return (DatabaseSnapshotListResponse) readResponse(response);
    }

    public DatabaseSnapshotDeleteResponse databaseSnapshotDelete(String dbId, String snapshotId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        params.put("snapshot_id", snapshotId);
        String url = getRequestURL("database.snapshot.delete", params);
        String response = executeRequest(url);
        return (DatabaseSnapshotDeleteResponse) readResponse(response);
    }

    public DatabaseSnapshotDeployResponse databaseSnapshotDeploy(String dbId, String snapshotId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        params.put("snapshot_id", snapshotId);
        String url = getRequestURL("database.snapshot.deploy", params);
        String response = executeRequest(url);
        return (DatabaseSnapshotDeployResponse) readResponse(response);
    }

    public DatabaseSnapshotInfo databaseSnapshotCreate(String dbId, String snapshotTitle) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        if (snapshotTitle != null) {
            params.put("snapshot_title", snapshotTitle);
        }
        String url = getRequestURL("database.snapshot.create", params);
        String response = executeRequest(url);
        return (DatabaseSnapshotInfo) readResponse(response);
    }

    public DatabaseBackupListResponse databaseBackupList(String dbId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        String url = getRequestURL("database.backup.list", params);
        String response = executeRequest(url);
        return (DatabaseBackupListResponse) readResponse(response);
    }

    public DatabaseBackupResponse databaseBackupCreate(String dbId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        String url = getRequestURL("database.backup.create", params);
        String response = executeRequest(url);
        return (DatabaseBackupResponse) readResponse(response);
    }

    public DatabaseBackupResponse databaseBackupRestore(String dbId, String backupId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        params.put("backup_id", backupId);
        String url = getRequestURL("database.backup.restore", params);
        String response = executeRequest(url);
        return (DatabaseBackupResponse) readResponse(response);
    }

    public DatabaseBackupResponse databaseBackupDelete(String dbId, String backupId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("database_id", dbId);
        params.put("backup_id", backupId);
        String url = getRequestURL("database.backup.delete", params);
        String response = executeRequest(url);
        return (DatabaseBackupResponse) readResponse(response);
    }

    public DatabaseClusterListResponse databaseClusterList(String account) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        String url = getRequestURL("database.cluster.list", params);
        String response = executeRequest(url);
        return (DatabaseClusterListResponse) readResponse(response);
    }

    public AccountKeysResponse accountKeys(String domain, String user, String password) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("user", user);
        params.put("password", password);
        if (domain != null) {
            params.put("domain", domain);
        }
        String url = getRequestURL("account.keys", params);
        String response = executeRequest(url);
        return (AccountKeysResponse) readResponse(response);
    }

    public AccountListResponse accountList() throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        String url = getRequestURL("account.list", params);
        String response = executeRequest(url);
        return (AccountListResponse) readResponse(response);
    }

    public ApplicationConfiguration getApplicationConfiguration(String warFilePath, String account,
            String[] environments) throws Exception {
        ApplicationConfiguration appConfig;
        File deployFile = asFile(warFilePath);
        if (deployFile.exists()) {
            appConfig = getAppConfig(deployFile, environments, new String[] { "deploy" });
        } else {
            throw new IllegalArgumentException("File not found: " + warFilePath);
        }

        String appid = appConfig.getApplicationId();
        if (appid == null || appid.equals("")) {
            throw new IllegalArgumentException("No application id specified");
        }

        String[] appIdParts = appid.split("/");
        if (appIdParts.length < 2) {
            if (account != null && !account.equals("")) {
                appConfig.setApplicationId(account + "/" + appid);
            } else {
                throw new IllegalArgumentException("Application account not specified");
            }
        }
        return appConfig;
    }

    public ConfigurationParametersUpdateResponse configurationParametersUpdate(String resourceId, String configType,
            File resourceFile) throws Exception {
        assertNotNull(resourceId, "resourceId");
        assertNotNull(configType, "configType");
        assertNotNull(resourceFile, "resourceFile");

        Map<String, String> params = new HashMap<String, String>();
        Map<String, File> fileParams = new HashMap<String, File>();

        params.put("resource_id", resourceId);
        params.put("config_type", configType);
        fileParams.put("resources", resourceFile);

        String url = getApiUrl("configuration.parameters.update").toString();
        params.put("action", "configuration.parameters.update");
        // use the upload method (POST) to handle the potentially large resource list
        String response = executeUpload(url, params, fileParams, null);
        return (ConfigurationParametersUpdateResponse) readResponse(response);
    }

    /**
     * Updates the configuration of the given resource from the configuration object model.
     */
    public ConfigurationParametersUpdateResponse configurationParametersUpdate(String resourceId, String configType,
            ConfigParameters model) throws Exception {

        File xmlFile = File.createTempFile("conf", "xml");
        FileWriter fos = null;
        try {
            fos = new FileWriter(xmlFile);
            fos.write(model.toXML());
            fos.close();
            return configurationParametersUpdate(resourceId, configType, xmlFile);
        } finally {
            IOUtils.closeQuietly(fos);
            xmlFile.delete();
        }
    }

    public ConfigurationParametersDeleteResponse configurationParametersDelete(String resourceId, String configType)
            throws Exception {
        assertNotNull(resourceId, "resourceId");
        assertNotNull(configType, "configType");

        Map<String, String> params = new HashMap<String, String>();
        params.put("resource_id", resourceId);
        params.put("config_type", configType);
        String url = getRequestURL("configuration.parameters.delete", params);
        String response = executeRequest(url);
        return (ConfigurationParametersDeleteResponse) readResponse(response);
    }

    public ConfigurationParametersResponse configurationParameters(String resourceId, String configType)
            throws Exception {
        assertNotNull(resourceId, "resourceId");
        assertNotNull(configType, "configType");

        Map<String, String> params = new HashMap<String, String>();
        params.put("resource_id", resourceId);
        params.put("config_type", configType);
        String url = getRequestURL("configuration.parameters", params);
        String response = executeRequest(url);
        return (ConfigurationParametersResponse) readResponse(response);
    }

    /**
     * Perform {@link #configurationParameters(String, String)} and obtains the result as a data-bound object.
     */
    public ConfigParameters configurationParametersAsObject(String resourceId, String configType) throws Exception {
        return ConfigParameters.parse(configurationParameters(resourceId, configType).getConfiguration());
    }

    public ServiceListResponse serviceList(String account) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        String url = getRequestURL("service.list", params);
        String response = executeRequest(url);
        return (ServiceListResponse) readResponse(response);
    }

    public ServiceSubscriptionInfo serviceSubscribe(String service, String plan, String account,
            Map<String, String> settings) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        params.put("service", service);
        if (plan != null) {
            params.put("plan", plan);
        }
        params.put("settings", createParameter(settings));
        String url = getRequestURL("service.subscribe", params);
        String response = executeRequest(url);
        return ((ServiceSubscriptionResponse) readResponse(response)).getSubscription();
    }

    public ServiceSubscriptionDeleteResponse serviceUnSubscribe(String service, String subscriptionId)
            throws Exception {
        return serviceUnSubscribe(service, subscriptionId, null);
    }

    public ServiceSubscriptionDeleteResponse serviceUnSubscribe(String service, String subscriptionId,
            Map<String, String> settings) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("subscription_id", subscriptionId);
        params.put("service", service);
        params.put("settings", createParameter(settings));
        String url = getRequestURL("service.unsubscribe", params);
        String response = executeRequest(url);
        return (ServiceSubscriptionDeleteResponse) readResponse(response);
    }

    public ServiceSubscriptionInfo serviceSubscriptionUpdate(String service, String plan, String subscriptionId,
            Map<String, String> settings) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("subscription_id", subscriptionId);
        params.put("service", service);
        if (plan != null) {
            params.put("plan", plan);
        }
        params.put("settings", createParameter(settings));
        String url = getRequestURL("service.subscription.update", params);
        String response = executeRequest(url);
        return ((ServiceSubscriptionResponse) readResponse(response)).getSubscription();
    }

    public ServiceSubscriptionInfo serviceSubscriptionInfo(String service, String subscriptionId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("subscription_id", subscriptionId);
        params.put("service", service);
        String url = getRequestURL("service.subscription.info", params);
        String response = executeRequest(url);
        return ((ServiceSubscriptionResponse) readResponse(response)).getSubscription();
    }

    public ServiceSubscriptionListResponse serviceSubscriptionList(String account) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        String url = getRequestURL("service.subscription.list", params);
        String response = executeRequest(url);
        return (ServiceSubscriptionListResponse) readResponse(response);
    }

    public ServiceResourceInfo serviceResourceInfo(String service, String resourceId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("service", service);
        params.put("resource_id", resourceId);
        String url = getRequestURL("service.resource.info", params);
        String response = executeRequest(url);
        return ((ServiceResourceResponse) readResponse(response)).getResource();
    }

    public ServiceResourceListResponse serviceResourceList(String service, String account, String resourceType)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        params.put("service", service);
        if (resourceType != null) {
            params.put("resource_type", resourceType);
        }
        String url = getRequestURL("service.resource.list", params);
        String response = executeRequest(url);
        return (ServiceResourceListResponse) readResponse(response);
    }

    public ServiceResourceBindingListResponse resourceBindingList(String service, String resourceId)
            throws Exception {
        return resourceBindingList(service, resourceId, false);
    }

    public ServiceResourceBindingListResponse resourceBindingList(String service, String resourceId,
            boolean bidirectional) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("resource_id", resourceId);
        params.put("service", service);
        params.put("bidirectional", Boolean.toString(bidirectional));
        String url = getRequestURL("resource.binding.list", params);
        String response = executeRequest(url);
        ServiceResourceBindingListResponse apiResponse = (ServiceResourceBindingListResponse) readResponse(
                response);
        return apiResponse;
    }

    public AccountRegionListResponse accountRegionList(String account, String service) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        if (service != null)
            params.put("service", service);
        String url = getRequestURL("account.region.list", params);
        String response = executeRequest(url);
        return (AccountRegionListResponse) readResponse(response);
    }

    public ApplicationInstanceListResponse applicationInstanceList(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.instance.list", params);
        String response = executeRequest(url);
        ApplicationInstanceListResponse apiResponse = (ApplicationInstanceListResponse) readResponse(response);
        return apiResponse;
    }

    public ApplicationInstanceStatusResponse applicationInstanceReplace(String instanceId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("instance_id", instanceId);
        String url = getRequestURL("application.instance.replace", params);
        String response = executeRequest(url);
        ApplicationInstanceStatusResponse apiResponse = (ApplicationInstanceStatusResponse) readResponse(response);
        return apiResponse;
    }

    public ApplicationInstanceStatusResponse applicationInstanceRestart(String instanceId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("instance_id", instanceId);
        String url = getRequestURL("application.instance.restart", params);
        String response = executeRequest(url);
        ApplicationInstanceStatusResponse apiResponse = (ApplicationInstanceStatusResponse) readResponse(response);
        return apiResponse;
    }

    public ApplicationInstanceStatusResponse applicationInstanceDelete(String instanceId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("instance_id", instanceId);
        String url = getRequestURL("application.instance.delete", params);
        String response = executeRequest(url);
        ApplicationInstanceStatusResponse apiResponse = (ApplicationInstanceStatusResponse) readResponse(response);
        return apiResponse;
    }

    public com.cloudbees.api.ApplicationInstanceInfo applicationInstanceTagsUpdate(String instanceId,
            Map<String, String> tags, boolean replace) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("instance_id", instanceId);
        params.put("reset", Boolean.toString(replace));
        params.put("parameters", createParameter(tags));

        String url = getApiUrl("application.instance.update").toString();
        params.put("action", "application.instance.update");
        // use the upload method (POST) to handle the potentially large tags payload
        String response = executeUpload(url, params, new HashMap<String, File>(), null);
        com.cloudbees.api.ApplicationInstanceInfo apiResponse = (com.cloudbees.api.ApplicationInstanceInfo) readResponse(
                response);
        return apiResponse;
    }

    public com.cloudbees.api.ApplicationInstanceInfo applicationInstanceInfo(String instanceId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("instance_id", instanceId);

        String url = getRequestURL("application.instance.info", params);
        String response = executeRequest(url);
        com.cloudbees.api.ApplicationInstanceInfo apiResponse = (com.cloudbees.api.ApplicationInstanceInfo) readResponse(
                response);
        return apiResponse;
    }

    public ApplicationInstanceInvokeResponse applicationInstanceInvoke(String instanceId, String invoke,
            Map<String, String> parameters) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("instance_id", instanceId);
        params.put("invoke", invoke);
        params.put("parameters", createParameter(parameters));

        String url = getRequestURL("application.instance.invoke", params);
        String response = executeRequest(url);
        ApplicationInstanceInvokeResponse apiResponse = (ApplicationInstanceInvokeResponse) readResponse(response);
        return apiResponse;
    }

    public void applicationInstanceTailLog(String instanceId, String logName, OutputStream out) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("instance_id", instanceId);
        params.put("log_name", logName);
        String url = getRequestURL("tail", params, false);
        trace("API call: " + url);
        InputStream input = executeCometRequest(url);

        byte[] bytes = new byte[1024];
        int numRead = input.read(bytes);
        while (numRead != -1) {
            out.write(bytes, 0, numRead);
            numRead = input.read(bytes);
        }
    }

    public ApplicationSnapshotListResponse applicationSnapshotList(String appId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        String url = getRequestURL("application.snapshot.list", params);
        String response = executeRequest(url);
        return (ApplicationSnapshotListResponse) readResponse(response);
    }

    public ApplicationSnapshotStatusResponse applicationSnapshotDelete(String snapshotId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("snapshot_id", snapshotId);
        String url = getRequestURL("application.snapshot.delete", params);
        String response = executeRequest(url);
        ApplicationSnapshotStatusResponse apiResponse = (ApplicationSnapshotStatusResponse) readResponse(response);
        return apiResponse;
    }

    public com.cloudbees.api.ApplicationSnapshotInfo applicationSnapshotInfo(String snapshotId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("snapshot_id", snapshotId);
        String url = getRequestURL("application.snapshot.info", params);
        String response = executeRequest(url);
        com.cloudbees.api.ApplicationSnapshotInfo apiResponse = (com.cloudbees.api.ApplicationSnapshotInfo) readResponse(
                response);
        return apiResponse;
    }

    public ApplicationSnapshotStatusResponse applicationSnapshotUpdate(String snapshotId,
            Map<String, String> parameters) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("snapshot_id", snapshotId);
        params.put("parameters", createParameter(parameters));
        String url = getRequestURL("application.snapshot.update", params);
        String response = executeRequest(url);
        ApplicationSnapshotStatusResponse apiResponse = (ApplicationSnapshotStatusResponse) readResponse(response);
        return apiResponse;
    }

    public ApplicationStatusResponse applicationProxyUpdate(String appId, Map<String, String> parameters)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        params.put("parameters", createParameter(parameters));
        String url = getRequestURL("application.proxy.update", params);
        String response = executeRequest(url);
        ApplicationStatusResponse apiResponse = (ApplicationStatusResponse) readResponse(response);
        return apiResponse;
    }

    public ApplicationConfigUpdateResponse applicationConfigUpdate(String appId, Map<String, String> parameters)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        params.put("parameters", createParameter(parameters));
        String url = getRequestURL("application.config.update", params);
        String response = executeRequest(url);
        ApplicationConfigUpdateResponse apiResponse = (ApplicationConfigUpdateResponse) readResponse(response);
        return apiResponse;
    }

    public ServiceResourceBindResponse resourceBind(String fromService, String fromResourceId, String toService,
            String toResourceId, String alias, Map<String, String> settings) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("to_service", toService);
        params.put("to_resource_id", toResourceId);
        params.put("from_service", fromService);
        params.put("from_resource_id", fromResourceId);
        params.put("alias", alias);
        params.put("settings", createParameter(settings));
        String url = getRequestURL("service.resource.bind", params);
        String response = executeRequest(url);
        ServiceResourceBindResponse apiResponse = (ServiceResourceBindResponse) readResponse(response);
        return apiResponse;
    }

    public ServiceResourceUnBindResponse resourceUnBind(String service, String resourceId, String alias)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("alias", alias);
        params.put("service", service);
        params.put("resource_id", resourceId);
        String url = getRequestURL("service.resource.unbind", params);
        String response = executeRequest(url);
        ServiceResourceUnBindResponse apiResponse = (ServiceResourceUnBindResponse) readResponse(response);
        return apiResponse;
    }

    public ServiceResourceInfo serviceResourceCreate(String service, String account, String resourceType,
            String resourceName, Map<String, String> settings) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        params.put("service", service);
        if (resourceType != null)
            params.put("resource_type", resourceType);
        params.put("resource_name", resourceName);
        params.put("settings", createParameter(settings));

        ServiceResourceResponse apiResponse = (ServiceResourceResponse) apiCall("service.resource.create", settings,
                params);
        return apiResponse.getResource();
    }

    public ServiceResourceInfo serviceResourceUpdate(String service, String resourceId,
            Map<String, String> settings) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("service", service);
        params.put("resource_id", resourceId);
        params.put("settings", createParameter(settings));

        ServiceResourceResponse apiResponse = (ServiceResourceResponse) apiCall("service.resource.update", settings,
                params);
        return apiResponse.getResource();
    }

    public ServiceResourceDeleteResponse serviceResourceDelete(String service, String resourceId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("service", service);
        params.put("resource_id", resourceId);
        String url = getRequestURL("service.resource.delete", params);
        String response = executeRequest(url);
        ServiceResourceDeleteResponse apiResponse = (ServiceResourceDeleteResponse) readResponse(response);
        return apiResponse;
    }

    public ApplicationCreateResponse applicationCreate(String appId, Map<String, String> parameters)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        params.put("parameters", createParameter(parameters));
        String url = getRequestURL("application.create", params);
        String response = executeRequest(url);
        ApplicationCreateResponse apiResponse = (ApplicationCreateResponse) readResponse(response);
        return apiResponse;
    }

    public ServerPoolInfo serverPoolCreate(String account, String poolName, Map<String, String> parameters)
            throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        params.put("pool_name", poolName);
        params.put("parameters", createParameter(parameters));
        String url = getRequestURL("server.pool.create", params);
        String response = executeRequest(url);
        ServerPoolInfo apiResponse = (ServerPoolInfo) readResponse(response);
        return apiResponse;
    }

    public ServerPoolInfo serverPoolInfo(String poolId) throws Exception {
        return serverPoolInfo(poolId, false);
    }

    public ServerPoolInfo serverPoolInfo(String poolId, boolean withApplications) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("pool_id", poolId);
        if (withApplications)
            params.put("with_applications", "true");
        String url = getRequestURL("server.pool.info", params);
        String response = executeRequest(url);
        ServerPoolInfo apiResponse = (ServerPoolInfo) readResponse(response);
        return apiResponse;
    }

    public ServerPoolListResponse serverPoolList(String account) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("account", account);
        String url = getRequestURL("server.pool.list", params);
        String response = executeRequest(url);
        ServerPoolListResponse apiResponse = (ServerPoolListResponse) readResponse(response);
        return apiResponse;
    }

    public ServerPoolDeleteResponse serverPoolDelete(String poolId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("pool_id", poolId);
        String url = getRequestURL("server.pool.delete", params);
        String response = executeRequest(url);
        ServerPoolDeleteResponse apiResponse = (ServerPoolDeleteResponse) readResponse(response);
        return apiResponse;
    }

    public ServerInfo serverCreate(String poolId, Map<String, String> parameters) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("pool_id", poolId);
        params.put("parameters", createParameter(parameters));
        String url = getRequestURL("server.create", params);
        String response = executeRequest(url);
        ServerInfo apiResponse = (ServerInfo) readResponse(response);
        return apiResponse;
    }

    public ServerRestoreResponse serverRestore(String serverId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("server_id", serverId);
        String url = getRequestURL("server.restore", params);
        String response = executeRequest(url);
        ServerRestoreResponse apiResponse = (ServerRestoreResponse) readResponse(response);
        return apiResponse;
    }

    public ServerInfo serverInfo(String serverId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("server_id", serverId);
        String url = getRequestURL("server.info", params);
        String response = executeRequest(url);
        ServerInfo apiResponse = (ServerInfo) readResponse(response);
        return apiResponse;
    }

    public ServerStatusResponse serverStop(String serverId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("server_id", serverId);
        String url = getRequestURL("server.stop", params);
        String response = executeRequest(url);
        ServerStatusResponse apiResponse = (ServerStatusResponse) readResponse(response);
        return apiResponse;
    }

    public ServerStatusResponse serverDelete(String serverId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("server_id", serverId);
        String url = getRequestURL("server.delete", params);
        String response = executeRequest(url);
        ServerStatusResponse apiResponse = (ServerStatusResponse) readResponse(response);
        return apiResponse;
    }

    public ServerStatusResponse serverDeactivate(String serverId) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("server_id", serverId);
        String url = getRequestURL("server.deactivate", params);
        String response = executeRequest(url);
        ServerStatusResponse apiResponse = (ServerStatusResponse) readResponse(response);
        return apiResponse;
    }

    public ServiceSubscriptionInvokeInfo serviceSubscriptionInvoke(String service, String subscriptionId,
            String invoke, Map<String, String> parameters) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("service", service);
        params.put("subscription_id", subscriptionId);
        params.put("invoke", invoke);
        params.put("parameters", createParameter(parameters));

        ServiceSubscriptionInvokeResponse apiResponse = (ServiceSubscriptionInvokeResponse) apiCall(
                "service.subscription.invoke", parameters, params);
        return apiResponse.getInvokeInfo();
    }

    public ServiceResourceInvokeInfo serviceResourceInvoke(String service, String resourceId, String invoke,
            Map<String, String> parameters) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("service", service);
        params.put("resource_id", resourceId);
        params.put("invoke", invoke);
        params.put("parameters", createParameter(parameters));

        ServiceResourceInvokeResponse apiResponse = (ServiceResourceInvokeResponse) apiCall(
                "service.resource.invoke", parameters, params);
        return apiResponse.getInvokeInfo();
    }

    public ApplicationCreateResponse applicationCreate(String appId, Map<String, String> parameters,
            Map<String, String> appParameters, Map<String, String> appVariables) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        params.put("parameters", createParameter(parameters));
        params.put("app_parameters", createParameter(appParameters));
        params.put("app_variables", createParameter(appVariables));
        String url = getRequestURL("application.create", params);
        String response = executeRequest(url);
        ApplicationCreateResponse apiResponse = (ApplicationCreateResponse) readResponse(response);
        return apiResponse;
    }

    public List<ResourceSettings> applicationResources(String appId, String resourceType, String configType,
            String environment) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        if (environment != null)
            params.put("environment", environment);
        if (resourceType != null)
            params.put("resource_type", resourceType);
        if (configType != null)
            params.put("config_type", configType);
        String url = getRequestURL("application.resources", params);
        String response = executeRequest(url);
        List<ResourceSettings> apiResponse = (List<ResourceSettings>) readResponse(response);
        return apiResponse;
    }

    @Override
    public String executeRequest(String url) throws Exception {
        trace("API call: " + url);
        String response = super.executeRequest(url);
        traceResponse(response);
        return response;
    }

    @Override
    protected String executeUpload(String uploadURL, Map<String, String> params, Map<String, File> files,
            UploadProgress writeListener) throws Exception {
        if (isVerbose()) {
            String msg = "API call: " + uploadURL;
            if (params != null && params.size() > 0)
                msg += ",P" + params;
            if (files != null && files.size() > 0)
                msg += ",F" + files;
            trace(msg);
        }
        return super.executeUpload(uploadURL, params, files, writeListener);
    }

    protected static ApplicationConfiguration getAppConfig(File deployZip, final String[] environments,
            final String[] implicitEnvironments) throws IOException {
        final ApplicationConfiguration applicationConfiguration = new ApplicationConfiguration();

        FileInputStream fin = new FileInputStream(deployZip);
        try {
            ZipHelper.unzipFile(fin, new ZipHelper.ZipEntryHandler() {
                public void unzip(ZipEntry entry, InputStream zis) throws IOException {
                    if (entry.getName().equals("META-INF/stax-application.xml")
                            || entry.getName().equals("WEB-INF/stax-web.xml")
                            || entry.getName().equals("WEB-INF/cloudbees-web.xml")) {
                        AppConfigHelper.load(applicationConfiguration, zis, environments, implicitEnvironments);
                    }
                }
            }, false);
        } finally {
            fin.close();
        }

        return applicationConfiguration;
    }

    protected String createParameter(Map<String, String> parameters) {
        if (parameters == null) {
            parameters = new HashMap<String, String>();
        }
        JSONObject jsonObject = new JSONObject(parameters);
        return jsonObject.toString();
    }

    public void tailLog(String appId, String logName, OutputStream out) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        params.put("app_id", appId);
        params.put("log_name", logName);
        String url = getRequestURL("tail", params, false);
        trace("API call: " + url);
        InputStream input = executeCometRequest(url);

        byte[] bytes = new byte[1024];
        int numRead = input.read(bytes);
        while (numRead != -1) {
            out.write(bytes, 0, numRead);
            numRead = input.read(bytes);
        }
    }

    protected Object apiCall(String apiMethod, Map<String, String> settings, Map<String, String> params)
            throws Exception {
        Map<String, File> fileParams = new HashMap<String, File>();
        if (settings != null) {
            for (Map.Entry<String, String> entry : settings.entrySet()) {
                String value = entry.getValue().toLowerCase();
                int idx = value.startsWith("file://") ? 7 : 0;
                if (idx == 0)
                    idx = value.startsWith("files://") ? 8 : 0;
                if (idx > 0) {
                    File file = new File(entry.getValue().substring(idx));
                    fileParams.put("FILE." + entry.getKey(), file);
                    params.put("FILENAME." + entry.getKey(), file.getName());
                }
            }
        }
        String response;
        // If files are to be uploaded use POST to upload
        if (fileParams.size() > 0) {
            String url = getApiUrl(apiMethod).toString();
            params.put("action", apiMethod);
            response = executeUpload(url, params, fileParams, new UploadProgress() {
                public void handleBytesWritten(long l, long l1, long l2) {
                }
            });
        } else {
            String url = getRequestURL(apiMethod, params);
            response = executeRequest(url);
        }
        return readResponse(response);
    }

    public String call(String action, Map<String, String> params) throws Exception {
        String url = getRequestURL(action, params);
        String response = executeRequest(url);
        return response;
    }

    protected XStream getXStream() throws Exception {
        XStream xstream;
        if (format.equals("json")) {
            xstream = new XStream(new JettisonMappedXmlDriver()) {
                protected MapperWrapper wrapMapper(MapperWrapper next) {
                    return new MapperWrapper(next) {
                        public boolean shouldSerializeMember(Class definedIn, String fieldName) {
                            return definedIn != Object.class && super.shouldSerializeMember(definedIn, fieldName);
                        }

                    };
                }
            };
        } else if (format.equals("xml")) {
            xstream = new XStream() {
                protected MapperWrapper wrapMapper(MapperWrapper next) {
                    return new MapperWrapper(next) {
                        public boolean shouldSerializeMember(Class definedIn, String fieldName) {
                            return definedIn != Object.class && super.shouldSerializeMember(definedIn, fieldName);
                        }

                    };
                }
            };
        } else {
            throw new Exception("Unknown format: " + format);
        }

        xstream.processAnnotations(SayHelloResponse.class);
        xstream.processAnnotations(ApplicationGetSourceUrlResponse.class);
        xstream.processAnnotations(ApplicationDeleteResponse.class);
        xstream.processAnnotations(ApplicationDeployResponse.class);
        xstream.processAnnotations(ApplicationDeployArchiveResponse.class);
        xstream.processAnnotations(ApplicationInstallResponse.class);
        xstream.processAnnotations(ApplicationInfo.class);
        xstream.processAnnotations(ApplicationInfoResponse.class);
        xstream.processAnnotations(ApplicationListResponse.class);
        xstream.processAnnotations(ApplicationRestartResponse.class);
        xstream.processAnnotations(ApplicationStatusResponse.class);
        xstream.processAnnotations(ApplicationSetMetaResponse.class);
        xstream.processAnnotations(ApplicationCheckSumsResponse.class);
        xstream.processAnnotations(ApplicationScaleResponse.class);
        xstream.processAnnotations(ApplicationSnapshotInfo.class);
        xstream.processAnnotations(ApplicationSnapshotListResponse.class);
        xstream.processAnnotations(ApplicationSnapshotStatusResponse.class);
        xstream.processAnnotations(DatabaseCreateResponse.class);
        xstream.processAnnotations(DatabaseSetPasswordResponse.class);
        xstream.processAnnotations(DatabaseDeleteResponse.class);
        xstream.processAnnotations(DatabaseInfo.class);
        xstream.processAnnotations(DatabaseInfoResponse.class);
        xstream.processAnnotations(DatabaseListResponse.class);
        xstream.processAnnotations(DatabaseSnapshotInfo.class);
        xstream.processAnnotations(DatabaseSnapshotListResponse.class);
        xstream.processAnnotations(DatabaseSnapshotDeployResponse.class);
        xstream.processAnnotations(DatabaseSnapshotDeleteResponse.class);
        xstream.processAnnotations(ErrorResponse.class);
        xstream.processAnnotations(AccountKeysResponse.class);
        xstream.processAnnotations(AccountInfo.class);
        xstream.processAnnotations(AccountListResponse.class);
        xstream.processAnnotations(ApplicationJarHashesResponse.class);
        xstream.processAnnotations(ConfigurationParametersResponse.class);
        xstream.processAnnotations(ConfigurationParametersUpdateResponse.class);
        xstream.processAnnotations(ConfigurationParametersDeleteResponse.class);
        xstream.processAnnotations(ServiceSubscriptionResponse.class);
        xstream.processAnnotations(ServiceSubscriptionListResponse.class);
        xstream.processAnnotations(ServiceSubscriptionDeleteResponse.class);
        xstream.processAnnotations(ServiceResourceInfo.class);
        xstream.processAnnotations(ServiceResourceResponse.class);
        xstream.processAnnotations(ServiceResourceListResponse.class);
        xstream.processAnnotations(ServiceResourceBindingListResponse.class);
        xstream.processAnnotations(ResourceBindingInfo.class);
        xstream.processAnnotations(AccountRegionInfo.class);
        xstream.processAnnotations(AccountRegionListResponse.class);
        xstream.processAnnotations(ApplicationInstanceInfo.class);
        xstream.processAnnotations(ApplicationInstanceListResponse.class);
        xstream.processAnnotations(ApplicationInstanceStatusResponse.class);
        xstream.processAnnotations(ApplicationInstanceInvokeResponse.class);
        xstream.processAnnotations(ApplicationConfigUpdateResponse.class);
        xstream.processAnnotations(ServiceResourceDeleteResponse.class);
        xstream.processAnnotations(ServiceResourceBindResponse.class);
        xstream.processAnnotations(ServiceResourceUnBindResponse.class);
        xstream.processAnnotations(RepositoryInfo.class);
        xstream.processAnnotations(CIInfo.class);
        xstream.processAnnotations(ApplicationCreateResponse.class);
        xstream.processAnnotations(ServerPoolInfo.class);
        xstream.processAnnotations(ServerPoolListResponse.class);
        xstream.processAnnotations(ServerPoolDeleteResponse.class);
        xstream.processAnnotations(ServerInfo.class);
        xstream.processAnnotations(ServerStatusResponse.class);
        xstream.processAnnotations(ServerRestoreResponse.class);
        xstream.processAnnotations(ServerReinitResponse.class);
        xstream.processAnnotations(ServiceResourceInvokeInfo.class);
        xstream.processAnnotations(ServiceSubscriptionInvokeInfo.class);
        xstream.processAnnotations(ServiceResourceResponse.class);
        xstream.processAnnotations(ServiceResourceInvokeResponse.class);
        xstream.processAnnotations(ServiceSubscriptionInvokeResponse.class);
        xstream.processAnnotations(ServiceListResponse.class);
        xstream.processAnnotations(ParameterSettings.class);
        xstream.processAnnotations(ResourceSettings.class);
        xstream.processAnnotations(DatabaseBackupInfo.class);
        xstream.processAnnotations(DatabaseBackupListResponse.class);
        xstream.processAnnotations(DatabaseBackupResponse.class);
        xstream.processAnnotations(DatabaseClusterInfo.class);
        xstream.processAnnotations(DatabaseClusterListResponse.class);

        // Hack to fix backward compatibility
        xstream.alias("net.stax.api.ApplicationStatusResponse", ApplicationStatusResponse.class);
        xstream.alias("net.stax.api.ApplicationSetMetaResponse", ApplicationSetMetaResponse.class);

        // BeesClient can be subtyped to offer more commands,
        // yet those may live in separate classloader.
        // use this.getClass().getClassLoader() to ensure
        // that all the request/response classes resolve.
        xstream.setClassLoader(getClass().getClassLoader());

        return xstream;
    }

    protected Object readResponse(String response) throws Exception {
        Object obj = getXStream().fromXML(response);
        if (obj instanceof ErrorResponse) {
            throw new BeesClientException((ErrorResponse) obj);
        }
        return obj;
    }

    public static String encodePassword(String password, String version) {
        if ("0.1".equals(version)) {
            try {
                MessageDigest sha = MessageDigest.getInstance("SHA");
                byte[] passwordDigest = sha.digest(password.getBytes("UTF8"));
                String result = new BigInteger(1, passwordDigest).toString(16);
                if (result.length() < 32) {
                    char[] padded = new char[32];
                    char[] raw = result.toCharArray();
                    Arrays.fill(padded, 0, 32 - raw.length, '0');
                    System.arraycopy(raw, 0, padded, 32 - raw.length, raw.length);
                    result = new String(padded);
                }
                return result;
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException("JVM is supposed to provide SHA instance of MessageDigest", e);
            } catch (UnsupportedEncodingException e) {
                throw new IllegalStateException("JVM is supposed to provide UTF-8 character encoding", e);
            }
        } else {
            return password;
        }
    }

    public void mainCall(String[] args) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        int argIndex = 0;
        if (argIndex < args.length) {
            String action = args[argIndex++];
            for (; argIndex < args.length; argIndex++) {
                String arg = args[argIndex];
                String[] pair = arg.split("=", 2);
                if (pair.length < 2) {
                    throw new BeesClient.UsageError("Malformed call parameter pair: " + arg);
                }
                params.put(pair[0], pair[1]);
            }
            String response = call(action, params);
            System.out.println(response);
        } else {
            throw new BeesClient.UsageError("Missing required action argument");
        }
    }

    public void main(String[] args) throws Exception {
        int argIndex = 0;
        Map<String, String> options = new HashMap<String, String>();
        for (; argIndex < args.length; argIndex++) {
            String arg = args[argIndex];
            if (arg.startsWith("-")) {
                if (arg.equals("--call") || arg.equals("-c")) {
                    options.put("operation", arg);
                } else if (arg.equals("--username") || arg.equals("-u")) {
                    options.put("username", arg);
                } else if (arg.equals("--password") || arg.equals("-p")) {
                    options.put("password", arg);
                } else if (arg.equals("--url") || arg.equals("-u")) {
                    options.put("url", arg);
                } else {
                    throw new BeesClient.UsageError("Unsupported option: " + arg);
                }
            } else {
                break;
            }
        }

        String operation = getRequiredOption("operation", options);
        BeesClient client = new BeesClient(getRequiredOption("url", options),
                getRequiredOption("username", options), getRequiredOption("password", options), "0.1", "1.0");

        if (operation.equals("call")) {
            String[] subArgs = new String[args.length - argIndex];
            for (int i = 0; i < subArgs.length; i++) {
                subArgs[i] = args[argIndex++];
            }
            client.main(subArgs);
        }
    }

    private static String getRequiredOption(String optionName, Map<String, String> options)
            throws BeesClient.UsageError {
        if (options.containsKey(optionName)) {
            return options.get(optionName);
        } else {
            throw new BeesClient.UsageError("Missing required flag: --" + optionName);
        }
    }

    private static File asFile(String filePath) {
        return filePath == null ? null : new File(filePath);
    }

    public static class UsageError extends Exception {
        UsageError(String reason) {
            super(reason);
        }
    }

    class PatchMethod extends EntityEnclosingMethod {
        PatchMethod() {
        }

        PatchMethod(String uri) {
            super(uri);
        }

        @Override
        public String getName() {
            return "PATCH";
        }
    }

    <T> T assertNotNull(T value, String arg) {
        if (value == null)
            throw new IllegalArgumentException("Null is not a valid value for the '" + arg + "' argument");
        return value;
    }
}