diff src/DML/MainVisBundle/Resources/assets/marionette/modules/DataModule/DataModule.10-CliopatriaAPI.js @ 0:493bcb69166c

added public content
author Daniel Wolff
date Tue, 09 Feb 2016 20:54:02 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/DML/MainVisBundle/Resources/assets/marionette/modules/DataModule/DataModule.10-CliopatriaAPI.js	Tue Feb 09 20:54:02 2016 +0100
@@ -0,0 +1,342 @@
+"use strict";
+/**
+ * The module wraps all API calls to the data server and abstracts JSONP calls.
+ *
+ * API spec: https://code.soundsoftware.ac.uk/projects/dml/wiki/Vis_API_-_front_end_~_data_server
+ *
+ *
+ * Module options:
+ *
+ *   apiRootPaths
+ *      A pool of URLs from where the data are obtained. Multiple entry points (domains) can be used.
+ *      The choice of a particular root path is "random", i.e. this is a "hash" function of a query contents:
+ *          "doThis?x=1&y=1"   -> path 1 of 3
+ *          "doThat?x=1&y=1"   -> path 3
+ *          "doThat?x=1&y=123" -> path 1
+ *          "doThat?x=1&y=2"   -> path 2
+ *          ...
+ *          "doThis?x=1&y=1"   -> path 1
+ *
+ *      The same query is always requested from the same instance (domain) to facilitate HTTP caching.
+ *      // instead of http:// and https:// can be used for protocol auto-selection.
+ *      Trailing slash is compulsory.
+ *
+ *      Examples:
+ *          ["//127.0.0.1/"],
+ *          ["//127.0.0.1/my-path/to/dml-api/"],
+ *          ["//a.example.com/dml-api/", "//b.example.com/dml-api/", "//c.example.com/dml-api/"],
+ *
+ *   apiVersion
+ *      A natural number representing the version of the data API (i.e. the available features of the interface)
+ *
+ *      Examples:
+ *          1
+ *          42
+ *
+ *   dataVersion
+ *      A natural number or a string representing the version of the selected music library.
+ *      As API calls are supplemented with dv=[dataVersion], a change in the value of dataVersion drops the HTTP cache.
+ *      dataVersion is delivered to the front end with the contents of the web page.
+ *
+ *      Examples:
+ *          42
+ *          "2015-01-01"
+ *
+ */
+App.module("DataModule.CliopatriaAPI", function(CliopatriaAPI, App, Backbone, Marionette, $, _, Logger) {
+    // Prevent auto start
+    CliopatriaAPI.startWithParent = false;
+
+    // Define module options
+    var defaultModuleOptions = {
+            // if a request is not returned in this number of ms, handle it as failed
+            callbackTimeout: 30000,
+
+            // expired requests are being checked this often (ms)
+            callbackTimeoutCheckingInterval: 1000,
+
+            // callback of JSONP responses
+            callbackFunctionName: "jsonp_cb",
+
+            // optional data version suffix that is appended to all API requests, e.g. "123" → &dv=123
+            dataVersion: null,
+
+            apiRootPaths: [],
+
+            apiVersion: 1,
+
+            dataCaching: true
+    };
+
+    // =========================================================================
+    // private variables
+    var logger = null;
+
+    /**
+     * derived from CliopatriaAPI.options.dataVersion
+     */
+    var dataVersionSuffix = null;
+
+    /**
+     * Real API root paths include API version (e.g. http://example.com/path/to/api/v42/)
+     */
+    var realAPIRootPaths = null;
+
+    /**
+     * The number of the real API paths
+     */
+    var realAPIRootPathsCount = null;
+
+
+    /**
+     * query pool: an object where keys correspond to sent queries (methodName?param1=1&...&dv=42) and values are:
+     *    [timeSent, methodName, parameters, callbacks, scriptElement, query]
+     */
+    var queryPool = null;
+
+    /**
+     * Generates query from API method name and parameters (+ appends data version)
+     * Parameters are sorted alphabetically, except dv, which is always the last
+     */
+    var generateQuery = null;
+
+    /**
+     * a function that is called every callbackTimeoutCheckingInterval ms and checks for expired query pool entries
+     */
+    var callbackTimeoutExpiryChecker = null;
+
+    /**
+     * timeout (interval) handler for callbackTimeoutExpiryChecker
+     */
+    var callbackTimeoutExpiryCheckerId = null;
+
+    /**
+     * Initialization checker
+     */
+    var assertModuleIsInitialized = function() {
+        if (!logger) {
+            throw "DataModule.CliopatriaAPI has not been initialized";
+        }
+    };
+
+    /**
+     * Module initializer
+     *
+     */
+    CliopatriaAPI.addInitializer(function(options) {
+
+        CliopatriaAPI.options = _.extend(defaultModuleOptions, options);
+
+        logger = Logger.get("DataModule.CliopatriaAPI");
+        //logger.setLevel(Logger.DEBUG);
+        //logger.setLevel(Logger.INFO);
+
+        // Set data version suffix (common for all requests)
+        if (CliopatriaAPI.options.dataVersion) {
+            //dataVersionSuffix = _.str.sprintf("&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion));
+            dataVersionSuffix = _.str.sprintf("&format=jsonp&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion));
+        } else {
+            //dataVersionSuffix = "";
+            dataVersionSuffix = "&format=jsonp";
+        }
+
+        realAPIRootPaths = [];
+        for (var i = 0; i < CliopatriaAPI.options.apiRootPaths.length; ++i) {
+            var apiRootPath = CliopatriaAPI.options.apiRootPaths[i];
+
+            realAPIRootPaths.push(_.str.sprintf("%sv%s/", apiRootPath, CliopatriaAPI.options.apiVersion));
+        }
+        realAPIRootPathsCount = realAPIRootPaths.length;
+
+        if (!realAPIRootPathsCount) {
+            logger.error("DataModule.CliopatriaAPI has no registered API paths");
+        }
+
+        queryPool = {};
+
+        // A function that generates a query by the method name and the parameters
+        generateQuery = function(methodName, parameters) {
+            // Sort parameters
+            var sortedParameterPairs = [];
+            for (var parameterName in parameters) {
+                sortedParameterPairs.push([parameterName, parameters[parameterName]]);
+            }
+            sortedParameterPairs.sort(function(a, b) {return _.str.naturalCmp(a[0], b[0]);});
+
+            // encode them
+            var encodedParametersAsArrayOfStrings = [];
+            for (var parameterPairIndex in sortedParameterPairs) {
+                encodedParametersAsArrayOfStrings.push(encodeURIComponent(sortedParameterPairs[parameterPairIndex][0]) + "=" + encodeURIComponent(sortedParameterPairs[parameterPairIndex][1]));
+            };
+            if (!CliopatriaAPI.options.dataCaching) {
+                encodedParametersAsArrayOfStrings.push("random="+Math.round(Math.random() * 100000));
+            }
+            var encodedParameters = encodedParametersAsArrayOfStrings.join("&");
+
+            // append data version (dv)
+            if (methodName == "getCollectionId") {
+                if (encodedParameters.length) {
+                    encodedParameters += dataVersionSuffix;
+                } else {
+                    encodedParameters += dataVersionSuffix.slice(1);
+                }
+            }
+
+            return methodName + "?" + encodedParameters;
+        };
+
+        // A function that handles API responses (successful JSONPs)
+        window[CliopatriaAPI.options.callbackFunctionName] = function(data) {
+
+            logger.debug("Callback function called with data:", data);
+
+            // Look for an entry in the queue pool
+            //// If the API server says that the query had %aa instead of expected %AA,
+            //// fix the quoted query in the response to match a query that waits in the query pool
+            var queryInData = data.query.replace(/%[a-z0-9]{2}/g, function(v) {return v.toUpperCase();});
+            // Delete all stuff that prepends the actual API query (path including the version)
+            queryInData = queryInData.replace(/^.*\/v[\d]\//, "");
+            var queryPoolEntry = queryPool[queryInData];
+
+            if (!queryPoolEntry) {
+                logger.warn(_.str.sprintf("No data API pool entry found for query %s. Response was wasted.", queryInData), data);
+                return;
+            }
+
+            // Remove the entry from the pool
+            delete queryPool[queryInData];
+
+            // Remove the correspondent JSONP DOM node in order to be able to make the same request again
+            $(queryPoolEntry[4]).remove();
+            // Execute all attached callbacks
+            executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, data.result);
+        };
+
+        // Executes all callbacks on request success or error (timeout)
+        var executeAllCallbacksOfTheQueryPoolEntry = function(queryPoolEntry, result) {
+            logger.info(_.str.sprintf("Executing %d callback%s for request %s", queryPoolEntry[3].length, queryPoolEntry[3].length != 1 ? "s" : "", queryPoolEntry[5]), result);
+            for (var i = 0; i < queryPoolEntry[3].length; i++) {
+                var callback = queryPoolEntry[3][i];
+                if (_.isFunction(callback)) {
+                    try {
+                        callback.call(null, result, queryPoolEntry[5], queryPoolEntry[1], queryPoolEntry[2]);
+                    } catch (e) {
+                        logger.error(_.str.sprintf("An error occured when executing callback %s of %s for data API query %s", i + 1, queryPoolEntry[3].length, queryPoolEntry[5]), e);
+                    }
+                }
+            }
+        };
+
+        // A function that checks expired query pool entries
+        callbackTimeoutExpiryChecker = function() {
+
+            var currentTimestamp = new Date().getTime();
+
+            for(var query in queryPool) {
+                var queryPoolEntry = queryPool[query];
+                logger.debug(_.str.sprintf("Checking callback timeouts: timestamp diff = %d ms for query %s", currentTimestamp - queryPoolEntry[0], query));
+                if (currentTimestamp - queryPoolEntry[0] > CliopatriaAPI.options.callbackTimeout) {
+                    delete queryPool[query];
+                    $(queryPoolEntry[4]).remove();
+                    executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, {
+                        errors: [
+                                 {
+                                     "code": 100,
+                                     "message": "API request failed"
+                                 }
+                             ]
+                    });
+                }
+            }
+        };
+
+        callbackTimeoutExpiryCheckerId = setInterval(callbackTimeoutExpiryChecker, CliopatriaAPI.options.callbackTimeoutCheckingInterval);
+    });
+
+
+    /**
+     * Sends a request to the data API
+     *
+     * @param {string} methodName
+     *          API method name (function)
+     *
+     * @param {object} parameters
+     *          unsorted list of request parameters ({string} key => {string|number} value})
+     *
+     * @param {function} callback
+     *          a function to be executed on success or failure
+     *
+     *          callback function arguments:
+     *              {string} result
+     *              {string} query (HTTP GET request)
+     *              {string} requestedMethodName
+     *              {object} requestedParameters
+     *
+     * //@return {void}
+     * //         nothing, but this may be replaced with a promise in future
+     *
+     * @return {string}
+     *          a uri that has been called
+     */
+    CliopatriaAPI.request = function(methodName, parameters, callback) {
+        assertModuleIsInitialized();
+
+        var query = generateQuery(methodName, parameters);
+
+        // Choose which of the alternative API paths to use
+        var realAPIRootPath;
+        if (realAPIRootPathsCount == 1) {
+            realAPIRootPath = realAPIRootPaths[0];
+        } else {
+            var sumOfCharCodes = 0;
+            for (var i = 0; i < query.length; i++) {
+                sumOfCharCodes += query.charCodeAt(i);
+            }
+            realAPIRootPath = realAPIRootPaths[sumOfCharCodes % realAPIRootPathsCount];
+        }
+        if (!realAPIRootPath) {
+            throw _.str.sprintf("API requsest to Cliopatria cannot be made – api root path is undefined");
+        }
+
+        var requestURI = realAPIRootPath + query;
+
+        var queryPoolEntry = queryPool[query];
+
+        if (!queryPoolEntry) {
+            // If a request with the same parameters is not in the current queue, send it
+
+            // Prepare a real jsonp request
+            logger.info("Sending request", query);
+            var scriptElement = document.createElement('script');
+            scriptElement.type = 'text/javascript';
+            scriptElement.async = true;
+            scriptElement.src = requestURI;
+
+            // Create an entry in the query pool
+            queryPoolEntry = [new Date().getTime(), methodName, parameters, [callback], scriptElement, query];
+            queryPool[query] = queryPoolEntry;
+            logger.debug("New entry in queryPool: ", query/*, queryPoolEntry*/);
+
+            // Start the request
+            document.getElementsByTagName('body')[0].appendChild(scriptElement);
+
+        } else {
+            // If an identical request has been called previously, don't initiate a new query,
+            // simply add a callback to the list of callbacks
+            logger.info("request (imaginary)", query);
+
+
+            //queryPoolEntry[0] = new Date().getTime();
+            queryPoolEntry[3].push(callback);
+        }
+        return requestURI;
+    };
+
+    /**
+     * Module finalizer
+     */
+    CliopatriaAPI.on("stop", function(){
+        clearInterval(callbackTimeoutExpiryCheckerId);
+      });
+
+}, Logger);