(function(window) { var Gitana = window.Gitana; Gitana.AbstractMap = Gitana.AbstractPersistable.extend( /** @lends Gitana.AbstractMap.prototype */ { /** * @constructs * @augments Gitana.AbstractPersistable * * @class Abstract base class for a map of Gitana objects * f * @param {Gitana} driver * @param [Object] object */ constructor: function(driver, object) { ////////////////////////////////////////////////////////////////////////////////////////////// // // PRIVILEGED METHODS // ////////////////////////////////////////////////////////////////////////////////////////////// // auto-manage a list of keys this.__keys = (function() { var list = []; return function(x) { if (!Gitana.isUndefined(x)) { if (x == 'empty') { while (list.length > 0) { list.shift(); } } else { if (!x && x.length) { for (var i = 0; i < x.length; i++) { list.push(x[i]); } } else { list.push(x); } } } return list; }; })(); this.__totalRows = (function() { var _totalRows = null; return function(totalRows) { if (!Gitana.isUndefined(totalRows)) { _totalRows = totalRows; } return _totalRows; }; })(); this.__size = (function() { var _size = null; return function(size) { if (!Gitana.isUndefined(size)) { _size = size; } return _size; }; })(); this.__offset = (function() { var _offset = 0; return function(offset) { if (!Gitana.isUndefined(offset) && offset >= 0) { _offset = offset; } return _offset; }; })(); this.base(driver, object); // in case the incoming object is a state-carrying object (like another map) if (object) { this.chainCopyState(object); } }, refs: function() { var references = []; for (var i = 0; i < this.__keys().length; i++) { var key = this.__keys()[i]; var object = this[key]; if (object.ref) { references.push(object.ref()); } } return references; }, /** * Override to include: * * __keys * __totalRows * __size * __offset * * @param otherObject */ chainCopyState: function(otherObject) { this.base(otherObject); // include keys if (otherObject.__keys) { this.__keys('empty'); for (var i = 0; i < otherObject.__keys().length; i++) { var k = otherObject.__keys()[i]; this.__keys().push(k); } } if (otherObject.__totalRows) { this.__totalRows(otherObject.__totalRows()); } if (otherObject.__size) { this.__size(otherObject.__size()); } if (otherObject.__offset) { this.__offset(otherObject.__offset()); } }, clear: function() { // clear object properties (but not member functions) Gitana.deleteProperties(this, false); // empty keys this.__keys('empty'); }, /** * @override * * Convert the json response object into the things we want to preserve on the object. * This should set the "object" property but may choose to set other properties as well. * * @param response */ handleResponse: function(response) { this.clear(); if (response) { // is it a gitana map? if (response.totalRows && response.size && response.offset) { this.__totalRows(response.totalRows()); this.__size(response.size()); this.__offset(response.offset()); } else { // otherwise assume it is a gitana result set this.__totalRows(response["total_rows"]); this.__size(response["size"]); this.__offset(response["offset"]); } if (response.rows) { // parse array if (Gitana.isArray(response.rows)) { for (var i = 0; i < response.rows.length; i++) { var o = this.buildObject(response.rows[i]); this[o.getId()] = o; this.__keys().push(o.getId()); } } else { // parse object for (var key in response.rows) { if (response.rows.hasOwnProperty(key) && !Gitana.isFunction(response.rows[key])) { var value = response.rows[key]; var o = this.buildObject(value); // determine key var k = (o.getId && o.getId()); if (!k) { k = key; } this[k] = o; this.__keys().push(k); } } } this.__size(this.__keys().length); } else { // otherwise, assume it is key/value pairs // it also might be another Gitana Map for (var key in response) { if (response.hasOwnProperty(key) && !Gitana.isFunction(response[key])) { var value = response[key]; var o = this.buildObject(value); // determine key var k = (o.getId && o.getId()); if (!k) { k = key; } this[k] = o; this[k] = o; this.__keys().push(k); } } this.__size(this.__keys().length); } } }, /** * @abstract * * @param json */ buildObject: function(json) { }, get: function(key) { return this[key]; }, asArray: function() { var array = []; for (var i = 0; i < this.__keys().length; i++) { var k = this.__keys()[i]; array.push(this[k]); } return array; }, size: function(callback) { if (callback) { return this.then(function() { callback.call(this, this.__size()); }); } return this.__size(); }, offset: function(callback) { if (callback) { return this.then(function() { callback.call(this, this.__offset()); }); } return this.__offset(); }, totalRows: function(callback) { if (callback) { return this.then(function() { callback.call(this, this.__totalRows()); }); } return this.__totalRows(); }, /** * Iterates over the map and fires the callback function in SERIAL for each element in the map. * The scope for the callback is the object from the map (i.e. repository object, node object). * * The arguments to the callback function are (key, value) where value is the same as "this". * * NOTE: This works against elements in the map in SERIAL. One at a time. If you are doing concurrent * remote operations for members of the set such that each operation is independent, you may want to use * the eachX() method. * * @chained this * * @param callback */ each: function(callback) { return this.then(function() { // run functions for (var i = 0; i < this.__keys().length; i++) { // key and value from the map var key = this.__keys()[i]; var value = this[key]; // a function that fires our callback // wrap in a closure so that we store the callback and key // note: this = the value wrapped in a chain, so we don't pass in value var f = function(callback, key, index, map) { return function() { callback.call(this, key, this, index); // manually copy resulting value back Gitana.deleteProperties(map[key]); Gitana.copyInto(map[key], this); }; }(callback, key, i, this); // create subchain mounted on this chainable and the run function this.subchain(value).then(f); } return this; }); }, /** * Iterates over the map and fires the callback function in PARALLEL for each element in the map. * The scope for the callback is the object from the map (i.e. repository object, node object). * * The arguments to the callback function are (key, value) where value is the same as "this". * * NOTE: This works against elements in the map in PARALLEL. All map members are fired against at the same * time on separate timeouts. There is no guaranteed order for their completion. If you require serial * execution, use the each() method. * * @chained * * @param callback */ eachX: function(callback) { return this.then(function() { // create an array of functions that invokes the callback for each element in the array var functions = []; for (var i = 0; i < this.__keys().length; i++) { var key = this.__keys()[i]; var value = this[key]; var f = function(callback, key, value, index) { return function() { // NOTE: we're running a parallel chain that is managed for us by the Chain then() method. // we can't change the parallel chain but we can subchain from it // in our subchain we run our method // the parallel chain kind of acts like one-hop noop so that we can take over and do our thing this.subchain(value).then(function() { callback.call(this, key, this, index); }); }; }(callback, key, value, i); functions.push(f); } // kick off all these functions in parallel // adding them to the subchain return this.then(functions); }); }, /** * Iterates over the map and applies the callback filter function to each element. * It should hand back true if it wants to keep the value and false to remove it. * * NOTE: the "this" for the callback is the object from the map. * * @chained * * @param callback */ filter: function(callback) { return this.then(function() { var keysToKeep = []; var keysToRemove = []; for (var i = 0; i < this.__keys().length; i++) { var key = this.__keys()[i]; var object = this[key]; var keepIt = callback.call(object); if (keepIt) { keysToKeep.push(key); } else { keysToRemove.push(key); } } // remove any keys we don't want from the map for (var i = 0; i < keysToRemove.length; i++) { delete this[keysToRemove[i]]; } // swap keys to keep // NOTE: we first clear the keys but we can't use slice(0,0) since that produces a NEW array // instead, do this shift trick this.__keys('empty'); for (var i = 0; i < keysToKeep.length; i++) { this.__keys().push(keysToKeep[i]); } }); }, /** * Applies a comparator to sort the map. * * If no comparator is applied, the map will be sorted by its modification timestamp (if possible). * * The comparator can be a string that uses dot-notation to identify a field in the JSON that * should be sorted. (example: "title" or "property1.property2.property3") * * Finally, the comparator can be a function. It takes (previousValue, currentValue) and hands back: * -1 if the currentValue is less than the previousValue (should be sorted lower) * 0 if they are equivalent * 1 if they currentValue is greater than the previousValue (should be sorted higher) * * @chained * * @param comparator */ sort: function(comparator) { return this.then(function() { // build a temporary array of values var array = []; for (var i = 0; i < this.__keys().length; i++) { var key = this.__keys()[i]; array.push(this[key]); } // sort the array array.sort(comparator); // now reset keys according to the order of the array this.__keys("empty"); for (var i = 0; i < array.length; i++) { this.__keys().push(array[i].getId()); } }); }, /** * Limits the number of elements in the map. * * @chained * * @param size */ limit: function(size) { return this.then(function() { var keysToRemove = []; if (size > this.__keys().length) { // keep everything return; } // figure out which keys to remove for (var i = 0; i < this.__keys().length; i++) { if (i >= size) { keysToRemove.push(this.__keys()[i]); } } // truncate the keys // NOTE: we can't use slice here since that produces a new array while (this.__keys().length > size) { this.__keys().pop(); } // remove any keys to remove from map for (var i = 0; i < keysToRemove.length; i++) { delete this[keysToRemove[i]]; } // reset the size this.__size(this.__keys().length); }); }, /** * Paginates elements in the map. * * @chained * * @param pagination */ paginate: function(pagination) { return this.then(function() { var skip = pagination.skip; var limit = pagination.limit; var keysToRemove = []; // figure out which keys to remove for (var i = 0; i < this.__keys().length; i++) { if (i < skip || i >= skip + limit) { keysToRemove.push(this.__keys()[i]); } } // truncate the keys // NOTE: we can't use slice here since that produces a new array while (this.__keys().length > limit + skip) { this.__keys().pop(); } // remove any keys to remove from map for (var i = 0; i < keysToRemove.length; i++) { delete this[keysToRemove[i]]; } // reset the limit this.__size(this.__keys().length); }); }, /** * Counts the number of elements in the map and fires it into a callback function. */ count: function(callback) { if (callback) { return this.then(function() { callback.call(this, this.__keys().length); }); } return this.__keys().length; }, /** * Keeps the first element in the map */ keepOne: function(emptyHandler) { var self = this; var json = {}; if (this.__keys().length > 0) { json = this[this.__keys()[0]]; } var chainable = this.buildObject(json); return this.subchain(chainable).then(function() { var chain = this; this.subchain(self).then(function() { if (this.__keys().length > 0) { var obj = this[this.__keys()[0]]; if (chain.loadFrom) { // for objects, like nodes or branches chain.loadFrom(obj); } else { // non-objects? (i.e. binary or attachment maps) chain.handleResponse(obj); } } else { var err = new Gitana.Error(); err.name = "Empty Map"; err.message = "The map doesn't have any elements in it"; if (emptyHandler) { return emptyHandler.call(self, err); } else { this.error(err); } } }); }); }, /** * Selects an individual element from the map and continues the chain. * * @param key */ select: function(key) { var self = this; var json = {}; if (this[key]) { json = this[key]; } // what we hand back var result = this.subchain(this.buildObject(json)); // preload some work return result.then(function() { var chain = this; this.subchain(self).then(function() { var obj = this[key]; if (!obj) { var err = new Gitana.Error(); err.name = "No element with key: " + key; err.message = err.name; this.error(err); return false; } if (result.loadFrom) { // for objects, like nodes or branches chain.loadFrom(obj); } else { // non-objects? (i.e. binary or attachment maps) chain.handleResponse(obj); } }); }); } }); })(window);