annotate src/DML/MainVisBundle/Resources/assets/marionette/modules/DataModule/DataModule.10-CliopatriaAPI.js @ 1:f38015048f48 tip

Added GPL
author Daniel Wolff
date Sat, 13 Feb 2016 20:43:38 +0100
parents 493bcb69166c
children
rev   line source
Daniel@0 1 "use strict";
Daniel@0 2 /**
Daniel@0 3 * The module wraps all API calls to the data server and abstracts JSONP calls.
Daniel@0 4 *
Daniel@0 5 * API spec: https://code.soundsoftware.ac.uk/projects/dml/wiki/Vis_API_-_front_end_~_data_server
Daniel@0 6 *
Daniel@0 7 *
Daniel@0 8 * Module options:
Daniel@0 9 *
Daniel@0 10 * apiRootPaths
Daniel@0 11 * A pool of URLs from where the data are obtained. Multiple entry points (domains) can be used.
Daniel@0 12 * The choice of a particular root path is "random", i.e. this is a "hash" function of a query contents:
Daniel@0 13 * "doThis?x=1&y=1" -> path 1 of 3
Daniel@0 14 * "doThat?x=1&y=1" -> path 3
Daniel@0 15 * "doThat?x=1&y=123" -> path 1
Daniel@0 16 * "doThat?x=1&y=2" -> path 2
Daniel@0 17 * ...
Daniel@0 18 * "doThis?x=1&y=1" -> path 1
Daniel@0 19 *
Daniel@0 20 * The same query is always requested from the same instance (domain) to facilitate HTTP caching.
Daniel@0 21 * // instead of http:// and https:// can be used for protocol auto-selection.
Daniel@0 22 * Trailing slash is compulsory.
Daniel@0 23 *
Daniel@0 24 * Examples:
Daniel@0 25 * ["//127.0.0.1/"],
Daniel@0 26 * ["//127.0.0.1/my-path/to/dml-api/"],
Daniel@0 27 * ["//a.example.com/dml-api/", "//b.example.com/dml-api/", "//c.example.com/dml-api/"],
Daniel@0 28 *
Daniel@0 29 * apiVersion
Daniel@0 30 * A natural number representing the version of the data API (i.e. the available features of the interface)
Daniel@0 31 *
Daniel@0 32 * Examples:
Daniel@0 33 * 1
Daniel@0 34 * 42
Daniel@0 35 *
Daniel@0 36 * dataVersion
Daniel@0 37 * A natural number or a string representing the version of the selected music library.
Daniel@0 38 * As API calls are supplemented with dv=[dataVersion], a change in the value of dataVersion drops the HTTP cache.
Daniel@0 39 * dataVersion is delivered to the front end with the contents of the web page.
Daniel@0 40 *
Daniel@0 41 * Examples:
Daniel@0 42 * 42
Daniel@0 43 * "2015-01-01"
Daniel@0 44 *
Daniel@0 45 */
Daniel@0 46 App.module("DataModule.CliopatriaAPI", function(CliopatriaAPI, App, Backbone, Marionette, $, _, Logger) {
Daniel@0 47 // Prevent auto start
Daniel@0 48 CliopatriaAPI.startWithParent = false;
Daniel@0 49
Daniel@0 50 // Define module options
Daniel@0 51 var defaultModuleOptions = {
Daniel@0 52 // if a request is not returned in this number of ms, handle it as failed
Daniel@0 53 callbackTimeout: 30000,
Daniel@0 54
Daniel@0 55 // expired requests are being checked this often (ms)
Daniel@0 56 callbackTimeoutCheckingInterval: 1000,
Daniel@0 57
Daniel@0 58 // callback of JSONP responses
Daniel@0 59 callbackFunctionName: "jsonp_cb",
Daniel@0 60
Daniel@0 61 // optional data version suffix that is appended to all API requests, e.g. "123" → &dv=123
Daniel@0 62 dataVersion: null,
Daniel@0 63
Daniel@0 64 apiRootPaths: [],
Daniel@0 65
Daniel@0 66 apiVersion: 1,
Daniel@0 67
Daniel@0 68 dataCaching: true
Daniel@0 69 };
Daniel@0 70
Daniel@0 71 // =========================================================================
Daniel@0 72 // private variables
Daniel@0 73 var logger = null;
Daniel@0 74
Daniel@0 75 /**
Daniel@0 76 * derived from CliopatriaAPI.options.dataVersion
Daniel@0 77 */
Daniel@0 78 var dataVersionSuffix = null;
Daniel@0 79
Daniel@0 80 /**
Daniel@0 81 * Real API root paths include API version (e.g. http://example.com/path/to/api/v42/)
Daniel@0 82 */
Daniel@0 83 var realAPIRootPaths = null;
Daniel@0 84
Daniel@0 85 /**
Daniel@0 86 * The number of the real API paths
Daniel@0 87 */
Daniel@0 88 var realAPIRootPathsCount = null;
Daniel@0 89
Daniel@0 90
Daniel@0 91 /**
Daniel@0 92 * query pool: an object where keys correspond to sent queries (methodName?param1=1&...&dv=42) and values are:
Daniel@0 93 * [timeSent, methodName, parameters, callbacks, scriptElement, query]
Daniel@0 94 */
Daniel@0 95 var queryPool = null;
Daniel@0 96
Daniel@0 97 /**
Daniel@0 98 * Generates query from API method name and parameters (+ appends data version)
Daniel@0 99 * Parameters are sorted alphabetically, except dv, which is always the last
Daniel@0 100 */
Daniel@0 101 var generateQuery = null;
Daniel@0 102
Daniel@0 103 /**
Daniel@0 104 * a function that is called every callbackTimeoutCheckingInterval ms and checks for expired query pool entries
Daniel@0 105 */
Daniel@0 106 var callbackTimeoutExpiryChecker = null;
Daniel@0 107
Daniel@0 108 /**
Daniel@0 109 * timeout (interval) handler for callbackTimeoutExpiryChecker
Daniel@0 110 */
Daniel@0 111 var callbackTimeoutExpiryCheckerId = null;
Daniel@0 112
Daniel@0 113 /**
Daniel@0 114 * Initialization checker
Daniel@0 115 */
Daniel@0 116 var assertModuleIsInitialized = function() {
Daniel@0 117 if (!logger) {
Daniel@0 118 throw "DataModule.CliopatriaAPI has not been initialized";
Daniel@0 119 }
Daniel@0 120 };
Daniel@0 121
Daniel@0 122 /**
Daniel@0 123 * Module initializer
Daniel@0 124 *
Daniel@0 125 */
Daniel@0 126 CliopatriaAPI.addInitializer(function(options) {
Daniel@0 127
Daniel@0 128 CliopatriaAPI.options = _.extend(defaultModuleOptions, options);
Daniel@0 129
Daniel@0 130 logger = Logger.get("DataModule.CliopatriaAPI");
Daniel@0 131 //logger.setLevel(Logger.DEBUG);
Daniel@0 132 //logger.setLevel(Logger.INFO);
Daniel@0 133
Daniel@0 134 // Set data version suffix (common for all requests)
Daniel@0 135 if (CliopatriaAPI.options.dataVersion) {
Daniel@0 136 //dataVersionSuffix = _.str.sprintf("&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion));
Daniel@0 137 dataVersionSuffix = _.str.sprintf("&format=jsonp&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion));
Daniel@0 138 } else {
Daniel@0 139 //dataVersionSuffix = "";
Daniel@0 140 dataVersionSuffix = "&format=jsonp";
Daniel@0 141 }
Daniel@0 142
Daniel@0 143 realAPIRootPaths = [];
Daniel@0 144 for (var i = 0; i < CliopatriaAPI.options.apiRootPaths.length; ++i) {
Daniel@0 145 var apiRootPath = CliopatriaAPI.options.apiRootPaths[i];
Daniel@0 146
Daniel@0 147 realAPIRootPaths.push(_.str.sprintf("%sv%s/", apiRootPath, CliopatriaAPI.options.apiVersion));
Daniel@0 148 }
Daniel@0 149 realAPIRootPathsCount = realAPIRootPaths.length;
Daniel@0 150
Daniel@0 151 if (!realAPIRootPathsCount) {
Daniel@0 152 logger.error("DataModule.CliopatriaAPI has no registered API paths");
Daniel@0 153 }
Daniel@0 154
Daniel@0 155 queryPool = {};
Daniel@0 156
Daniel@0 157 // A function that generates a query by the method name and the parameters
Daniel@0 158 generateQuery = function(methodName, parameters) {
Daniel@0 159 // Sort parameters
Daniel@0 160 var sortedParameterPairs = [];
Daniel@0 161 for (var parameterName in parameters) {
Daniel@0 162 sortedParameterPairs.push([parameterName, parameters[parameterName]]);
Daniel@0 163 }
Daniel@0 164 sortedParameterPairs.sort(function(a, b) {return _.str.naturalCmp(a[0], b[0]);});
Daniel@0 165
Daniel@0 166 // encode them
Daniel@0 167 var encodedParametersAsArrayOfStrings = [];
Daniel@0 168 for (var parameterPairIndex in sortedParameterPairs) {
Daniel@0 169 encodedParametersAsArrayOfStrings.push(encodeURIComponent(sortedParameterPairs[parameterPairIndex][0]) + "=" + encodeURIComponent(sortedParameterPairs[parameterPairIndex][1]));
Daniel@0 170 };
Daniel@0 171 if (!CliopatriaAPI.options.dataCaching) {
Daniel@0 172 encodedParametersAsArrayOfStrings.push("random="+Math.round(Math.random() * 100000));
Daniel@0 173 }
Daniel@0 174 var encodedParameters = encodedParametersAsArrayOfStrings.join("&");
Daniel@0 175
Daniel@0 176 // append data version (dv)
Daniel@0 177 if (methodName == "getCollectionId") {
Daniel@0 178 if (encodedParameters.length) {
Daniel@0 179 encodedParameters += dataVersionSuffix;
Daniel@0 180 } else {
Daniel@0 181 encodedParameters += dataVersionSuffix.slice(1);
Daniel@0 182 }
Daniel@0 183 }
Daniel@0 184
Daniel@0 185 return methodName + "?" + encodedParameters;
Daniel@0 186 };
Daniel@0 187
Daniel@0 188 // A function that handles API responses (successful JSONPs)
Daniel@0 189 window[CliopatriaAPI.options.callbackFunctionName] = function(data) {
Daniel@0 190
Daniel@0 191 logger.debug("Callback function called with data:", data);
Daniel@0 192
Daniel@0 193 // Look for an entry in the queue pool
Daniel@0 194 //// If the API server says that the query had %aa instead of expected %AA,
Daniel@0 195 //// fix the quoted query in the response to match a query that waits in the query pool
Daniel@0 196 var queryInData = data.query.replace(/%[a-z0-9]{2}/g, function(v) {return v.toUpperCase();});
Daniel@0 197 // Delete all stuff that prepends the actual API query (path including the version)
Daniel@0 198 queryInData = queryInData.replace(/^.*\/v[\d]\//, "");
Daniel@0 199 var queryPoolEntry = queryPool[queryInData];
Daniel@0 200
Daniel@0 201 if (!queryPoolEntry) {
Daniel@0 202 logger.warn(_.str.sprintf("No data API pool entry found for query %s. Response was wasted.", queryInData), data);
Daniel@0 203 return;
Daniel@0 204 }
Daniel@0 205
Daniel@0 206 // Remove the entry from the pool
Daniel@0 207 delete queryPool[queryInData];
Daniel@0 208
Daniel@0 209 // Remove the correspondent JSONP DOM node in order to be able to make the same request again
Daniel@0 210 $(queryPoolEntry[4]).remove();
Daniel@0 211 // Execute all attached callbacks
Daniel@0 212 executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, data.result);
Daniel@0 213 };
Daniel@0 214
Daniel@0 215 // Executes all callbacks on request success or error (timeout)
Daniel@0 216 var executeAllCallbacksOfTheQueryPoolEntry = function(queryPoolEntry, result) {
Daniel@0 217 logger.info(_.str.sprintf("Executing %d callback%s for request %s", queryPoolEntry[3].length, queryPoolEntry[3].length != 1 ? "s" : "", queryPoolEntry[5]), result);
Daniel@0 218 for (var i = 0; i < queryPoolEntry[3].length; i++) {
Daniel@0 219 var callback = queryPoolEntry[3][i];
Daniel@0 220 if (_.isFunction(callback)) {
Daniel@0 221 try {
Daniel@0 222 callback.call(null, result, queryPoolEntry[5], queryPoolEntry[1], queryPoolEntry[2]);
Daniel@0 223 } catch (e) {
Daniel@0 224 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);
Daniel@0 225 }
Daniel@0 226 }
Daniel@0 227 }
Daniel@0 228 };
Daniel@0 229
Daniel@0 230 // A function that checks expired query pool entries
Daniel@0 231 callbackTimeoutExpiryChecker = function() {
Daniel@0 232
Daniel@0 233 var currentTimestamp = new Date().getTime();
Daniel@0 234
Daniel@0 235 for(var query in queryPool) {
Daniel@0 236 var queryPoolEntry = queryPool[query];
Daniel@0 237 logger.debug(_.str.sprintf("Checking callback timeouts: timestamp diff = %d ms for query %s", currentTimestamp - queryPoolEntry[0], query));
Daniel@0 238 if (currentTimestamp - queryPoolEntry[0] > CliopatriaAPI.options.callbackTimeout) {
Daniel@0 239 delete queryPool[query];
Daniel@0 240 $(queryPoolEntry[4]).remove();
Daniel@0 241 executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, {
Daniel@0 242 errors: [
Daniel@0 243 {
Daniel@0 244 "code": 100,
Daniel@0 245 "message": "API request failed"
Daniel@0 246 }
Daniel@0 247 ]
Daniel@0 248 });
Daniel@0 249 }
Daniel@0 250 }
Daniel@0 251 };
Daniel@0 252
Daniel@0 253 callbackTimeoutExpiryCheckerId = setInterval(callbackTimeoutExpiryChecker, CliopatriaAPI.options.callbackTimeoutCheckingInterval);
Daniel@0 254 });
Daniel@0 255
Daniel@0 256
Daniel@0 257 /**
Daniel@0 258 * Sends a request to the data API
Daniel@0 259 *
Daniel@0 260 * @param {string} methodName
Daniel@0 261 * API method name (function)
Daniel@0 262 *
Daniel@0 263 * @param {object} parameters
Daniel@0 264 * unsorted list of request parameters ({string} key => {string|number} value})
Daniel@0 265 *
Daniel@0 266 * @param {function} callback
Daniel@0 267 * a function to be executed on success or failure
Daniel@0 268 *
Daniel@0 269 * callback function arguments:
Daniel@0 270 * {string} result
Daniel@0 271 * {string} query (HTTP GET request)
Daniel@0 272 * {string} requestedMethodName
Daniel@0 273 * {object} requestedParameters
Daniel@0 274 *
Daniel@0 275 * //@return {void}
Daniel@0 276 * // nothing, but this may be replaced with a promise in future
Daniel@0 277 *
Daniel@0 278 * @return {string}
Daniel@0 279 * a uri that has been called
Daniel@0 280 */
Daniel@0 281 CliopatriaAPI.request = function(methodName, parameters, callback) {
Daniel@0 282 assertModuleIsInitialized();
Daniel@0 283
Daniel@0 284 var query = generateQuery(methodName, parameters);
Daniel@0 285
Daniel@0 286 // Choose which of the alternative API paths to use
Daniel@0 287 var realAPIRootPath;
Daniel@0 288 if (realAPIRootPathsCount == 1) {
Daniel@0 289 realAPIRootPath = realAPIRootPaths[0];
Daniel@0 290 } else {
Daniel@0 291 var sumOfCharCodes = 0;
Daniel@0 292 for (var i = 0; i < query.length; i++) {
Daniel@0 293 sumOfCharCodes += query.charCodeAt(i);
Daniel@0 294 }
Daniel@0 295 realAPIRootPath = realAPIRootPaths[sumOfCharCodes % realAPIRootPathsCount];
Daniel@0 296 }
Daniel@0 297 if (!realAPIRootPath) {
Daniel@0 298 throw _.str.sprintf("API requsest to Cliopatria cannot be made – api root path is undefined");
Daniel@0 299 }
Daniel@0 300
Daniel@0 301 var requestURI = realAPIRootPath + query;
Daniel@0 302
Daniel@0 303 var queryPoolEntry = queryPool[query];
Daniel@0 304
Daniel@0 305 if (!queryPoolEntry) {
Daniel@0 306 // If a request with the same parameters is not in the current queue, send it
Daniel@0 307
Daniel@0 308 // Prepare a real jsonp request
Daniel@0 309 logger.info("Sending request", query);
Daniel@0 310 var scriptElement = document.createElement('script');
Daniel@0 311 scriptElement.type = 'text/javascript';
Daniel@0 312 scriptElement.async = true;
Daniel@0 313 scriptElement.src = requestURI;
Daniel@0 314
Daniel@0 315 // Create an entry in the query pool
Daniel@0 316 queryPoolEntry = [new Date().getTime(), methodName, parameters, [callback], scriptElement, query];
Daniel@0 317 queryPool[query] = queryPoolEntry;
Daniel@0 318 logger.debug("New entry in queryPool: ", query/*, queryPoolEntry*/);
Daniel@0 319
Daniel@0 320 // Start the request
Daniel@0 321 document.getElementsByTagName('body')[0].appendChild(scriptElement);
Daniel@0 322
Daniel@0 323 } else {
Daniel@0 324 // If an identical request has been called previously, don't initiate a new query,
Daniel@0 325 // simply add a callback to the list of callbacks
Daniel@0 326 logger.info("request (imaginary)", query);
Daniel@0 327
Daniel@0 328
Daniel@0 329 //queryPoolEntry[0] = new Date().getTime();
Daniel@0 330 queryPoolEntry[3].push(callback);
Daniel@0 331 }
Daniel@0 332 return requestURI;
Daniel@0 333 };
Daniel@0 334
Daniel@0 335 /**
Daniel@0 336 * Module finalizer
Daniel@0 337 */
Daniel@0 338 CliopatriaAPI.on("stop", function(){
Daniel@0 339 clearInterval(callbackTimeoutExpiryCheckerId);
Daniel@0 340 });
Daniel@0 341
Daniel@0 342 }, Logger);