(function($) { var Alpaca = $.alpaca; var ONE_HOUR = 3600000; Alpaca.Connector = Base.extend( /** * @lends Alpaca.Connector.prototype */ { /** * @constructs * @class Connects Alpaca to remote data stores. * @param {String} id Connector ID * @param {Object} config Connector Config */ constructor: function(id, config) { this.id = id; this.config = config; // helper function to determine if a resource is a uri this.isUri = function(resource) { return !Alpaca.isEmpty(resource) && Alpaca.isUri(resource); }; this.cache = new AjaxCache('URL', true, ONE_HOUR); }, /** * Makes initial connections to data source. * * @param {Function} onSuccess onSuccess callback. * @param {Function} onError onError callback. */ connect: function (onSuccess, onError) { onSuccess(); }, /** * Loads a template (HTML or Text). * * If the source is a URI, then it is loaded. * If it is not a URI, then the source is simply handed back. * * @param {Object|String} source Source to be loaded. * @param {Function} onSuccess onSuccess callback. * @param {Function} onError onError callback. */ loadTemplate : function (source, onSuccess, onError) { if (!Alpaca.isEmpty(source)) { if (Alpaca.isUri(source)) { this.loadUri(source, false, function(loadedData) { if (onSuccess && Alpaca.isFunction(onSuccess)) { onSuccess(loadedData); } }, function (loadError) { if (onError && Alpaca.isFunction(onError)) { onError(loadError); } }); } else { onSuccess(source); } } else { onError({ "message":"Empty data source.", "reason": "TEMPLATE_LOADING_ERROR" }); } }, /** * Loads JSON data. * * @param {Object|String} resource Resource to be loaded * @param {Object} resources Map of resources * @param {Function} onSuccess onSuccess callback * @param {Function} onError onError callback */ loadData: function (resource, resources, successCallback, errorCallback) { return this._handleLoadJsonResource(resource, successCallback, errorCallback); }, /** * Loads JSON schema. * * @param {Object|String} resource Resource to be loaded * @param {Object} resources Map of resources * @param {Function} onSuccess onSuccess callback * @param {Function} onError onError callback */ loadSchema: function (resource, resources, successCallback, errorCallback) { return this._handleLoadJsonResource(resource, successCallback, errorCallback); }, /** * Loads JSON options. * * @param {Object|String} resource Resource to be loaded * @param {Object} resources Map of resources * @param {Function} onSuccess onSuccess callback * @param {Function} onError onError callback */ loadOptions: function (resource, resources, successCallback, errorCallback) { return this._handleLoadJsonResource(resource, successCallback, errorCallback); }, /** * Loads JSON view. * * @param {Object|String} resource Resource to be loaded * @param {Object} resources Map of resources * @param {Function} onSuccess onSuccess callback * @param {Function} onError onError callback */ loadView: function (resource, resources, successCallback, errorCallback) { return this._handleLoadJsonResource(resource, successCallback, errorCallback); }, /** * Loads schema, form, view and data in a single call. * * @param {Object} resources resources * @param {Function} onSuccess onSuccess callback. * @param {Function} onError onError callback. */ loadAll: function (resources, onSuccess, onError) { var self = this; var onConnectSuccess = function() { var dataSource = resources.dataSource; var schemaSource = resources.schemaSource; var optionsSource = resources.optionsSource; var viewSource = resources.viewSource; // we allow "schema" to contain a URI as well (backwards-compatibility) if (!schemaSource && typeof(resources.schema) === "string") { schemaSource = resources.schema; } // we allow "options" to contain a URI as well (backwards-compatibility) if (!optionsSource && typeof(resources.options) === "string") { optionsSource = resources.options; } // we allow "view" to contain a URI as well (backwards-compatibility) if (!viewSource && typeof(resources.view) === "string") { viewSource = resources.view; } var loaded = {}; var loadCounter = 0; var invocationCount = 0; var successCallback = function() { if (loadCounter === invocationCount) { if (onSuccess && Alpaca.isFunction(onSuccess)) { onSuccess(loaded.data, loaded.options, loaded.schema, loaded.view); } } }; var errorCallback = function (loadError) { if (onError && Alpaca.isFunction(onError)) { onError(loadError); } }; // count out the total # of invokes we're going to fire off if (dataSource) { invocationCount++; } if (schemaSource) { invocationCount++; } if (optionsSource) { invocationCount++; } if (viewSource) { invocationCount++; } if (invocationCount === 0) { // nothing to invoke, so just hand back successCallback(); return; } var doMerge = function(p, v1, v2) { loaded[p] = v1; if (v2) { if ((typeof(loaded[p]) === "object") && (typeof(v2) === "object")) { Alpaca.mergeObject(loaded[p], v2); } else { loaded[p] = v2; } } }; // fire off all of the invokes if (dataSource) { self.loadData(dataSource, resources, function(data) { doMerge("data", resources.data, data); loadCounter++; successCallback(); }, errorCallback); } if (schemaSource) { self.loadSchema(schemaSource, resources, function(schema) { doMerge("schema", resources.schema, schema); loadCounter++; successCallback(); }, errorCallback); } if (optionsSource) { self.loadOptions(optionsSource, resources, function(options) { doMerge("options", resources.options, options); loadCounter++; successCallback(); }, errorCallback); } if (viewSource) { self.loadView(viewSource, resources, function(view) { doMerge("view", resources.view, view); loadCounter++; successCallback(); }, errorCallback); } }; var onConnectError = function(err) { if (onError && Alpaca.isFunction(onError)) { onError(err); } }; self.connect(onConnectSuccess, onConnectError); }, /** * Loads a JSON through Ajax call. * * @param {String} uri location of the json document * @param {Function} onSuccess onSuccess callback. * @param {Function} onError onError callback. */ loadJson : function(uri, onSuccess, onError) { this.loadUri(uri, true, onSuccess, onError); } , /** * Extension point. Set up default ajax configuration for URL retrieval. * * @param uri * @param isJson * @returns {{url: *, type: string}} */ buildAjaxConfig: function(uri, isJson) { var ajaxConfig = { "url": uri, "type": "get" }; if (isJson) { ajaxConfig.dataType = "json"; } else { ajaxConfig.dataType = "text"; } return ajaxConfig; }, /** * Loads a general document through Ajax call. * * This uses jQuery to perform the Ajax call. If you need to customize connectivity to your own remote server, * this would be the appropriate place to do so. * * @param {String} uri uri to be loaded * @param {Boolean} isJson Whether the document is a JSON or not. * @param {Function} onSuccess onSuccess callback. * @param {Function} onError onError callback. */ loadUri : function(uri, isJson, onSuccess, onError) { var self = this; var ajaxConfig = self.buildAjaxConfig(uri, isJson); ajaxConfig["success"] = function(jsonDocument) { self.cache.put(uri, jsonDocument); if (onSuccess && Alpaca.isFunction(onSuccess)) { onSuccess(jsonDocument); } }; ajaxConfig["error"] = function(jqXHR, textStatus, errorThrown) { if (onError && Alpaca.isFunction(onError)) { onError({ "message":"Unable to load data from uri : " + uri, "stage": "DATA_LOADING_ERROR", "details": { "jqXHR" : jqXHR, "textStatus" : textStatus, "errorThrown" : errorThrown } }); } }; var cachedDocument = self.cache.get(uri); if (cachedDocument !== false && onSuccess && Alpaca.isFunction(onSuccess)) { onSuccess(cachedDocument); } else { $.ajax(ajaxConfig); } }, /** * Loads referenced JSON schema. * * @param {Object|String} resource Resource to be loaded. * @param {Function} onSuccess onSuccess callback. * @param {Function} onError onError callback. */ loadReferenceSchema: function (resource, successCallback, errorCallback) { return this._handleLoadJsonResource(resource, successCallback, errorCallback); }, /** * Loads referenced JSON options. * * @param {Object|String} resource Resource to be loaded. * @param {Function} onSuccess onSuccess callback. * @param {Function} onError onError callback. */ loadReferenceOptions: function (resource, successCallback, errorCallback) { return this._handleLoadJsonResource(resource, successCallback, errorCallback); }, _handleLoadJsonResource: function (resource, successCallback, errorCallback) { if (this.isUri(resource)) { this.loadJson(resource, function(loadedResource) { successCallback(loadedResource); }, errorCallback); } else { successCallback(resource); } }, /** * Loads data source (value/text) pairs from a remote source. * This default implementation allows for config to be a string identifying a URL. * * @param config * @param successCallback * @param errorCallback * @returns {*} */ loadDataSource: function (config, successCallback, errorCallback) { return this._handleLoadDataSource(config, successCallback, errorCallback); }, _handleLoadDataSource: function(config, successCallback, errorCallback) { var url = config; if (Alpaca.isObject(url)) { url = config.url; } return this._handleLoadJsonResource(url, successCallback, errorCallback); } }); Alpaca.registerConnectorClass("default", Alpaca.Connector); ///////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////// // // AJAX CACHE // ///////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////// /*! * ajax-cache JavaScript Library v0.2.1 * http://code.google.com/p/ajax-cache/ * * Includes few JSON methods (open source) * http://www.json.org/js.html * * Date: 2010-08-03 */ var AjaxCache = function AjaxCache(type, on, lifetime) { if (on) { this.on = true; } else { this.on = false; } // set default cache lifetime if (lifetime != null) { this.defaultLifetime = lifetime; } // set type this.type = type; // set cache functions according to type switch (this.type) { case 'URL': this.put = this.put_url; break; case 'GET': this.put = this.put_GET; break; } }; AjaxCache.prototype.on = false; AjaxCache.prototype.type = undefined; AjaxCache.prototype.defaultLifetime = 1800000; // 1800000=30min, 300000=5min, 30000=30sec AjaxCache.prototype.items = {}; /** * Caches the request and its response. Type: url * * @param url - url of ajax response * @param response - ajax response * @param lifetime - (optional) sets cache lifetime in miliseconds * @return true on success */ AjaxCache.prototype.put_url = function(url, response, lifetime) { if (lifetime == null) { lifetime = this.defaultLifetime; } var key = this.make_key(url); this.items[key] = {}; this.items[key].key = key; this.items[key].url = url; this.items[key].response = response; this.items[key].expire = (new Date().getTime()) + lifetime; return true; }; /** * Caches the request and its response. Type: GET * * @param url - url of ajax response * @param data - data params (query) * @param response - ajax response * @param lifetime - (optional) sets cache lifetime in miliseconds * @return true on success */ AjaxCache.prototype.put_GET = function(url, data, response, lifetime) { if (lifetime == null) { lifetime = this.defaultLifetime; } var key = this.make_key(url, [ data ]); this.items[key] = {}; this.items[key].key = key; this.items[key].url = url; this.items[key].data = data; this.items[key].response = response; this.items[key].expire = (new Date().getTime()) + lifetime; return true; }; /** * Get cached ajax response * * @param url - url of ajax response * @param params - Array of additional parameters, to make key * @return ajax response or false if such does not exist or is expired */ AjaxCache.prototype.get = function(url, params) { var key = this.make_key(url, params); // if cache does not exist if (this.items[key] == null) { return false; } // if cache expired if (this.items[key].expire < (new Date().getTime())) { return false; } // everything is passed - lets return the response return this.items[key].response; }; /** * Make unique key for each request depending on url and additional parameters * * @param url - url of ajax response * @param params - Array of additional parameters, to make key * @return unique key */ AjaxCache.prototype.make_key = function(url, params) { var key = url; switch (this.type) { case 'URL': break; case 'GET': key += this.stringify(params[0]); break; } return key; }; /** * Flush cache * * @return true on success */ AjaxCache.prototype.flush = function() { // flush all cache cache.items = {}; return true; }; /* * Methods to stringify JavaScript/JSON objects. * * Taken from: http://www.json.org/js.html to be more exact, this file: * http://www.json.org/json2.js copied on 2010-07-19 * * Taken methods: stringify, quote and str * * Methods are slightly modified to best fit ajax-cache functionality * */ AjaxCache.prototype.stringify = function(value, replacer, space) { // The stringify method takes a value and an optional replacer, and an // optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the // keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent // string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return this.str('', { '' : value }); }; AjaxCache.prototype.quote = function(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function(a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a .charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; }; AjaxCache.prototype.str = function(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return this.quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an // array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is // 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object // value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a // placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = this.str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and // wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be // stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { k = rep[i]; if (typeof k === 'string') { v = this.str(k, value); if (v) { partial.push(this.quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.hasOwnProperty.call(value, k)) { v = this.str(k, value); if (v) { partial.push(this.quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } }; })(jQuery);