Source: Gitana.js

(function(window)
{
    Gitana = Base.extend(
    /** @lends Gitana.prototype */
    {
        /**
         * @constructs
         *
         * @class Gitana
         *
         * Configuration options should look like:
         *
         * {
         *    "clientKey": {String} the oauth2 client id,
         *    "clientSecret": [String] the oauth2 client secret,
         *    "baseURL": [String] the relative URI path of the base URL (assumed to be "/proxy"),
         *    "locale": [String] optional locale (assumed to be en_US),
         *    "storage": [String|Object] Gitana.OAuth2.Storage implementation or a string identifying where to store
         *       Gitana OAuth2 tokens ("local", "session", "memory") or empty for memory-only storage
         * }
         */
        constructor: function(settings)
        {
            var self = this;

            if (!settings)
            {
                settings = {};
            }

            if (settings.host)
            {
                settings.baseURL = settings.host + "/proxy";
            }

            this.applicationInfo = {};
            this.stackInfo = {};

            // build config
            var config = {
                "clientKey": null,
                "clientSecret": null,
                "baseURL": "/proxy",
                "locale": null,
                "application": null,
                "loadAppHelper": true,
                "storage": null
            };
            if (Gitana.DEFAULT_CONFIG)
            {
                for (var k in Gitana.DEFAULT_CONFIG)
                {
                    if (Gitana.DEFAULT_CONFIG.hasOwnProperty(k))
                    {
                        config[k] = Gitana.DEFAULT_CONFIG[k];
                    }
                }
            }
            Gitana.copyKeepers(config, Gitana.loadDefaultConfig());
            Gitana.copyKeepers(config, settings);

            if (typeof(config.cacheBuster) === "undefined")
            {
                config.cacheBuster = true;
            }


            //////////////////////////////////////////////////////////////////////////
            //
            // APPLY CONFIGURATION SETTINGS
            //

            // baseURL
            this.baseURL = config.baseURL;

            // locale
            this.locale = config.locale;




            //////////////////////////////////////////////////////////////////////////
            //
            // APPLY OAUTH2 SETTINGS
            //

            // set up our oAuth2 connection
            var options = {};
            if (config.clientKey) {
                options.clientKey = config.clientKey;
            }
            if (config.clientSecret) {
                options.clientSecret = config.clientSecret;
            }
            if (this.baseURL)
            {
                options.baseURL = this.baseURL;
                options.tokenURL = "/oauth/token";
            }
            // the driver requires the "api" scope to be granted
            options.requestedScope = "api";



            //////////////////////////////////////////////////////////////////////////
            //
            // PRIVILEGED METHODS
            //
            //

            this.updateOptions = function(o)
            {
                if (o)
                {
                    Gitana.copyInto(options, o);
                }
            };

            this.resetHttp = function(config)
            {
                var o = {};
                Gitana.copyInto(o, options);

                if (config)
                {
                    Gitana.copyInto(o, config);
                }

                if (!o.storage)
                {
                    o.storage = this.getOriginalConfiguration().storage;
                }

                self.http = new Gitana.OAuth2Http(o, o.storage);
            };

            this.setAuthInfo = function(authInfo)
            {
                this.authInfo = authInfo;
            };

            this.setStackInfo = function(stackInfo)
            {
                this.stackInfo = stackInfo;
            };

            this.setApplicationInfo = function(applicationInfo)
            {
                this.applicationInfo = applicationInfo;
            };

            this.getOriginalConfiguration = function()
            {
                return config;
            };

            this.getHttpHeaders = function()
            {
                var self = this;

                var headers = {};

                if (self.http && self.http.getBearerAuthorizationHeader())
                {
                    headers["Authorization"] = self.http.getBearerAuthorizationHeader();
                }

                return headers;
            };
        },

        /**
         * Sets the authentication info
         */
        getAuthInfo: function()
        {
            return this.authInfo;
        },

        getStackInfo: function()
        {
            return this.stackInfo;
        },

        getApplicationInfo: function()
        {
            return this.applicationInfo;
        },

        /**
         * Sets the default locale for interactions with the Gitana server by this driver.
         *
         * @public
         *
         * @param {String} locale locale string
         */
        setLocale: function(locale)
        {
            this.locale = locale;
        },

        /**
         * Retrieves the default locale being used by this driver.
         *
         * @returns {String} locale string
         */
        getLocale: function()
        {
            return this.locale;
        },

        /**
         * Default AJAX failure callback
         *
         * @public
         */
        defaultFailureCallback: function(http)
        {
            // if we're in debug mode, log a bunch of good stuff out to console
            if (this.debug)
            {
                if (typeof console != "undefined")
                {
                    var message = "Received bad http state (" + http.status + ")";
                    var stacktrace = null;

                    var json = null;

                    var responseText = http.responseText;
                    if (responseText)
                    {
                        json = JSON.parse(responseText);
                        if (json && json.message)
                        {
                            message = message + ": " + json.message;
                        }
                    }

                    if (json && json["stacktrace"])
                    {
                        stacktrace = json["stacktrace"];
                    }

                    console.log(message);
                    if (stacktrace)
                    {
                        console.log(stacktrace);
                    }
                }
            }
        },


        /**
         * Performs Ajax communication with the Gitana server.
         *
         * NOTE: For the most part, you shouldn't have to use this function since most of the things you'd want
         * to do with the Gitana server are wrapped by helper functions.
         *
         * @see Gitana.Driver#gitanaGet
         * @see Gitana.Driver#gitanaPost
         * @see Gitana.Driver#gitanaPut
         * @see Gitana.Driver#gitanaDel
         * @see Gitana.Driver#gitanaRequest
         *
         * @public
         *
         * @param {String} method The kind of method to invoke - "get", "post", "put", or "del"
         * @param {String} url The full URL to the resource being requested (i.e. "http://server:port/uri"}
         * @param {String} [contentType] In the case of a payload carrying request (i.e. not GET), the content type being sent.
         * @param {Object} [data] In the case of a payload carrying request (i.e. not GET), the data to plug into the payload.
         * @param {Object} [headers] A key/value map of headers to place into the request.
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.  If none provided, the default driver callback is used.
         */
        ajax: function(method, url, contentType, data, headers, successCallback, failureCallback)
        {
            var _this = this;

            // ensure headers
            if (!headers)
            {
                headers = {};
            }

            // treat the method
            if (method == null) {
                method = "GET";
            }
            method = method.toUpperCase();

            // flags
            var json = false;
            if (contentType == "application/json")
            {
                json = true;
            }

            // error checking
            if ( (method == "POST" || method == "PUT") )
            {
                headers["Content-Type"] = contentType;
                if (!contentType)
                {
                    Gitana.debug("Performing method: " + method + " but missing content type");
                    return;
                }
            }

            var toSend = data;

            // special handling for json
            if (json)
            {
                // if empty payload for payload-bearing methods, populate with {}
                if (method == "PUT" || method == "POST")
                {
                    if (!data)
                    {
                        data = {};
                    }
                }

                if (!Gitana.isString(data))
                {
                    // stringify
                    toSend = Gitana.stringify(data);
                }
            }

            //
            // if the URL is relative and we're running in a browser, then we can pad the URL
            // based on the URL of the browser
            //
            // otherwise, we can't handle relative URLs
            //
            if (url.substring(0,1) == "/")
            {
                // if window.location exists, then we're running on a browser
                if (!Gitana.isUndefined(window.location))
                {
                    var u = window.location.protocol + "//" + window.location.host;
                    if (window.location.host.indexOf(":") == -1)
                    {
                        if (window.location.port) {
                            u += ":" + window.location.port;
                        }
                    }
                    url = u + url;
                }
                else
                {
                    // relative urls are not supported outside of the browser
                    throw new Error("Relative URL not supported outside of the browser: " + url);
                }
            }

            var config = {
                "method": method,
                "url": url,
                "data": toSend,
                "headers": headers,
                "success": successCallback,
                "failure": failureCallback
            };

            Gitana.requestCount++;
            this.http.request(config);

            return this;
        },

        /**
         * Send an HTTP request via AJAX to the Gitana Server.
         *
         * This method will additionally make sure of the following:
         *
         *   1) That the Gitana Driver authentication ticket is plugged onto the request.
         *   2) That the Gitana Driver locale is plugged onto the request.
         *   3) That full object data is returned (including metadata).
         *
         * @public
         *
         * @param {String} method The kind of method to invoke - "get", "post", "put", or "del"
         * @param {String} url Either a full URL (i.e. "http://server:port/uri") or a URI against the driver's server URL (i.e. /repositories/...)
         * @param {Object} params parameter map
         * @param [String] contentType If the case of a payload carrying request (i.e. not GET), the content type being sent.
         * @param {Object} data In the case of a payload carrying request (i.e. not GET), the JSON to plug into the payload.
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.
         */
        gitanaRequest: function(method, url, params, contentType, data, headers, successCallback, failureCallback)
        {
            // make sure we compute the real url
            if (Gitana.startsWith(url, "/")) {
                url = this.baseURL + url;
            }

            if (!failureCallback)
            {
                failureCallback = this.defaultFailureCallback;
            }

            if (!headers)
            {
                headers = {};
            }

            /**
             * Primary success callback handler for oAuth call to server.
             *
             * @param responseObject
             * @param xhr
             */
            var onSuccess = function(responseObject, xhr)
            {
                if (successCallback)
                {
                    // call back with just the response text (or json)

                    var arg = responseObject.text;
                    if (contentType == "application/json")
                    {
                        try {
                            arg = new Gitana.Response(JSON.parse(arg));
                        } catch (ex) {
                            failureCallback(ex);
                        }
                    }

                    successCallback(arg);
                }
            };

            /**
             * Primary failure callback handler for oAuth call to server.
             *
             * @param responseObject
             * @param xhr
             */
            var onFailure = function(responseObject, xhr)
            {
                if (failureCallback)
                {
                    var httpError = {};

                    if (responseObject.timeout)
                    {
                        // due to a timeout
                        httpError["statusText"] = "Connection timed out";
                        httpError["status"] = xhr.status;
                        httpError["errorType"] = "timeout";
                        httpError["message"] = "Connection timed out";
                        httpError["response"] = responseObject;
                        httpError["xhr"] = xhr;
                    }
                    else
                    {
                        // due to an HTTP error
                        httpError["statusText"] = xhr.statusText;
                        httpError["status"] = xhr.status;
                        httpError["errorType"] = "http";
                        httpError["response"] = responseObject;
                        httpError["xhr"] = xhr;

                        var message = null;
                        var stacktrace = null;

                        var arg = responseObject.text;
                        if (contentType == "application/json")
                        {
                            try
                            {
                                var obj = new Gitana.Response(JSON.parse(arg));
                                if (obj.message)
                                {
                                    message = obj.message;
                                }
                                if (obj.stacktrace)
                                {
                                    stacktrace = obj.stacktrace;
                                }
                            }
                            catch (e) { }
                        }
                        if (message)
                        {
                            httpError.message = message;
                        }
                        if (stacktrace)
                        {
                            httpError.stacktrace = stacktrace;
                        }
                    }

                    failureCallback(httpError);
                }
            };

            // ensure we have some params
            if (!params)
            {
                params = {};
            }

            // adjust url to include "full" as well as "metadata" if not included
            if (Gitana.isEmpty(params["metadata"]))
            {
                params["metadata"] = true;
            }
            if (Gitana.isEmpty(params["full"]))
            {
                params["full"] = true;
            }

            if (this.locale) {
                headers["accept-language"] = this.locale;
                params["locale"] = this.locale;
            }


            // cache buster
            var cacheBuster = null;
            if (this.getOriginalConfiguration().cacheBuster === true)
            {
                cacheBuster = new Date().getTime();
            }
            else if (typeof(this.getOriginalConfiguration().cacheBuster) === "string")
            {
                cacheBuster = this.getOriginalConfiguration().cacheBuster;
            }
            else if (typeof(this.getOriginalConfiguration().cacheBuster) === "function")
            {
                cacheBuster = this.getOriginalConfiguration().cacheBuster();
            }
            if (cacheBuster)
            {
                params["cb"] = cacheBuster;
            }

            // update URL to include params
            for (var paramKey in params)
            {
                var paramValue = params[paramKey];
                if (Gitana.isFunction(paramValue))
                {
                    paramValue = paramValue.call();
                }
                else if (Gitana.isString(paramValue))
                {
                    // NOTHING TO DO
                }
                else if (Gitana.isNumber(paramValue))
                {
                    // NOTHING TO DO
                }
                else
                {
                    paramValue = escape(Gitana.stringify(paramValue, false));
                }

                // apply
                if (url.indexOf("?") > -1)
                {
                    url = url + "&" + paramKey + "=" + paramValue;
                }
                else
                {
                    url = url + "?" + paramKey + "=" + paramValue;
                }
            }

            return this.ajax(method, url, contentType, data, headers, onSuccess, onFailure);
        },

        /**
         * Sends an HTTP GET request to the Gitana server.
         *
         * @public
         *
         * @param {String} url Either a full URL (i.e. "http://server:port/uri") or a URI against the driver's server URL (i.e. /repositories/...)
         * @param {Object} params request parameters
         * @param {Object} headers request headers
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.
         */
        gitanaGet: function(url, params, headers, successCallback, failureCallback)
        {
            return this.gitanaRequest("GET", url, params, "application/json", null, headers, successCallback, failureCallback);
        },

        /**
         * Sends an HTTP GET request to the Gitana server.
         *
         * @public
         *
         * @param {String} url Either a full URL (i.e. "http://server:port/uri") or a URI against the driver's server URL (i.e. /repositories/...)
         * @param {Object} params request parameters
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.
         */
        gitanaDownload: function(url, params, successCallback, failureCallback)
        {
            return this.gitanaRequest("GET", url, params, null, null, {}, successCallback, failureCallback);
        },

        /**
         * Sends an HTTP POST request to the Gitana server.
         *
         * @public
         *
         * @param {String} url Either a full URL (i.e. "http://server:port/uri") or a URI against the driver's server URL (i.e. /repositories/...)
         * @param {Object} params request parameters
         * @param {Object} [jsonData] The JSON to plug into the payload.
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.
         */
        gitanaPost: function(url, params, jsonData, successCallback, failureCallback)
        {
            return this.gitanaRequest("POST", url, params, "application/json", jsonData, {}, successCallback, failureCallback);
        },

        /**
         * Sends an HTTP POST request to the Gitana server.
         *
         * @public
         *
         * @param {String} url Either a full URL (i.e. "http://server:port/uri") or a URI against the driver's server URL (i.e. /repositories/...)
         * @param {Object} params request parameters
         * @param {String} contentType content type being sent
         * @param {Object} [jsonData] The JSON to plug into the payload.
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.
         */
        gitanaUpload: function(url, params, contentType, data, successCallback, failureCallback)
        {
            return this.gitanaRequest("POST", url, params, contentType, data, {}, successCallback, failureCallback);
        },

        /**
         * Sends an HTTP PUT request to the Gitana server.
         *
         * @public
         *
         * @param {String} url Either a full URL (i.e. "http://server:port/uri") or a URI against the driver's server URL (i.e. /repositories/...)
         * @param {Object} params request parameters
         * @param {Object} [jsonData] The JSON to plug into the payload.
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.
         */
        gitanaPut: function(url, params, jsonData, successCallback, failureCallback)
        {
            return this.gitanaRequest("PUT", url, params, "application/json", jsonData, {}, successCallback, failureCallback);
        },

        /**
         * Sends an HTTP DELETE request to the Gitana server.
         *
         * @public
         *
         * @param {String} url Either a full URL (i.e. "http://server:port/uri") or a URI against the driver's server URL (i.e. /repositories/...)
         * @param {Object} params request parameters
         * @param {Function} [successCallback] The function to call if the operation succeeds.
         * @param {Function} [failureCallback] The function to call if the operation fails.
         */
        gitanaDelete: function(url, params, successCallback, failureCallback)
        {
            return this.gitanaRequest("DELETE", url, params, "application/json", null, {}, successCallback, failureCallback);
        },

        getFactory: function()
        {
            return new Gitana.ObjectFactory();
        },


        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // CHAINING METHODS
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Authenticates as the supplied user.
         *
         * A user can either be authenticated using username/password credentials or via an authentication code.
         *
         * Authorization Code flow:
         *
         *   {
         *     "code": "<code>",
         *     "redirectUri": "<redirectUri>"
         *   }

         * Username/password flow:
         *
         *   {
         *     "username": "<username>",
         *     "password": "<password>"
         *   }
         *
         * Implicit flow:
         *
         *   {
         *     "accessToken": "<accessToken>",
         *     "redirectUri": "<redirectUri>"
         *   }
         *
         * Using Gitana Ticket from a cookie:
         *
         *   {
         *     "cookie": true
         *   }
         *
         * Using Gitana Ticket (explicitly provided):
         *
         *   {
         *     "ticket": "<ticket>"
         *   }
         *
         * An authentication failure handler can be passed as the final argument
         *
         * @chained platform
         *
         * @param {Object} settings
         * @param [Function] authentication failure handler
         */
        authenticate: function(settings, authFailureHandler)
        {
            var driver = this;

            // build config
            var config = {
                "code": null,
                "redirectUri": null,
                "username": null,
                "password": null,
                "accessToken": null,
                "ticket": null,
                "cookie": null,
                "ticketMaxAge": null
            };
            Gitana.copyKeepers(config, Gitana.loadDefaultConfig());
            Gitana.copyKeepers(config, settings);

            // platform config (for cache key determination)
            var platformConfig = {
                "key": null,
                "ticket": null,
                "username": null,
                "clientKey": null
            };
            Gitana.copyKeepers(platformConfig, this.getOriginalConfiguration());
            Gitana.copyKeepers(platformConfig, settings);
            var platformCacheKey = platformConfig.key;
            if (!platformCacheKey)
            {
                platformCacheKey = Gitana.determinePlatformCacheKey(platformConfig, true);
            }
            if (platformCacheKey)
            {
                this.platformCacheKey = platformCacheKey;
            }

            // build a cluster instance
            var cluster = new Gitana.Cluster(this, {});

            var applyPlatformCache = function(driver, platform)
            {
                var platformCacheKey = driver.platformCacheKey;
                if (platformCacheKey)
                {
                    Gitana.PLATFORM_CACHE(platformCacheKey, platform);
                }

                // always cache on ticket as well
                var ticket = driver.getAuthInfo().getTicket();
                if (ticket) {
                    Gitana.PLATFORM_CACHE(ticket, platform);
                }
            };

            // run with this = platform
            var doAuthenticate = function()
            {
                var platform = this;

                // we provide a fallback if no flow type is specified, using "password" flow with guest/guest
                if (!config.code && !config.username && !config.accessToken && !config.cookie && !config.ticket)
                {
                    config.username = "guest";
                    config.password = "guest";
                }

                //
                // authenticate via the authentication flow
                //
                if (config.code)
                {
                    // clear existing cookie and ticket
                    config.authorizationFlow = Gitana.OAuth2Http.AUTHORIZATION_CODE;
                    driver.resetHttp(config);
                    Gitana.deleteCookie("GITANA_TICKET", "/");

                    // fetch the auth info
                    driver.gitanaGet("/auth/info", {}, {}, function(response) {

                        var authInfo = new Gitana.AuthInfo(response);
                        driver.setAuthInfo(authInfo);

                        // TODO: fix this
                        // kill the JSESSIONID cookie which comes back from the proxy and ties us to a session
                        // on the Gitana server
                        Gitana.deleteCookie("JSESSIONID", "/");

                        // apply platform cache
                        applyPlatformCache(driver, platform);

                        // now continue the platform chain after we reload
                        platform.reload();
                        platform.next();

                    }, function(http) {

                        // if authentication fails, respond to custom auth failure handler
                        if (authFailureHandler)
                        {
                            authFailureHandler.call(platform, http);
                        }

                    });
                }

                //
                // authenticate via password flow
                //
                else if (config.username)
                {
                    // clear existing cookie and ticket
                    config.authorizationFlow = Gitana.OAuth2Http.PASSWORD;
                    driver.resetHttp(config);
                    Gitana.deleteCookie("GITANA_TICKET", "/");

                    // retrieve auth info and plug into the driver
                    driver.gitanaGet("/auth/info", {}, {}, function(response) {
                        var authInfo = new Gitana.AuthInfo(response);
                        driver.setAuthInfo(authInfo);

                        // TODO: fix this
                        // kill the JSESSIONID cookie which comes back from the proxy and ties us to a session
                        // on the Gitana server
                        Gitana.deleteCookie("JSESSIONID", "/");

                        // apply platform cache
                        applyPlatformCache(driver, platform);

                        // now continue the platform chain after we reload
                        platform.reload();
                        platform.next();

                    }, function(http) {

                        // if authentication fails, respond to custom auth failure handler
                        if (authFailureHandler)
                        {
                            authFailureHandler.call(platform, http);
                        }

                    });
                }

                //
                // authenticate via implicit "token" flow
                //
                else if (config.accessToken)
                {
                    // clear existing cookie and ticket
                    config.authorizationFlow = Gitana.OAuth2Http.TOKEN;
                    driver.resetHttp(config);
                    Gitana.deleteCookie("GITANA_TICKET", "/");

                    // fetch the auth info
                    driver.gitanaGet("/auth/info", {}, {}, function(response) {

                        var authInfo = new Gitana.AuthInfo(response);
                        driver.setAuthInfo(authInfo);

                        // TODO: fix this
                        // kill the JSESSIONID cookie which comes back from the proxy and ties us to a session
                        // on the Gitana server
                        Gitana.deleteCookie("JSESSIONID", "/");

                        // apply platform cache
                        applyPlatformCache(driver, platform);

                        // now continue the platform chain after we reload
                        platform.reload();
                        platform.next();

                    }, function(http) {

                        // if authentication fails, respond to custom auth failure handler
                        if (authFailureHandler)
                        {
                            authFailureHandler.call(platform, http);
                        }

                    });
                }

                //
                // authenticate using an existing cookie
                //
                else if (config.cookie)
                {
                    // reuse an existing cookie (token flow)
                    config.authorizationFlow = Gitana.OAuth2Http.COOKIE;
                    driver.resetHttp(config);

                    // fetch the auth info
                    driver.gitanaGet("/auth/info", {}, {}, function(response) {

                        var authInfo = new Gitana.AuthInfo(response);
                        driver.setAuthInfo(authInfo);

                        if (authInfo.accessToken)
                        {
                            driver.http.accessToken(authInfo.accessToken);
                        }

                        // TODO: fix this
                        // kill the JSESSIONID cookie which comes back from the proxy and ties us to a session
                        // on the Gitana server
                        Gitana.deleteCookie("JSESSIONID", "/");

                        // apply platform cache
                        applyPlatformCache(driver, platform);

                        // now continue the platform chain after we reload
                        platform.reload();
                        platform.next();

                    }, function(http) {

                        // if authentication fails, respond to custom auth failure handler
                        if (authFailureHandler)
                        {
                            authFailureHandler.call(platform, http);
                        }

                    });

                }

                //
                // authenticate using an explicit gitana ticket
                //
                else if (config.ticket)
                {
                    // reuse an existing cookie (token flow)
                    config.authorizationFlow = Gitana.OAuth2Http.TICKET;
                    driver.resetHttp(config);

                    var headers = {
                        "GITANA_TICKET": config.ticket
                    };

                    // fetch the auth info
                    driver.gitanaGet("/auth/info", {}, headers, function(response) {

                        var authInfo = new Gitana.AuthInfo(response);
                        driver.setAuthInfo(authInfo);

                        // TODO: fix this
                        // kill the JSESSIONID cookie which comes back from the proxy and ties us to a session
                        // on the Gitana server
                        Gitana.deleteCookie("JSESSIONID", "/");

                        // apply platform cache
                        applyPlatformCache(driver, platform);

                        // now continue the platform chain after we reload
                        platform.reload();
                        platform.next();

                    }, function(http) {

                        // if authentication fails, respond to custom auth failure handler
                        if (authFailureHandler)
                        {
                            authFailureHandler.call(platform, http);
                        }

                    });

                }
                else
                {
                    var message = "Unsupported authentication flow - you must provide either a username, authorization code, access token or select cookie-based authentication";

                    if (authFailureHandler)
                    {
                        authFailureHandler.call(platform, {
                            "message": message
                        });
                    }
                    else
                    {
                        throw new Error(message);
                    }
                }
            };

            var result = this.getFactory().platform(cluster);
            return Chain(result).then(function() {

                // NOTE: this = platform

                doAuthenticate.call(this);

                // tell the chain that we'll manually handle calling next()
                return false;
            });
        },

        reloadAuthInfo: function(callback)
        {
            var driver = this;

            driver.gitanaGet("/auth/info", {}, {}, function(response) {

                var authInfo = new Gitana.AuthInfo(response);
                driver.setAuthInfo(authInfo);

                callback();

            }, function(http) {
                callback(null, http);
            });
        },

        /**
         * Clears any authentication for the driver.
         */
        clearAuthentication: function()
        {
            if (this.http.clearStorage)
            {
                this.http.clearStorage();
            }

            this.resetHttp();
            Gitana.deleteCookie("GITANA_TICKET", "/");
        },

        /**
         * Refreshes the authentication access token.
         *
         * @param callback
         */
        refreshAuthentication: function(callback)
        {
            this.http.refresh(function(err) {
                callback(err);
            });
        },

        /**
         * Destructor function, called at the end of the driver instance's lifecycle
         */
        destroy: function()
        {
            this.clearAuthentication();
        }

    });


    //
    // STATICS
    // Special Groups

    Gitana.EVERYONE = {
        "name": "everyone",
        "type": "GROUP"
    };

    // temporary location for this code
    Gitana.toCopyDependencyChain = function(typedID)
    {
        var array = [];

        if (typedID.getType() === "node")
        {
            array = array.concat(Gitana.toCopyDependencyChain(typedID.getBranch()));
            array = array.concat({
                "typeId": "changeset",
                "id": typedID.getSystemMetadata().getChangesetId()
            });
        }
        else if (typedID.getType() === "association")
        {
            array = array.concat(Gitana.toCopyDependencyChain(typedID.getBranch()));
            array = array.concat({
                "typeId": "changeset",
                "id": typedID.getSystemMetadata().getChangesetId()
            });
        }
        else if (typedID.getType() === "branch")
        {
            array = array.concat(Gitana.toCopyDependencyChain(typedID.getRepository()));
        }
        else if (typedID.getType() === "platform")
        {
            // nothing to do here
        }
        else if (typedID.getType() === "stack")
        {
            array = array.concat(Gitana.toCopyDependencyChain(typedID.getPlatform()));
        }
        else if (typedID.getType() === "project")
        {
            array = array.concat(Gitana.toCopyDependencyChain(typedID.getPlatform()));
        }
        else
        {
            array = array.concat(Gitana.toCopyDependencyChain(typedID.getPlatform()));
        }

        array.push(Gitana.toDependencyObject(typedID));

        return array;
    };

    Gitana.toDependencyObject = function(typedID)
    {
        return {
            "typeId": typedID.getType(),
            "id": typedID.getId()
        };
    };

    Gitana.TypedIDConstants = {};
    Gitana.TypedIDConstants.TYPE_APPLICATION = "application";
    Gitana.TypedIDConstants.TYPE_EMAIL = "email";
    Gitana.TypedIDConstants.TYPE_EMAIL_PROVIDER = "emailprovider";
    Gitana.TypedIDConstants.TYPE_REGISTRATION = "registration";
    Gitana.TypedIDConstants.TYPE_PAGE_RENDITION = "pageRendition";
    Gitana.TypedIDConstants.TYPE_SETTINGS = "settings";

    // cluster
    Gitana.TypedIDConstants.TYPE_CLUSTER = "cluster";
    Gitana.TypedIDConstants.TYPE_JOB = "job";
    Gitana.TypedIDConstants.TYPE_LOG_ENTRY = "logEntry";

    // directory
    Gitana.TypedIDConstants.TYPE_DIRECTORY = "directory";
    Gitana.TypedIDConstants.TYPE_IDENTITY = "identity";
    Gitana.TypedIDConstants.TYPE_CONNECTION = "connection";

    // domain
    Gitana.TypedIDConstants.TYPE_DOMAIN = "domain";
    Gitana.TypedIDConstants.TYPE_DOMAIN_GROUP = "group";
    Gitana.TypedIDConstants.TYPE_DOMAIN_USER = "user";

    // platform
    Gitana.TypedIDConstants.TYPE_PLATFORM = "platform";
    Gitana.TypedIDConstants.TYPE_AUTHENTICATION_GRANT = "authenticationGrant";
    Gitana.TypedIDConstants.TYPE_BILLING_PROVIDERS_CONFIGURATION = "billingProviderConfiguration";
    Gitana.TypedIDConstants.TYPE_CLIENT = "client";
    Gitana.TypedIDConstants.TYPE_DESCRIPTOR = "externalServiceDescriptor";
    Gitana.TypedIDConstants.TYPE_STACK = "stack";
    Gitana.TypedIDConstants.TYPE_PROJECT = "project";
    Gitana.TypedIDConstants.TYPE_SCHEDULED_WORK = "scheduled-work";
    Gitana.TypedIDConstants.TYPE_REPORT = "report";
    Gitana.TypedIDConstants.TYPE_WORKFLOW_INSTANCE = "workflowInstance";
    Gitana.TypedIDConstants.TYPE_WORKFLOW_MODEL = "workflowModel";
    Gitana.TypedIDConstants.TYPE_WORKFLOW_TASK = "workflowTask";
    Gitana.TypedIDConstants.TYPE_WORKFLOW_COMMENT = "workflowComment";

    // registrar
    Gitana.TypedIDConstants.TYPE_REGISTRAR = "registrar";
    Gitana.TypedIDConstants.TYPE_METER = "meter";
    Gitana.TypedIDConstants.TYPE_PLAN = "plan";
    Gitana.TypedIDConstants.TYPE_TENANT = "tenant";

    // repository
    Gitana.TypedIDConstants.TYPE_REPOSITORY = "repository";
    Gitana.TypedIDConstants.TYPE_ASSOCIATION = "association";
    Gitana.TypedIDConstants.TYPE_BRANCH = "branch";
    Gitana.TypedIDConstants.TYPE_CHANGESET = "changeset";
    Gitana.TypedIDConstants.TYPE_NODE = "node";
    Gitana.TypedIDConstants.TYPE_RELEASE = "release";

    // vault
    Gitana.TypedIDConstants.TYPE_VAULT = "vault";
    Gitana.TypedIDConstants.TYPE_ARCHIVE = "archive";

    // warehouse
    Gitana.TypedIDConstants.TYPE_WAREHOUSE = "warehouse";
    Gitana.TypedIDConstants.TYPE_INTERACTION = "interaction";
    Gitana.TypedIDConstants.TYPE_INTERACTION_APPLICATION = "interactionApplication";
    Gitana.TypedIDConstants.TYPE_INTERACTION_NODE = "interactionNode";
    Gitana.TypedIDConstants.TYPE_INTERACTION_PAGE = "interactionPage";
    Gitana.TypedIDConstants.TYPE_INTERACTION_REPORT = "interactionReport";
    Gitana.TypedIDConstants.TYPE_INTERACTION_REPORT_ENTRY = "interactionReportEntry";
    Gitana.TypedIDConstants.TYPE_INTERACTION_SESSION = "interactionSession";
    Gitana.TypedIDConstants.TYPE_INTERACTION_USER = "interactionUser";

    Gitana.TypedIDConstants.TYPE_INTERACTION_CONTINENT = "interactionContinent";
    Gitana.TypedIDConstants.TYPE_INTERACTION_COUNTRY = "interactionCountry";
    Gitana.TypedIDConstants.TYPE_INTERACTION_CITY = "interactionCity";
    Gitana.TypedIDConstants.TYPE_INTERACTION_REGION = "interactionRegion";
    Gitana.TypedIDConstants.TYPE_INTERACTION_POSTALCODE = "interactionPostalCode";
    Gitana.TypedIDConstants.TYPE_INTERACTION_USERAGENT = "interactionUserAgent";
    Gitana.TypedIDConstants.TYPE_INTERACTION_OPERATINGSYSTEM = "interactionOperatingSystem";
    Gitana.TypedIDConstants.TYPE_INTERACTION_DEVICE = "interactionDevice";

    Gitana.TypedIDConstants.TYPE_CONVERSION_TRIGGER = "conversionTrigger";

    // web host
    Gitana.TypedIDConstants.TYPE_WEB_HOST = "webhost";
    Gitana.TypedIDConstants.TYPE_AUTO_CLIENT_MAPPING = "autoClientMapping";
    Gitana.TypedIDConstants.TYPE_TRUSTED_DOMAIN_MAPPING = "trustedDomainMapping";
    Gitana.TypedIDConstants.TYPE_DEPLOYED_APPLICATION = "deployedApplication";

    Gitana.handleJobCompletion = function(chain, cluster, jobId, synchronous, reportFn)
    {
        var jobFinalizer = function() {

            return Chain(cluster).readJob(jobId).then(function() {

                if (reportFn) {
                    reportFn(this);
                }

                if (!synchronous || (synchronous && (this.getState() == "FINISHED" || this.getState() == "ERROR")))
                {
                    chain.loadFrom(this);
                    chain.next();
                }
                else
                {
                    // reset timeout
                    window.setTimeout(jobFinalizer, 1000);
                }

            });
        };

        // set timeout
        window.setTimeout(jobFinalizer, 1000);
    };

    /** Extension point for loading default config for server-side containers **/
    Gitana.loadDefaultConfig = function()
    {
    };

    /**
     * Simple in-memory cache implementation for use by-default by the driver.
     *
     * @return {Function}
     */
    Gitana.MemoryCache = function()
    {
        var cache = {};

        return function(k, v)
        {
            if (!Gitana.isUndefined(v))
            {
                if (v) {
                    cache[k] = v;
                }
                else {
                    delete cache[k];
                }
            }

            // support for "clear" method - removes everything from cache
            if (k == "clear")
            {
                var za = [];
                for (var z in cache)
                {
                    za.push(z);
                }
                for (var i = 0; i < za.length; i++)
                {
                    delete cache[za[i]];
                }
            }

            return cache[k];
        };
    };


    /////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // PLATFORM CACHE
    //
    //

    // extension point - override with other implementations
    Gitana.PLATFORM_CACHE = Gitana.MemoryCache();

    Gitana.determinePlatformCacheKey = function(config, fallbackToDefault)
    {
        var cacheKey = null;

        // "ticket" authentication - key = ticket
        if (config.ticket) {
            cacheKey = config.ticket;
        }
        else if (config.clientKey && config.username) {
            cacheKey = config.clientKey + ":" + config.username;
        }
        else if (fallbackToDefault)
        {
            // if no config provided, use "default" key
            cacheKey = "default";
        }

        return cacheKey;
    };

    /**
     * Connects to a Gitana platform.
     *
     * @param config
     * @param [callback] optional callback function that gets called once the server has been connected to.  If no
     *                   "application" config parameter is present, then the callback function is called with the this
     *                   context set to the platform.  If an "application" config parameter is present, then the stack
     *                   for the application is loaded and references are resolved and the this context will be the
     *                   app helper instance.  This callback also acts as an error handler for any authentication issues.
     *                   If an auth error happens, the err is passed to the callback as the first and only argument.
     *
     * @return {*}
     */
    Gitana.connect = function(config, callback)
    {
        // allow for no config, callback-only
        if (Gitana.isFunction(config)) {
            callback = config;
            config = null;
        }

        var missingConfig = false;

        if (!config) {
            config = {};
            missingConfig = true;
        }

        if (Gitana.isString(config)) {
            config = {"key": config};
        }

        // by default, set invalidatePlatformCache to false
        if (typeof(config.invalidatePlatformCache) == "undefined")
        {
            config.invalidatePlatformCache = false;
        }

        // if no config key specified, we can generate one...
        if (!config.key)
        {
            config.key = Gitana.determinePlatformCacheKey(config, missingConfig);
        }

        // default to load app helper if not defined
        if (typeof(config.loadAppHelper) == "undefined")
        {
            config.loadAppHelper = true;
        }

        // this gets called once the platform is drawn from cache or created
        // fires the callback and passes in the platform or the app helper
        var setupContext = function(platformCacheKey)
        {
            // NOTE: this == platform

            // if their configuration contains the "application" setting, then auto-load the app() context
            // note that config.application could be undefined (we require explicit NULL here for copyKeepers)
            if (config.loadAppHelper)
            {
                var appConfig = {
                    "application": (config.application ? config.application: null)
                };
                Gitana.copyKeepers(appConfig, Gitana.loadDefaultConfig());
                Gitana.copyKeepers(appConfig, this.getDriver().getOriginalConfiguration());
                if (appConfig.application) {

                    var appSettings = {
                        "application": appConfig.application
                    };
                    if (platformCacheKey) {
                        appSettings.appCacheKey = platformCacheKey + "_" + appConfig.application;
                    }
                    this.app(appSettings, function(err) {
                        if (callback) {
                            // NOTE: this == app helper
                            callback.call(this, err);
                        }
                    });
                }
                else {
                    if (callback) {
                        callback.call(this);
                    }
                }
            }
            else
            {
                if (callback) {
                    callback.call(this);
                }
            }
        };

        // support for invalidatePlatformCache
        if (config.key && config.invalidatePlatformCache)
        {
            Gitana.disconnect(config.key);
        }

        // either retrieve platform from cache or authenticate
        var platform = null;
        if (config.key) {
            platform = Gitana.PLATFORM_CACHE(config.key);
        }
        if (platform)
        {
            // platform already loaded

            // spawn off a new copy for thread safety
            platform = Chain(new Gitana.Platform(platform.getCluster(), platform));
            setupContext.call(platform, config.key);
            return platform;
        }

        // if they didn't provide a config and made it this far, then lets assume a cookie based config?
        if (missingConfig)
        {
            config["cookie"] = true;
        }

        // load it up
        return new Gitana(config).authenticate(config, function(err) {

            if (callback) {
                callback.call(this, err);
            }

        }).then(function() {

            // NOTE: this == platform

            setupContext.call(this, config.key);

        });
    };

    /**
     * Disconnects a platform from the cache.
     *
     * @param key
     * @param expireAccessToken
     */
    Gitana.disconnect = function(key, expireAccessToken)
    {
        if (!key) {
            key = "default";
        }

        var platform = Gitana.PLATFORM_CACHE(key);
        if (platform)
        {
            // if we are meant to expire the server-side access token,
            // fire off a signal to the Cloud CMS server to do so
            // we ignore whether this succeeds or fails
            if (expireAccessToken)
            {
                platform.getDriver().gitanaPost("/auth/expire", {}, {}, function() {
                    // success
                }, function(err) {
                    // error
                });
            }

            var badKeys = [];
            for (var k in Gitana.APPS)
            {
                if (k.indexOf(key + "_") == 0)
                {
                    badKeys.push(k);
                }
            }
            for (var i = 0; i < badKeys.length; i++)
            {
                delete Gitana.APPS[badKeys[i]];
            }

            var ticket = platform.getDriver().getAuthInfo().getTicket();
            if (ticket)
            {
                Gitana.PLATFORM_CACHE(ticket, null);
            }

            Gitana.PLATFORM_CACHE(key, null);

            platform.getDriver().destroy();
        }
    };

    // holds a total count of Ajax requests originated from the driver
    Gitana.requestCount = 0;

    // version of the driver
    Gitana.VERSION = "__VERSION__";

    // allow for optional global assignment
    // TODO: until we clean up the "window" variable reliance, we have to always set onto window again
    // TODO: to support loading within NodeJS
    //if (window && !window.Gitana) {
    if (window) {
        window.Gitana = Gitana;
    }

    /**
     * Resets the driver (used for test purposes).
     */
    Gitana.reset = function()
    {
        Gitana.HTTP_TIMEOUT = 120000;

        Gitana.PLATFORM_CACHE("clear");
        Gitana.deleteCookie("GITANA_TICKET");
    };

    // insertion point for on-load adjustments (cloudcms-net server)
    Gitana.__INSERT_MARKER = null;

    // toggles use of GET method when possible (rather than POST)
    // useful for branch.queryNodes()
    Gitana.PREFER_GET_OVER_POST = false;

    // method to call when a refresh token fails to acquire the access token
    Gitana.REFRESH_TOKEN_FAILURE_FN = function(http) {
        http.clearStorage();
        Gitana.deleteCookie("GITANA_TICKET");
    };

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // support for CSRF / XSRF
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////

    // the CSRF token can be explicitly stored here if you want to forgo cookies as a storage mechanism
    Gitana.CSRF_TOKEN = null;

    // these cookies can be consulted by the driver to acquire the csrf token
    // override this with different cookie names if your framework requires it
    Gitana.CSRF_COOKIE_NAMES = ["CSRF-TOKEN", "XSRF-TOKEN"];

    // the csrf token is sent over the wire using XHR and this header name
    Gitana.CSRF_HEADER_NAME = "X-CSRF-TOKEN";

})(window);